Forum sistemi kaldırıldı
This commit is contained in:
parent
ddf2eac23e
commit
a663cc0079
36 changed files with 177 additions and 5712 deletions
|
|
@ -1,34 +0,0 @@
|
|||
using System;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public class ForumCategoryDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Icon { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsLocked { get; set; }
|
||||
|
||||
public int TopicCount { get; set; }
|
||||
public int PostCount { get; set; }
|
||||
|
||||
public Guid? LastPostId { get; set; }
|
||||
public DateTime? LastPostDate { get; set; }
|
||||
public string LastPostUserName { get; set; }
|
||||
}
|
||||
|
||||
public class CreateUpdateForumCategoryDto
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Icon { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsLocked { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public class ForumPostDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid? TopicId { get; set; }
|
||||
public Guid? CategoryId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
public AuthorDto Author { get; set; }
|
||||
public Guid? ParentId { get; set; }
|
||||
public int LikeCount { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public int ReplyCount { get; set; }
|
||||
public bool IsLiked { get; set; }
|
||||
public bool IsBestAnswer { get; set; }
|
||||
public bool IsEdited { get; set; }
|
||||
public DateTime? EditedAt { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
public LastReplyDto LastReply { get; set; }
|
||||
public List<ForumPostDto> Replies { get; set; }
|
||||
}
|
||||
|
||||
public class CreatePostRequest
|
||||
{
|
||||
public Guid? TopicId { get; set; }
|
||||
public Guid? CategoryId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public Guid? ParentId { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
}
|
||||
|
||||
public class LastReplyDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public AuthorDto Author { get; set; }
|
||||
public DateTime CreationTime { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public class ForumTopicDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
|
||||
public Guid CategoryId { get; set; }
|
||||
public ForumCategoryDto Category { get; set; }
|
||||
|
||||
public AuthorDto Author { get; set; }
|
||||
|
||||
public int ViewCount { get; set; }
|
||||
public int ReplyCount { get; set; }
|
||||
public int LikeCount { get; set; }
|
||||
|
||||
public bool IsPinned { get; set; }
|
||||
public bool IsLocked { get; set; }
|
||||
public bool IsSolved { get; set; }
|
||||
|
||||
public Guid? LastPostId { get; set; }
|
||||
public DateTime? LastPostDate { get; set; }
|
||||
public string LastPostUserName { get; set; }
|
||||
|
||||
public List<string> Tags { get; set; }
|
||||
public bool IsLiked { get; set; }
|
||||
|
||||
public ForumTopicDto()
|
||||
{
|
||||
Tags = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthorDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Avatar { get; set; }
|
||||
}
|
||||
|
||||
public class CreateForumTopicDto
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public Guid CategoryId { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
public CreateForumTopicDto()
|
||||
{
|
||||
Tags = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateForumTopicDto
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
public UpdateForumTopicDto()
|
||||
{
|
||||
Tags = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ForumTopicListDto : EntityDto<Guid>
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public Guid CategoryId { get; set; }
|
||||
public string CategoryName { get; set; }
|
||||
|
||||
public AuthorDto Author { get; set; }
|
||||
|
||||
public int ViewCount { get; set; }
|
||||
public int ReplyCount { get; set; }
|
||||
public int LikeCount { get; set; }
|
||||
|
||||
public bool IsPinned { get; set; }
|
||||
public bool IsLocked { get; set; }
|
||||
public bool IsSolved { get; set; }
|
||||
|
||||
public DateTime? LastPostDate { get; set; }
|
||||
public string LastPostUserName { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
public ForumTopicListDto()
|
||||
{
|
||||
Tags = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ForumStatsDto
|
||||
{
|
||||
public int TotalCategories { get; set; }
|
||||
public int TotalTopics { get; set; }
|
||||
public int TotalPosts { get; set; }
|
||||
public int TotalUsers { get; set; }
|
||||
public int OnlineUsers { get; set; }
|
||||
public ForumTopicListDto LatestTopic { get; set; }
|
||||
public List<string> PopularTags { get; set; }
|
||||
|
||||
public ForumStatsDto()
|
||||
{
|
||||
PopularTags = new List<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Application.Services;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public interface IForumAppService : IApplicationService
|
||||
{
|
||||
// Category methods
|
||||
Task<List<ForumCategoryDto>> GetCategoriesAsync();
|
||||
Task<ForumCategoryDto> GetCategoryAsync(Guid id);
|
||||
Task<ForumCategoryDto> GetCategoryBySlugAsync(string slug);
|
||||
Task<List<ForumPostDto>> GetPostsByCategoryAsync(Guid categoryId, int page = 1, int pageSize = 20);
|
||||
Task<ForumCategoryDto> CreateCategoryAsync(CreateUpdateForumCategoryDto input);
|
||||
Task<ForumCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateForumCategoryDto input);
|
||||
Task DeleteCategoryAsync(Guid id);
|
||||
|
||||
// Topic methods
|
||||
Task<PagedResultDto<ForumTopicListDto>> GetTopicsAsync(GetForumTopicsInput input);
|
||||
Task<ForumTopicDto> GetTopicAsync(Guid id);
|
||||
Task<ForumTopicDto> CreateTopicAsync(CreateForumTopicDto input);
|
||||
Task<ForumTopicDto> UpdateTopicAsync(Guid id, UpdateForumTopicDto input);
|
||||
Task DeleteTopicAsync(Guid id);
|
||||
|
||||
// Topic actions
|
||||
Task<ForumTopicDto> PinTopicAsync(Guid id);
|
||||
Task<ForumTopicDto> UnpinTopicAsync(Guid id);
|
||||
Task<ForumTopicDto> LockTopicAsync(Guid id);
|
||||
Task<ForumTopicDto> UnlockTopicAsync(Guid id);
|
||||
Task<ForumTopicDto> MarkAsSolvedAsync(Guid id);
|
||||
Task<ForumTopicDto> MarkAsUnsolvedAsync(Guid id);
|
||||
Task LikeTopicAsync(Guid id);
|
||||
Task UnlikeTopicAsync(Guid id);
|
||||
Task IncrementViewCountAsync(Guid id);
|
||||
|
||||
// Search and filters
|
||||
Task<PagedResultDto<ForumTopicListDto>> SearchTopicsAsync(SearchForumTopicsInput input);
|
||||
Task<PagedResultDto<ForumTopicListDto>> GetMyTopicsAsync(PagedAndSortedResultRequestDto input);
|
||||
Task<PagedResultDto<ForumTopicListDto>> GetTopicsByTagAsync(string tag, PagedAndSortedResultRequestDto input);
|
||||
|
||||
// Stats
|
||||
Task<ForumStatsDto> GetStatsAsync();
|
||||
Task<List<string>> GetPopularTagsAsync(int count = 20);
|
||||
}
|
||||
|
||||
public class GetForumTopicsInput : PagedAndSortedResultRequestDto
|
||||
{
|
||||
public Guid? CategoryId { get; set; }
|
||||
public string Filter { get; set; }
|
||||
public bool? IsPinned { get; set; }
|
||||
public bool? IsLocked { get; set; }
|
||||
public bool? IsSolved { get; set; }
|
||||
public string SortBy { get; set; } = "latest"; // latest, popular, mostviewed
|
||||
}
|
||||
|
||||
public class SearchForumTopicsInput : PagedAndSortedResultRequestDto
|
||||
{
|
||||
public string Query { get; set; }
|
||||
public Guid? CategoryId { get; set; }
|
||||
public string Tag { get; set; }
|
||||
public Guid? AuthorId { get; set; }
|
||||
public bool? IsSolved { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using AutoMapper;
|
||||
using Kurs.Platform.Blog;
|
||||
|
||||
namespace Kurs.Platform;
|
||||
|
||||
public class BlogAutoMapperProfile : Profile
|
||||
{
|
||||
public BlogAutoMapperProfile()
|
||||
{
|
||||
// Blog mappings
|
||||
CreateMap<BlogCategory, BlogCategoryDto>();
|
||||
CreateMap<BlogPost, BlogPostDto>();
|
||||
CreateMap<BlogPost, BlogPostListDto>();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,648 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.Users;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
[Authorize]
|
||||
public class ForumAppService : PlatformAppService, IForumAppService
|
||||
{
|
||||
private readonly IRepository<ForumCategory, Guid> _categoryRepository;
|
||||
private readonly IRepository<ForumTopic, Guid> _topicRepository;
|
||||
private readonly IRepository<ForumPost, Guid> _postRepository;
|
||||
private readonly IRepository<ForumTopicTag, Guid> _tagRepository;
|
||||
private readonly IRepository<ForumTopicLike, Guid> _topicLikeRepository;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
|
||||
public ForumAppService(
|
||||
IRepository<ForumCategory, Guid> categoryRepository,
|
||||
IRepository<ForumTopic, Guid> topicRepository,
|
||||
IRepository<ForumPost, Guid> postRepository,
|
||||
IRepository<ForumTopicTag, Guid> tagRepository,
|
||||
IRepository<ForumTopicLike, Guid> topicLikeRepository,
|
||||
ICurrentUser currentUser)
|
||||
{
|
||||
_categoryRepository = categoryRepository;
|
||||
_topicRepository = topicRepository;
|
||||
_postRepository = postRepository;
|
||||
_tagRepository = tagRepository;
|
||||
_topicLikeRepository = topicLikeRepository;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
// Category methods
|
||||
public async Task<List<ForumCategoryDto>> GetCategoriesAsync()
|
||||
{
|
||||
var categories = await _categoryRepository.GetListAsync(x => x.IsActive);
|
||||
|
||||
return ObjectMapper.Map<List<ForumCategory>, List<ForumCategoryDto>>(categories);
|
||||
}
|
||||
|
||||
public async Task<ForumCategoryDto> GetCategoryAsync(Guid id)
|
||||
{
|
||||
var category = await _categoryRepository.GetAsync(id);
|
||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
||||
}
|
||||
|
||||
public async Task<ForumCategoryDto> GetCategoryBySlugAsync(string slug)
|
||||
{
|
||||
var category = await _categoryRepository.FirstOrDefaultAsync(x => x.Slug == slug);
|
||||
if (category == null)
|
||||
{
|
||||
throw new Volo.Abp.Domain.Entities.EntityNotFoundException(typeof(ForumCategory), slug);
|
||||
}
|
||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
||||
}
|
||||
|
||||
public async Task<List<ForumPostDto>> GetPostsByCategoryAsync(Guid categoryId, int page = 1, int pageSize = 20)
|
||||
{
|
||||
var topics = await _topicRepository.GetListAsync(x => x.CategoryId == categoryId);
|
||||
var posts = new List<ForumPostDto>();
|
||||
|
||||
foreach (var topic in topics.Skip((page - 1) * pageSize).Take(pageSize))
|
||||
{
|
||||
var post = new ForumPostDto
|
||||
{
|
||||
Id = topic.Id,
|
||||
CategoryId = topic.CategoryId,
|
||||
Title = topic.Title,
|
||||
Content = topic.Content,
|
||||
ViewCount = topic.ViewCount,
|
||||
ReplyCount = topic.ReplyCount,
|
||||
LikeCount = topic.LikeCount,
|
||||
CreationTime = topic.CreationTime
|
||||
};
|
||||
|
||||
// Get author info
|
||||
post.Author = new AuthorDto
|
||||
{
|
||||
Id = topic.AuthorId,
|
||||
Name = "User"
|
||||
};
|
||||
|
||||
// Get tags
|
||||
var tags = await _tagRepository.GetListAsync(x => x.TopicId == topic.Id);
|
||||
post.Tags = tags.Select(x => x.Tag).ToList();
|
||||
|
||||
// Get last reply
|
||||
var lastPost = await _postRepository
|
||||
.GetListAsync(x => x.TopicId == topic.Id)
|
||||
.ContinueWith(t => t.Result.OrderByDescending(x => x.CreationTime).FirstOrDefault());
|
||||
|
||||
if (lastPost != null)
|
||||
{
|
||||
post.LastReply = new LastReplyDto
|
||||
{
|
||||
Id = lastPost.Id,
|
||||
Author = new AuthorDto
|
||||
{
|
||||
Id = lastPost.AuthorId,
|
||||
Name = "User"
|
||||
},
|
||||
CreationTime = lastPost.CreationTime
|
||||
};
|
||||
}
|
||||
|
||||
posts.Add(post);
|
||||
}
|
||||
|
||||
return posts;
|
||||
}
|
||||
|
||||
public async Task<ForumCategoryDto> CreateCategoryAsync(CreateUpdateForumCategoryDto input)
|
||||
{
|
||||
var category = new ForumCategory(
|
||||
GuidGenerator.Create(),
|
||||
input.Name,
|
||||
input.Slug,
|
||||
input.Description,
|
||||
input.Icon,
|
||||
input.DisplayOrder,
|
||||
CurrentTenant.Id
|
||||
);
|
||||
|
||||
category.IsActive = input.IsActive;
|
||||
category.IsLocked = input.IsLocked;
|
||||
|
||||
await _categoryRepository.InsertAsync(category);
|
||||
|
||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
||||
}
|
||||
|
||||
public async Task<ForumCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateForumCategoryDto input)
|
||||
{
|
||||
var category = await _categoryRepository.GetAsync(id);
|
||||
|
||||
category.Name = input.Name;
|
||||
category.Slug = input.Slug;
|
||||
category.Description = input.Description;
|
||||
category.Icon = input.Icon;
|
||||
category.DisplayOrder = input.DisplayOrder;
|
||||
category.IsActive = input.IsActive;
|
||||
category.IsLocked = input.IsLocked;
|
||||
|
||||
await _categoryRepository.UpdateAsync(category);
|
||||
|
||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
||||
}
|
||||
|
||||
public async Task DeleteCategoryAsync(Guid id)
|
||||
{
|
||||
await _categoryRepository.DeleteAsync(id);
|
||||
}
|
||||
|
||||
// Topic methods
|
||||
public async Task<PagedResultDto<ForumTopicListDto>> GetTopicsAsync(GetForumTopicsInput input)
|
||||
{
|
||||
var query = await _topicRepository.GetQueryableAsync();
|
||||
|
||||
if (input.CategoryId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.CategoryId == input.CategoryId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input.Filter))
|
||||
{
|
||||
query = query.Where(x => x.Title.Contains(input.Filter));
|
||||
}
|
||||
|
||||
if (input.IsPinned.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsPinned == input.IsPinned.Value);
|
||||
}
|
||||
|
||||
if (input.IsLocked.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsLocked == input.IsLocked.Value);
|
||||
}
|
||||
|
||||
if (input.IsSolved.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsSolved == input.IsSolved.Value);
|
||||
}
|
||||
|
||||
// Sorting
|
||||
if (input.SortBy == "popular")
|
||||
{
|
||||
query = query.OrderByDescending(x => x.LikeCount);
|
||||
}
|
||||
else if (input.SortBy == "mostviewed")
|
||||
{
|
||||
query = query.OrderByDescending(x => x.ViewCount);
|
||||
}
|
||||
else // latest
|
||||
{
|
||||
query = query.OrderByDescending(x => x.CreationTime);
|
||||
}
|
||||
|
||||
var totalCount = await AsyncExecuter.CountAsync(query);
|
||||
var topics = await AsyncExecuter.ToListAsync(
|
||||
query.Skip(input.SkipCount).Take(input.MaxResultCount)
|
||||
);
|
||||
|
||||
var topicDtos = new List<ForumTopicListDto>();
|
||||
foreach (var topic in topics)
|
||||
{
|
||||
var dto = ObjectMapper.Map<ForumTopic, ForumTopicListDto>(topic);
|
||||
|
||||
// Get category name
|
||||
var category = await _categoryRepository.GetAsync(topic.CategoryId);
|
||||
dto.CategoryName = category.Name;
|
||||
|
||||
// Get author info
|
||||
dto.Author = new AuthorDto
|
||||
{
|
||||
Id = topic.AuthorId,
|
||||
Name = "User"
|
||||
};
|
||||
|
||||
// Get tags
|
||||
var tags = await _tagRepository.GetListAsync(x => x.TopicId == topic.Id);
|
||||
dto.Tags = tags.Select(x => x.Tag).ToList();
|
||||
|
||||
topicDtos.Add(dto);
|
||||
}
|
||||
|
||||
return new PagedResultDto<ForumTopicListDto>(totalCount, topicDtos);
|
||||
}
|
||||
|
||||
public async Task<ForumTopicDto> GetTopicAsync(Guid id)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
var dto = ObjectMapper.Map<ForumTopic, ForumTopicDto>(topic);
|
||||
|
||||
// Get category
|
||||
dto.Category = ObjectMapper.Map<ForumCategory, ForumCategoryDto>(
|
||||
await _categoryRepository.GetAsync(topic.CategoryId)
|
||||
);
|
||||
|
||||
// Get author info
|
||||
dto.Author = new AuthorDto
|
||||
{
|
||||
Id = topic.AuthorId,
|
||||
Name = "User"
|
||||
};
|
||||
|
||||
// Get tags
|
||||
var tags = await _tagRepository.GetListAsync(x => x.TopicId == topic.Id);
|
||||
dto.Tags = tags.Select(x => x.Tag).ToList();
|
||||
|
||||
// Check if current user liked this topic
|
||||
if (_currentUser.IsAuthenticated)
|
||||
{
|
||||
dto.IsLiked = await _topicLikeRepository.AnyAsync(
|
||||
x => x.TopicId == id && x.UserId == _currentUser.Id.Value
|
||||
);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<ForumTopicDto> CreateTopicAsync(CreateForumTopicDto input)
|
||||
{
|
||||
var topic = new ForumTopic(
|
||||
GuidGenerator.Create(),
|
||||
input.Title,
|
||||
input.Content,
|
||||
input.CategoryId,
|
||||
_currentUser.Id.Value,
|
||||
CurrentTenant.Id
|
||||
);
|
||||
|
||||
await _topicRepository.InsertAsync(topic);
|
||||
|
||||
// Add tags
|
||||
foreach (var tag in input.Tags)
|
||||
{
|
||||
await _tagRepository.InsertAsync(new ForumTopicTag(
|
||||
GuidGenerator.Create(),
|
||||
topic.Id,
|
||||
tag,
|
||||
CurrentTenant.Id
|
||||
));
|
||||
}
|
||||
|
||||
// Update category counts
|
||||
var category = await _categoryRepository.GetAsync(input.CategoryId);
|
||||
category.IncrementTopicCount();
|
||||
await _categoryRepository.UpdateAsync(category);
|
||||
|
||||
return await GetTopicAsync(topic.Id);
|
||||
}
|
||||
|
||||
public async Task<ForumTopicDto> UpdateTopicAsync(Guid id, UpdateForumTopicDto input)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
|
||||
// Check if user is author or has permission
|
||||
if (topic.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Forum.Topics.Update"))
|
||||
{
|
||||
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||
}
|
||||
|
||||
topic.Title = input.Title;
|
||||
topic.Content = input.Content;
|
||||
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
|
||||
// Update tags
|
||||
await _tagRepository.DeleteAsync(x => x.TopicId == id);
|
||||
foreach (var tag in input.Tags)
|
||||
{
|
||||
await _tagRepository.InsertAsync(new ForumTopicTag(
|
||||
GuidGenerator.Create(),
|
||||
topic.Id,
|
||||
tag,
|
||||
CurrentTenant.Id
|
||||
));
|
||||
}
|
||||
|
||||
return await GetTopicAsync(topic.Id);
|
||||
}
|
||||
|
||||
public async Task DeleteTopicAsync(Guid id)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
|
||||
// Check if user is author or has permission
|
||||
if (topic.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Forum.Topics.Delete"))
|
||||
{
|
||||
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||
}
|
||||
|
||||
// Update category counts
|
||||
var category = await _categoryRepository.GetAsync(topic.CategoryId);
|
||||
category.DecrementTopicCount();
|
||||
await _categoryRepository.UpdateAsync(category);
|
||||
|
||||
await _topicRepository.DeleteAsync(id);
|
||||
}
|
||||
|
||||
// Topic actions
|
||||
public async Task<ForumTopicDto> PinTopicAsync(Guid id)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
topic.Pin();
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
return await GetTopicAsync(id);
|
||||
}
|
||||
|
||||
public async Task<ForumTopicDto> UnpinTopicAsync(Guid id)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
topic.Unpin();
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
return await GetTopicAsync(id);
|
||||
}
|
||||
|
||||
public async Task<ForumTopicDto> LockTopicAsync(Guid id)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
topic.Lock();
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
return await GetTopicAsync(id);
|
||||
}
|
||||
|
||||
public async Task<ForumTopicDto> UnlockTopicAsync(Guid id)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
topic.Unlock();
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
return await GetTopicAsync(id);
|
||||
}
|
||||
|
||||
public async Task<ForumTopicDto> MarkAsSolvedAsync(Guid id)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
|
||||
// Only author can mark as solved
|
||||
if (topic.AuthorId != _currentUser.Id)
|
||||
{
|
||||
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||
}
|
||||
|
||||
topic.MarkAsSolved();
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
return await GetTopicAsync(id);
|
||||
}
|
||||
|
||||
public async Task<ForumTopicDto> MarkAsUnsolvedAsync(Guid id)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
|
||||
// Only author can mark as unsolved
|
||||
if (topic.AuthorId != _currentUser.Id)
|
||||
{
|
||||
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||
}
|
||||
|
||||
topic.MarkAsUnsolved();
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
return await GetTopicAsync(id);
|
||||
}
|
||||
|
||||
public async Task LikeTopicAsync(Guid id)
|
||||
{
|
||||
var existingLike = await _topicLikeRepository.FirstOrDefaultAsync(
|
||||
x => x.TopicId == id && x.UserId == _currentUser.Id.Value
|
||||
);
|
||||
|
||||
if (existingLike == null)
|
||||
{
|
||||
await _topicLikeRepository.InsertAsync(new ForumTopicLike(
|
||||
GuidGenerator.Create(),
|
||||
id,
|
||||
_currentUser.Id.Value,
|
||||
CurrentTenant.Id
|
||||
));
|
||||
|
||||
// Update like count
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
var likeCount = await _topicLikeRepository.CountAsync(x => x.TopicId == id);
|
||||
topic.UpdateLikeCount((int)likeCount);
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UnlikeTopicAsync(Guid id)
|
||||
{
|
||||
var existingLike = await _topicLikeRepository.FirstOrDefaultAsync(
|
||||
x => x.TopicId == id && x.UserId == _currentUser.Id.Value
|
||||
);
|
||||
|
||||
if (existingLike != null)
|
||||
{
|
||||
await _topicLikeRepository.DeleteAsync(existingLike);
|
||||
|
||||
// Update like count
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
var likeCount = await _topicLikeRepository.CountAsync(x => x.TopicId == id);
|
||||
topic.UpdateLikeCount((int)likeCount);
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
}
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
public async Task IncrementViewCountAsync(Guid id)
|
||||
{
|
||||
var topic = await _topicRepository.GetAsync(id);
|
||||
topic.IncrementViewCount();
|
||||
await _topicRepository.UpdateAsync(topic);
|
||||
}
|
||||
|
||||
// Search and filters
|
||||
public async Task<PagedResultDto<ForumTopicListDto>> SearchTopicsAsync(SearchForumTopicsInput input)
|
||||
{
|
||||
var query = await _topicRepository.GetQueryableAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input.Query))
|
||||
{
|
||||
query = query.Where(x =>
|
||||
x.Title.Contains(input.Query) ||
|
||||
x.Content.Contains(input.Query)
|
||||
);
|
||||
}
|
||||
|
||||
if (input.CategoryId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.CategoryId == input.CategoryId.Value);
|
||||
}
|
||||
|
||||
if (input.AuthorId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.AuthorId == input.AuthorId.Value);
|
||||
}
|
||||
|
||||
if (input.IsSolved.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsSolved == input.IsSolved.Value);
|
||||
}
|
||||
|
||||
// Search by tag
|
||||
if (!string.IsNullOrWhiteSpace(input.Tag))
|
||||
{
|
||||
var topicIds = await _tagRepository
|
||||
.GetListAsync(x => x.Tag == input.Tag)
|
||||
.ContinueWith(t => t.Result.Select(x => x.TopicId).ToList());
|
||||
|
||||
query = query.Where(x => topicIds.Contains(x.Id));
|
||||
}
|
||||
|
||||
var totalCount = await AsyncExecuter.CountAsync(query);
|
||||
var topics = await AsyncExecuter.ToListAsync(
|
||||
query.OrderByDescending(x => x.CreationTime)
|
||||
.Skip(input.SkipCount)
|
||||
.Take(input.MaxResultCount)
|
||||
);
|
||||
|
||||
var topicDtos = new List<ForumTopicListDto>();
|
||||
foreach (var topic in topics)
|
||||
{
|
||||
var dto = ObjectMapper.Map<ForumTopic, ForumTopicListDto>(topic);
|
||||
|
||||
// Get category name
|
||||
var category = await _categoryRepository.GetAsync(topic.CategoryId);
|
||||
dto.CategoryName = category.Name;
|
||||
|
||||
// Get author info
|
||||
dto.Author = new AuthorDto
|
||||
{
|
||||
Id = topic.AuthorId,
|
||||
Name = "User"
|
||||
};
|
||||
|
||||
// Get tags
|
||||
var tags = await _tagRepository.GetListAsync(x => x.TopicId == topic.Id);
|
||||
dto.Tags = tags.Select(x => x.Tag).ToList();
|
||||
|
||||
topicDtos.Add(dto);
|
||||
}
|
||||
|
||||
return new PagedResultDto<ForumTopicListDto>(totalCount, topicDtos);
|
||||
}
|
||||
|
||||
public async Task<PagedResultDto<ForumTopicListDto>> GetMyTopicsAsync(PagedAndSortedResultRequestDto input)
|
||||
{
|
||||
var searchInput = new GetForumTopicsInput
|
||||
{
|
||||
MaxResultCount = input.MaxResultCount,
|
||||
SkipCount = input.SkipCount,
|
||||
Sorting = input.Sorting
|
||||
};
|
||||
|
||||
var query = await _topicRepository.GetQueryableAsync();
|
||||
query = query.Where(x => x.AuthorId == _currentUser.Id.Value);
|
||||
|
||||
var totalCount = await AsyncExecuter.CountAsync(query);
|
||||
var topics = await AsyncExecuter.ToListAsync(
|
||||
query.OrderByDescending(x => x.CreationTime)
|
||||
.Skip(input.SkipCount)
|
||||
.Take(input.MaxResultCount)
|
||||
);
|
||||
|
||||
var topicDtos = new List<ForumTopicListDto>();
|
||||
foreach (var topic in topics)
|
||||
{
|
||||
var dto = ObjectMapper.Map<ForumTopic, ForumTopicListDto>(topic);
|
||||
|
||||
// Get category name
|
||||
var category = await _categoryRepository.GetAsync(topic.CategoryId);
|
||||
dto.CategoryName = category.Name;
|
||||
|
||||
// Get author info
|
||||
dto.Author = new AuthorDto
|
||||
{
|
||||
Id = topic.AuthorId,
|
||||
Name = _currentUser.Name ?? _currentUser.UserName
|
||||
};
|
||||
|
||||
// Get tags
|
||||
var tags = await _tagRepository.GetListAsync(x => x.TopicId == topic.Id);
|
||||
dto.Tags = tags.Select(x => x.Tag).ToList();
|
||||
|
||||
topicDtos.Add(dto);
|
||||
}
|
||||
|
||||
return new PagedResultDto<ForumTopicListDto>(totalCount, topicDtos);
|
||||
}
|
||||
|
||||
public async Task<PagedResultDto<ForumTopicListDto>> GetTopicsByTagAsync(string tag, PagedAndSortedResultRequestDto input)
|
||||
{
|
||||
var searchInput = new SearchForumTopicsInput
|
||||
{
|
||||
Tag = tag,
|
||||
MaxResultCount = input.MaxResultCount,
|
||||
SkipCount = input.SkipCount,
|
||||
Sorting = input.Sorting
|
||||
};
|
||||
|
||||
return await SearchTopicsAsync(searchInput);
|
||||
}
|
||||
|
||||
// Stats
|
||||
[AllowAnonymous]
|
||||
public async Task<ForumStatsDto> GetStatsAsync()
|
||||
{
|
||||
var stats = new ForumStatsDto
|
||||
{
|
||||
TotalCategories = (int)await _categoryRepository.CountAsync(x => x.IsActive),
|
||||
TotalTopics = (int)await _topicRepository.CountAsync(),
|
||||
TotalPosts = (int)await _postRepository.CountAsync(),
|
||||
TotalUsers = 100, // You should get this from identity service
|
||||
OnlineUsers = 10, // You should implement online user tracking
|
||||
PopularTags = await GetPopularTagsAsync(10)
|
||||
};
|
||||
|
||||
// Get latest topic
|
||||
var latestTopic = await _topicRepository
|
||||
.GetQueryableAsync()
|
||||
.ContinueWith(async t =>
|
||||
{
|
||||
var query = await t;
|
||||
return await AsyncExecuter.FirstOrDefaultAsync(
|
||||
query.OrderByDescending(x => x.CreationTime)
|
||||
);
|
||||
})
|
||||
.Unwrap();
|
||||
|
||||
if (latestTopic != null)
|
||||
{
|
||||
var dto = ObjectMapper.Map<ForumTopic, ForumTopicListDto>(latestTopic);
|
||||
|
||||
// Get category name
|
||||
var category = await _categoryRepository.GetAsync(latestTopic.CategoryId);
|
||||
dto.CategoryName = category.Name;
|
||||
|
||||
// Get author info
|
||||
dto.Author = new AuthorDto
|
||||
{
|
||||
Id = latestTopic.AuthorId,
|
||||
Name = "User"
|
||||
};
|
||||
|
||||
stats.LatestTopic = dto;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
public async Task<List<string>> GetPopularTagsAsync(int count = 20)
|
||||
{
|
||||
var tags = await _tagRepository.GetListAsync();
|
||||
|
||||
return tags
|
||||
.GroupBy(x => x.Tag)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.Take(count)
|
||||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
using AutoMapper;
|
||||
using Kurs.Platform.Blog;
|
||||
using Kurs.Platform.Forum;
|
||||
|
||||
namespace Kurs.Platform;
|
||||
|
||||
public class PlatformApplicationAutoMapperProfile : Profile
|
||||
{
|
||||
public PlatformApplicationAutoMapperProfile()
|
||||
{
|
||||
/* You can configure your AutoMapper mapping configuration here.
|
||||
* Alternatively, you can split your mapping configurations
|
||||
* into multiple profile classes for a better organization. */
|
||||
|
||||
// Blog mappings
|
||||
CreateMap<BlogCategory, BlogCategoryDto>();
|
||||
CreateMap<BlogPost, BlogPostDto>();
|
||||
CreateMap<BlogPost, BlogPostListDto>();
|
||||
|
||||
// Forum mappings
|
||||
CreateMap<ForumCategory, ForumCategoryDto>();
|
||||
CreateMap<ForumTopic, ForumTopicDto>();
|
||||
CreateMap<ForumTopic, ForumTopicListDto>()
|
||||
.ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => src.CreationTime));
|
||||
}
|
||||
}
|
||||
|
|
@ -6354,16 +6354,6 @@
|
|||
"RequiredPermissionName": "App.Blog",
|
||||
"IsDisabled": false
|
||||
},
|
||||
{
|
||||
"ParentCode": "App.Saas",
|
||||
"Code": "App.Forum",
|
||||
"DisplayName": "App.Forum",
|
||||
"Order": 11,
|
||||
"Url": "/admin/forum",
|
||||
"Icon": "FcReading",
|
||||
"RequiredPermissionName": "App.Forum",
|
||||
"IsDisabled": false
|
||||
},
|
||||
{
|
||||
"ParentCode": "App.Administration",
|
||||
"Code": "Abp.Identity",
|
||||
|
|
@ -6525,10 +6515,6 @@
|
|||
{
|
||||
"Name": "App.Blog",
|
||||
"DisplayName": "App.Blog"
|
||||
},
|
||||
{
|
||||
"Name": "App.Forum",
|
||||
"DisplayName": "App.Forum"
|
||||
}
|
||||
],
|
||||
"PermissionDefinitionRecords": [
|
||||
|
|
@ -6732,14 +6718,6 @@
|
|||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Forum",
|
||||
"Name": "App.Forum",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Forum",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Setting",
|
||||
"Name": "Abp.Account",
|
||||
|
|
@ -7779,38 +7757,6 @@
|
|||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Forum",
|
||||
"Name": "App.Forum.Create",
|
||||
"ParentName": "App.Forum",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Forum",
|
||||
"Name": "App.Forum.Delete",
|
||||
"ParentName": "App.Forum",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Forum",
|
||||
"Name": "App.Forum.Export",
|
||||
"ParentName": "App.Forum",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Forum",
|
||||
"Name": "App.Forum.Update",
|
||||
"ParentName": "App.Forum",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
}
|
||||
],
|
||||
"Sectors": [
|
||||
|
|
@ -20210,40 +20156,6 @@
|
|||
"Abbreviation": "Prof."
|
||||
}
|
||||
],
|
||||
"ForumCategories": [
|
||||
{
|
||||
"Name": "Genel Tartışma",
|
||||
"Slug": "genel-tartisma",
|
||||
"Description": "Her türlü konunun tartışılabileceği genel forum alanı",
|
||||
"Icon": "💬",
|
||||
"Order": 1,
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Name": "Teknik Destek",
|
||||
"Slug": "teknik-destek",
|
||||
"Description": "Teknik sorunlar ve çözümler için destek forumu",
|
||||
"Icon": "🔧",
|
||||
"Order": 2,
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Name": "Öneriler",
|
||||
"Slug": "oneriler",
|
||||
"Description": "Platform geliştirmeleri için öneri ve istekler",
|
||||
"Icon": "💡",
|
||||
"Order": 3,
|
||||
"IsActive": true
|
||||
},
|
||||
{
|
||||
"Name": "Duyurular",
|
||||
"Slug": "duyurular",
|
||||
"Description": "Platform duyuruları ve güncellemeler",
|
||||
"Icon": "📢",
|
||||
"Order": 4,
|
||||
"IsActive": true
|
||||
}
|
||||
],
|
||||
"BlogCategories": [
|
||||
{
|
||||
"Id": "1e97bf2c-dec8-50bb-af20-70e71d752871",
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Kurs.Platform.Forum;
|
||||
using Volo.Abp.Data;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.Guids;
|
||||
|
||||
namespace Kurs.Platform.Data
|
||||
{
|
||||
public class ForumDataSeedContributor : IDataSeedContributor, ITransientDependency
|
||||
{
|
||||
private readonly IRepository<ForumCategory, Guid> _categoryRepository;
|
||||
private readonly IGuidGenerator _guidGenerator;
|
||||
|
||||
public ForumDataSeedContributor(
|
||||
IRepository<ForumCategory, Guid> categoryRepository,
|
||||
IGuidGenerator guidGenerator)
|
||||
{
|
||||
_categoryRepository = categoryRepository;
|
||||
_guidGenerator = guidGenerator;
|
||||
}
|
||||
|
||||
public async Task SeedAsync(DataSeedContext context)
|
||||
{
|
||||
if (await _categoryRepository.AnyAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var categories = new[]
|
||||
{
|
||||
new { Name = "Genel Tartışma", Slug = "genel-tartisma", Description = "Her türlü konunun tartışılabileceği genel forum", Icon = "message-circle", DisplayOrder = 1 },
|
||||
new { Name = "Duyurular", Slug = "duyurular", Description = "Sistem duyuruları ve haberler", Icon = "megaphone", DisplayOrder = 2 },
|
||||
new { Name = "Teknik Destek", Slug = "teknik-destek", Description = "Teknik sorunlar ve çözümler", Icon = "wrench", DisplayOrder = 3 },
|
||||
new { Name = "Öneriler", Slug = "oneriler", Description = "Sistem için öneri ve istekler", Icon = "lightbulb", DisplayOrder = 4 },
|
||||
new { Name = "Eğitim", Slug = "egitim", Description = "Eğitim materyalleri ve kaynaklar", Icon = "book", DisplayOrder = 5 }
|
||||
};
|
||||
|
||||
foreach (var cat in categories)
|
||||
{
|
||||
await _categoryRepository.InsertAsync(
|
||||
new ForumCategory(
|
||||
_guidGenerator.Create(),
|
||||
cat.Name,
|
||||
cat.Slug,
|
||||
cat.Description,
|
||||
cat.Icon,
|
||||
cat.DisplayOrder,
|
||||
context.TenantId
|
||||
),
|
||||
autoSave: true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public class ForumCategory : FullAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Icon { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsLocked { get; set; }
|
||||
|
||||
public int TopicCount { get; set; }
|
||||
public int PostCount { get; set; }
|
||||
|
||||
public Guid? LastPostId { get; set; }
|
||||
public DateTime? LastPostDate { get; set; }
|
||||
public Guid? LastPostUserId { get; set; }
|
||||
|
||||
public virtual ICollection<ForumTopic> Topics { get; set; }
|
||||
|
||||
protected ForumCategory()
|
||||
{
|
||||
Topics = new HashSet<ForumTopic>();
|
||||
}
|
||||
|
||||
public ForumCategory(
|
||||
Guid id,
|
||||
string name,
|
||||
string slug,
|
||||
string description,
|
||||
string icon = null,
|
||||
int displayOrder = 0,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
Name = name;
|
||||
Slug = slug;
|
||||
Description = description;
|
||||
Icon = icon;
|
||||
DisplayOrder = displayOrder;
|
||||
TenantId = tenantId;
|
||||
IsActive = true;
|
||||
IsLocked = false;
|
||||
TopicCount = 0;
|
||||
PostCount = 0;
|
||||
|
||||
Topics = new HashSet<ForumTopic>();
|
||||
}
|
||||
|
||||
public void UpdateLastPost(Guid postId, Guid userId)
|
||||
{
|
||||
LastPostId = postId;
|
||||
LastPostDate = DateTime.UtcNow;
|
||||
LastPostUserId = userId;
|
||||
}
|
||||
|
||||
public void IncrementTopicCount()
|
||||
{
|
||||
TopicCount++;
|
||||
}
|
||||
|
||||
public void DecrementTopicCount()
|
||||
{
|
||||
if (TopicCount > 0)
|
||||
TopicCount--;
|
||||
}
|
||||
|
||||
public void IncrementPostCount()
|
||||
{
|
||||
PostCount++;
|
||||
}
|
||||
|
||||
public void DecrementPostCount()
|
||||
{
|
||||
if (PostCount > 0)
|
||||
PostCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public class ForumPost : FullAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public Guid TopicId { get; set; }
|
||||
public virtual ForumTopic Topic { get; set; }
|
||||
|
||||
public string Content { get; set; }
|
||||
|
||||
public Guid AuthorId { get; set; }
|
||||
|
||||
public int LikeCount { get; set; }
|
||||
|
||||
public bool IsAcceptedAnswer { get; set; }
|
||||
|
||||
public virtual ICollection<ForumPostLike> Likes { get; set; }
|
||||
|
||||
protected ForumPost()
|
||||
{
|
||||
Likes = new HashSet<ForumPostLike>();
|
||||
}
|
||||
|
||||
public ForumPost(
|
||||
Guid id,
|
||||
Guid topicId,
|
||||
string content,
|
||||
Guid authorId,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
TopicId = topicId;
|
||||
Content = content;
|
||||
AuthorId = authorId;
|
||||
TenantId = tenantId;
|
||||
LikeCount = 0;
|
||||
IsAcceptedAnswer = false;
|
||||
|
||||
Likes = new HashSet<ForumPostLike>();
|
||||
}
|
||||
|
||||
public void UpdateLikeCount(int count)
|
||||
{
|
||||
LikeCount = count;
|
||||
}
|
||||
|
||||
public void MarkAsAcceptedAnswer()
|
||||
{
|
||||
IsAcceptedAnswer = true;
|
||||
}
|
||||
|
||||
public void UnmarkAsAcceptedAnswer()
|
||||
{
|
||||
IsAcceptedAnswer = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
using System;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public class ForumPostLike : CreationAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public Guid PostId { get; set; }
|
||||
public virtual ForumPost Post { get; set; }
|
||||
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
protected ForumPostLike()
|
||||
{
|
||||
}
|
||||
|
||||
public ForumPostLike(
|
||||
Guid id,
|
||||
Guid postId,
|
||||
Guid userId,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
PostId = postId;
|
||||
UserId = userId;
|
||||
TenantId = tenantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public class ForumTopic : FullAuditedAggregateRoot<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
|
||||
public Guid CategoryId { get; set; }
|
||||
public virtual ForumCategory Category { get; set; }
|
||||
|
||||
public Guid AuthorId { get; set; }
|
||||
|
||||
public int ViewCount { get; set; }
|
||||
public int ReplyCount { get; set; }
|
||||
public int LikeCount { get; set; }
|
||||
|
||||
public bool IsPinned { get; set; }
|
||||
public bool IsLocked { get; set; }
|
||||
public bool IsSolved { get; set; }
|
||||
|
||||
public Guid? LastPostId { get; set; }
|
||||
public DateTime? LastPostDate { get; set; }
|
||||
public Guid? LastPostUserId { get; set; }
|
||||
|
||||
public virtual ICollection<ForumPost> Posts { get; set; }
|
||||
public virtual ICollection<ForumTopicTag> Tags { get; set; }
|
||||
public virtual ICollection<ForumTopicLike> Likes { get; set; }
|
||||
|
||||
protected ForumTopic()
|
||||
{
|
||||
Posts = new HashSet<ForumPost>();
|
||||
Tags = new HashSet<ForumTopicTag>();
|
||||
Likes = new HashSet<ForumTopicLike>();
|
||||
}
|
||||
|
||||
public ForumTopic(
|
||||
Guid id,
|
||||
string title,
|
||||
string content,
|
||||
Guid categoryId,
|
||||
Guid authorId,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
Title = title;
|
||||
Content = content;
|
||||
CategoryId = categoryId;
|
||||
AuthorId = authorId;
|
||||
TenantId = tenantId;
|
||||
|
||||
ViewCount = 0;
|
||||
ReplyCount = 0;
|
||||
LikeCount = 0;
|
||||
IsPinned = false;
|
||||
IsLocked = false;
|
||||
IsSolved = false;
|
||||
|
||||
Posts = new HashSet<ForumPost>();
|
||||
Tags = new HashSet<ForumTopicTag>();
|
||||
Likes = new HashSet<ForumTopicLike>();
|
||||
}
|
||||
|
||||
public void IncrementViewCount()
|
||||
{
|
||||
ViewCount++;
|
||||
}
|
||||
|
||||
public void UpdateLastPost(Guid postId, Guid userId)
|
||||
{
|
||||
LastPostId = postId;
|
||||
LastPostDate = DateTime.UtcNow;
|
||||
LastPostUserId = userId;
|
||||
ReplyCount++;
|
||||
}
|
||||
|
||||
public void UpdateLikeCount(int count)
|
||||
{
|
||||
LikeCount = count;
|
||||
}
|
||||
|
||||
public void Pin()
|
||||
{
|
||||
IsPinned = true;
|
||||
}
|
||||
|
||||
public void Unpin()
|
||||
{
|
||||
IsPinned = false;
|
||||
}
|
||||
|
||||
public void Lock()
|
||||
{
|
||||
IsLocked = true;
|
||||
}
|
||||
|
||||
public void Unlock()
|
||||
{
|
||||
IsLocked = false;
|
||||
}
|
||||
|
||||
public void MarkAsSolved()
|
||||
{
|
||||
IsSolved = true;
|
||||
}
|
||||
|
||||
public void MarkAsUnsolved()
|
||||
{
|
||||
IsSolved = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
using System;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public class ForumTopicLike : CreationAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public Guid TopicId { get; set; }
|
||||
public virtual ForumTopic Topic { get; set; }
|
||||
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
protected ForumTopicLike()
|
||||
{
|
||||
}
|
||||
|
||||
public ForumTopicLike(
|
||||
Guid id,
|
||||
Guid topicId,
|
||||
Guid userId,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
TopicId = topicId;
|
||||
UserId = userId;
|
||||
TenantId = tenantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
using System;
|
||||
using Volo.Abp.Domain.Entities;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Kurs.Platform.Forum
|
||||
{
|
||||
public class ForumTopicTag : Entity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public Guid TopicId { get; set; }
|
||||
public virtual ForumTopic Topic { get; set; }
|
||||
|
||||
public string Tag { get; set; }
|
||||
|
||||
protected ForumTopicTag()
|
||||
{
|
||||
}
|
||||
|
||||
public ForumTopicTag(
|
||||
Guid id,
|
||||
Guid topicId,
|
||||
string tag,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
TopicId = topicId;
|
||||
Tag = tag;
|
||||
TenantId = tenantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using Kurs.Languages.EntityFrameworkCore;
|
||||
using Kurs.Platform.Entities;
|
||||
using Kurs.Platform.Blog;
|
||||
using Kurs.Platform.Forum;
|
||||
using Kurs.Settings.EntityFrameworkCore;
|
||||
using Kurs.MailQueue.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -61,14 +60,6 @@ public class PlatformDbContext :
|
|||
public DbSet<BlogPost> BlogPosts { get; set; }
|
||||
public DbSet<BlogCategory> BlogCategories { get; set; }
|
||||
|
||||
// Forum Entities
|
||||
public DbSet<ForumCategory> ForumCategories { get; set; }
|
||||
public DbSet<ForumTopic> ForumTopics { get; set; }
|
||||
public DbSet<ForumPost> ForumPosts { get; set; }
|
||||
public DbSet<ForumTopicTag> ForumTopicTags { get; set; }
|
||||
public DbSet<ForumTopicLike> ForumTopicLikes { get; set; }
|
||||
public DbSet<ForumPostLike> ForumPostLikes { get; set; }
|
||||
|
||||
#region Entities from the modules
|
||||
|
||||
/* Notice: We only implemented IIdentityDbContext and ITenantManagementDbContext
|
||||
|
|
@ -408,11 +399,11 @@ public class PlatformDbContext :
|
|||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + "BlogCategories", PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
|
||||
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
b.Property(x => x.Slug).IsRequired().HasMaxLength(128);
|
||||
b.Property(x => x.Description).HasMaxLength(512);
|
||||
|
||||
|
||||
b.HasIndex(x => x.Slug);
|
||||
});
|
||||
|
||||
|
|
@ -420,110 +411,21 @@ public class PlatformDbContext :
|
|||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + "BlogPosts", PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
|
||||
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
|
||||
b.Property(x => x.Slug).IsRequired().HasMaxLength(256);
|
||||
b.Property(x => x.Summary).IsRequired().HasMaxLength(512);
|
||||
b.Property(x => x.Content).IsRequired();
|
||||
b.Property(x => x.CoverImage).HasMaxLength(512);
|
||||
|
||||
|
||||
b.HasIndex(x => x.Slug);
|
||||
b.HasIndex(x => x.IsPublished);
|
||||
b.HasIndex(x => x.PublishedAt);
|
||||
|
||||
|
||||
b.HasOne(x => x.Category)
|
||||
.WithMany(x => x.Posts)
|
||||
.HasForeignKey(x => x.CategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// Forum Entity Configurations
|
||||
builder.Entity<ForumCategory>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + "ForumCategories", PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
b.Property(x => x.Description).HasMaxLength(512);
|
||||
b.Property(x => x.Icon).HasMaxLength(64);
|
||||
|
||||
b.HasIndex(x => x.DisplayOrder);
|
||||
});
|
||||
|
||||
builder.Entity<ForumTopic>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + "ForumTopics", PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
|
||||
b.Property(x => x.Content).IsRequired();
|
||||
|
||||
b.HasIndex(x => x.CategoryId);
|
||||
b.HasIndex(x => x.IsPinned);
|
||||
b.HasIndex(x => x.LastPostDate);
|
||||
|
||||
b.HasOne(x => x.Category)
|
||||
.WithMany(x => x.Topics)
|
||||
.HasForeignKey(x => x.CategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<ForumPost>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + "ForumPosts", PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Content).IsRequired();
|
||||
|
||||
b.HasIndex(x => x.TopicId);
|
||||
|
||||
b.HasOne(x => x.Topic)
|
||||
.WithMany(x => x.Posts)
|
||||
.HasForeignKey(x => x.TopicId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<ForumTopicTag>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + "ForumTopicTags", PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Tag).IsRequired().HasMaxLength(64);
|
||||
|
||||
b.HasIndex(x => new { x.TopicId, x.Tag }).IsUnique();
|
||||
b.HasIndex(x => x.Tag);
|
||||
|
||||
b.HasOne(x => x.Topic)
|
||||
.WithMany(x => x.Tags)
|
||||
.HasForeignKey(x => x.TopicId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<ForumTopicLike>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + "ForumTopicLikes", PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.HasIndex(x => new { x.TopicId, x.UserId }).IsUnique();
|
||||
|
||||
b.HasOne(x => x.Topic)
|
||||
.WithMany(x => x.Likes)
|
||||
.HasForeignKey(x => x.TopicId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<ForumPostLike>(b =>
|
||||
{
|
||||
b.ToTable(PlatformConsts.DbTablePrefix + "ForumPostLikes", PlatformConsts.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.HasIndex(x => new { x.PostId, x.UserId }).IsUnique();
|
||||
|
||||
b.HasOne(x => x.Post)
|
||||
.WithMany(x => x.Likes)
|
||||
.HasForeignKey(x => x.PostId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,347 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Kurs.Platform.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBlogForumEntities : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PBlogCategories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Slug = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
Icon = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DisplayOrder = table.Column<int>(type: "int", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
PostCount = table.Column<int>(type: "int", nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PBlogCategories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PForumCategories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Slug = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
Icon = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
DisplayOrder = table.Column<int>(type: "int", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsLocked = table.Column<bool>(type: "bit", nullable: false),
|
||||
TopicCount = table.Column<int>(type: "int", nullable: false),
|
||||
PostCount = table.Column<int>(type: "int", nullable: false),
|
||||
LastPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastPostDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PForumCategories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PBlogPosts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Title = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Slug = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Summary = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||
CoverImage = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
ReadTime = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ViewCount = table.Column<int>(type: "int", nullable: false),
|
||||
LikeCount = table.Column<int>(type: "int", nullable: false),
|
||||
CommentCount = table.Column<int>(type: "int", nullable: false),
|
||||
IsPublished = table.Column<bool>(type: "bit", nullable: false),
|
||||
PublishedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
ExtraProperties = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PBlogPosts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PBlogPosts_PBlogCategories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "PBlogCategories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PForumTopics",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Title = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ViewCount = table.Column<int>(type: "int", nullable: false),
|
||||
ReplyCount = table.Column<int>(type: "int", nullable: false),
|
||||
LikeCount = table.Column<int>(type: "int", nullable: false),
|
||||
IsPinned = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsLocked = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsSolved = table.Column<bool>(type: "bit", nullable: false),
|
||||
LastPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastPostDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
ExtraProperties = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PForumTopics", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PForumTopics_PForumCategories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "PForumCategories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PForumPosts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
TopicId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
LikeCount = table.Column<int>(type: "int", nullable: false),
|
||||
IsAcceptedAnswer = table.Column<bool>(type: "bit", nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PForumPosts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PForumPosts_PForumTopics_TopicId",
|
||||
column: x => x.TopicId,
|
||||
principalTable: "PForumTopics",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PForumTopicLikes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
TopicId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PForumTopicLikes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PForumTopicLikes_PForumTopics_TopicId",
|
||||
column: x => x.TopicId,
|
||||
principalTable: "PForumTopics",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PForumTopicTags",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
TopicId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Tag = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PForumTopicTags", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PForumTopicTags_PForumTopics_TopicId",
|
||||
column: x => x.TopicId,
|
||||
principalTable: "PForumTopics",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PForumPostLikes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
PostId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PForumPostLikes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PForumPostLikes_PForumPosts_PostId",
|
||||
column: x => x.PostId,
|
||||
principalTable: "PForumPosts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogCategories_Slug",
|
||||
table: "PBlogCategories",
|
||||
column: "Slug");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogPosts_CategoryId",
|
||||
table: "PBlogPosts",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogPosts_IsPublished",
|
||||
table: "PBlogPosts",
|
||||
column: "IsPublished");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogPosts_PublishedAt",
|
||||
table: "PBlogPosts",
|
||||
column: "PublishedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogPosts_Slug",
|
||||
table: "PBlogPosts",
|
||||
column: "Slug");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PForumCategories_DisplayOrder",
|
||||
table: "PForumCategories",
|
||||
column: "DisplayOrder");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PForumPostLikes_PostId_UserId",
|
||||
table: "PForumPostLikes",
|
||||
columns: new[] { "PostId", "UserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PForumPosts_TopicId",
|
||||
table: "PForumPosts",
|
||||
column: "TopicId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PForumTopicLikes_TopicId_UserId",
|
||||
table: "PForumTopicLikes",
|
||||
columns: new[] { "TopicId", "UserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PForumTopics_CategoryId",
|
||||
table: "PForumTopics",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PForumTopics_IsPinned",
|
||||
table: "PForumTopics",
|
||||
column: "IsPinned");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PForumTopics_LastPostDate",
|
||||
table: "PForumTopics",
|
||||
column: "LastPostDate");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PForumTopicTags_Tag",
|
||||
table: "PForumTopicTags",
|
||||
column: "Tag");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PForumTopicTags_TopicId_Tag",
|
||||
table: "PForumTopicTags",
|
||||
columns: new[] { "TopicId", "Tag" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PBlogPosts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PForumPostLikes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PForumTopicLikes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PForumTopicTags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PBlogCategories");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PForumPosts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PForumTopics");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PForumCategories");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Kurs.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20250620094517_AddBlogForumEntities")]
|
||||
partial class AddBlogForumEntities
|
||||
[Migration("20250622112214_AddBlogEntities")]
|
||||
partial class AddBlogEntities
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
|
|
@ -2435,347 +2435,6 @@ namespace Kurs.Platform.Migrations
|
|||
b.ToTable("PUomCategory", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<int>("DisplayOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Icon")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsLocked")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<DateTime?>("LastPostDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("LastPostId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("LastPostUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<int>("PostCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<int>("TopicCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayOrder");
|
||||
|
||||
b.ToTable("PForumCategories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPost", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AuthorId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsAcceptedAnswer")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<int>("LikeCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("TopicId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TopicId");
|
||||
|
||||
b.ToTable("PForumPosts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPostLike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid>("PostId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PostId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PForumPostLikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopic", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AuthorId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)")
|
||||
.HasColumnName("ConcurrencyStamp");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<string>("ExtraProperties")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("ExtraProperties");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsLocked")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsPinned")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsSolved")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<DateTime?>("LastPostDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("LastPostId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("LastPostUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("LikeCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ReplyCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<int>("ViewCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("IsPinned");
|
||||
|
||||
b.HasIndex("LastPostDate");
|
||||
|
||||
b.ToTable("PForumTopics", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopicLike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("TopicId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TopicId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PForumTopicLikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopicTag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Tag")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("TopicId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Tag");
|
||||
|
||||
b.HasIndex("TopicId", "Tag")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PForumTopicTags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Settings.Entities.SettingDefinition", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -4929,61 +4588,6 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("UomCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPost", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumTopic", "Topic")
|
||||
.WithMany("Posts")
|
||||
.HasForeignKey("TopicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Topic");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPostLike", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumPost", "Post")
|
||||
.WithMany("Likes")
|
||||
.HasForeignKey("PostId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Post");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopic", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumCategory", "Category")
|
||||
.WithMany("Topics")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopicLike", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumTopic", "Topic")
|
||||
.WithMany("Likes")
|
||||
.HasForeignKey("TopicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Topic");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopicTag", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumTopic", "Topic")
|
||||
.WithMany("Tags")
|
||||
.HasForeignKey("TopicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Topic");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Skill", b =>
|
||||
{
|
||||
b.HasOne("SkillType", null)
|
||||
|
|
@ -5160,25 +4764,6 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("Units");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumCategory", b =>
|
||||
{
|
||||
b.Navigation("Topics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPost", b =>
|
||||
{
|
||||
b.Navigation("Likes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopic", b =>
|
||||
{
|
||||
b.Navigation("Likes");
|
||||
|
||||
b.Navigation("Posts");
|
||||
|
||||
b.Navigation("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SkillType", b =>
|
||||
{
|
||||
b.Navigation("Levels");
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Kurs.Platform.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBlogEntities : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PBlogCategories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Slug = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
Icon = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DisplayOrder = table.Column<int>(type: "int", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
PostCount = table.Column<int>(type: "int", nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PBlogCategories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PBlogPosts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Title = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Slug = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Summary = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||
CoverImage = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
ReadTime = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ViewCount = table.Column<int>(type: "int", nullable: false),
|
||||
LikeCount = table.Column<int>(type: "int", nullable: false),
|
||||
CommentCount = table.Column<int>(type: "int", nullable: false),
|
||||
IsPublished = table.Column<bool>(type: "bit", nullable: false),
|
||||
PublishedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
ExtraProperties = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PBlogPosts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PBlogPosts_PBlogCategories_CategoryId",
|
||||
column: x => x.CategoryId,
|
||||
principalTable: "PBlogCategories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogCategories_Slug",
|
||||
table: "PBlogCategories",
|
||||
column: "Slug");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogPosts_CategoryId",
|
||||
table: "PBlogPosts",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogPosts_IsPublished",
|
||||
table: "PBlogPosts",
|
||||
column: "IsPublished");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogPosts_PublishedAt",
|
||||
table: "PBlogPosts",
|
||||
column: "PublishedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PBlogPosts_Slug",
|
||||
table: "PBlogPosts",
|
||||
column: "Slug");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PBlogPosts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PBlogCategories");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2432,347 +2432,6 @@ namespace Kurs.Platform.Migrations
|
|||
b.ToTable("PUomCategory", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<int>("DisplayOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Icon")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsLocked")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<DateTime?>("LastPostDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("LastPostId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("LastPostUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<int>("PostCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<int>("TopicCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayOrder");
|
||||
|
||||
b.ToTable("PForumCategories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPost", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AuthorId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<bool>("IsAcceptedAnswer")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<int>("LikeCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("TopicId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TopicId");
|
||||
|
||||
b.ToTable("PForumPosts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPostLike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid>("PostId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PostId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PForumPostLikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopic", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AuthorId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("nvarchar(40)")
|
||||
.HasColumnName("ConcurrencyStamp");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
||||
b.Property<DateTime?>("DeletionTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("DeletionTime");
|
||||
|
||||
b.Property<string>("ExtraProperties")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("ExtraProperties");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("IsDeleted");
|
||||
|
||||
b.Property<bool>("IsLocked")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsPinned")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsSolved")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("LastModificationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("LastModificationTime");
|
||||
|
||||
b.Property<Guid?>("LastModifierId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<DateTime?>("LastPostDate")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("LastPostId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("LastPostUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("LikeCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ReplyCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<int>("ViewCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("IsPinned");
|
||||
|
||||
b.HasIndex("LastPostDate");
|
||||
|
||||
b.ToTable("PForumTopics", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopicLike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationTime");
|
||||
|
||||
b.Property<Guid?>("CreatorId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("TopicId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TopicId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PForumTopicLikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopicTag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Tag")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("TopicId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Tag");
|
||||
|
||||
b.HasIndex("TopicId", "Tag")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PForumTopicTags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Settings.Entities.SettingDefinition", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -4926,61 +4585,6 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("UomCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPost", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumTopic", "Topic")
|
||||
.WithMany("Posts")
|
||||
.HasForeignKey("TopicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Topic");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPostLike", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumPost", "Post")
|
||||
.WithMany("Likes")
|
||||
.HasForeignKey("PostId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Post");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopic", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumCategory", "Category")
|
||||
.WithMany("Topics")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopicLike", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumTopic", "Topic")
|
||||
.WithMany("Likes")
|
||||
.HasForeignKey("TopicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Topic");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopicTag", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Forum.ForumTopic", "Topic")
|
||||
.WithMany("Tags")
|
||||
.HasForeignKey("TopicId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Topic");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Skill", b =>
|
||||
{
|
||||
b.HasOne("SkillType", null)
|
||||
|
|
@ -5157,25 +4761,6 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("Units");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumCategory", b =>
|
||||
{
|
||||
b.Navigation("Topics");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumPost", b =>
|
||||
{
|
||||
b.Navigation("Likes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Forum.ForumTopic", b =>
|
||||
{
|
||||
b.Navigation("Likes");
|
||||
|
||||
b.Navigation("Posts");
|
||||
|
||||
b.Navigation("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SkillType", b =>
|
||||
{
|
||||
b.Navigation("Levels");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import Layout from './components/layout/Layout';
|
||||
|
|
@ -10,14 +9,8 @@ import About from './pages/About';
|
|||
import Blog from './pages/Blog';
|
||||
import Contact from './pages/Contact';
|
||||
import BlogDetail from './pages/BlogDetail';
|
||||
import LoginWithTenant from './pages/LoginWithTenant';
|
||||
import Register from './pages/Register';
|
||||
import Forum from './pages/Forum';
|
||||
import ForumCategory from './pages/ForumCategory';
|
||||
import Profile from './pages/Profile';
|
||||
import NotFound from './pages/NotFound';
|
||||
import { LanguageProvider } from './context/LanguageContext';
|
||||
import { useAuthStore } from './store/authStore';
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient({
|
||||
|
|
@ -29,24 +22,7 @@ const queryClient = new QueryClient({
|
|||
},
|
||||
});
|
||||
|
||||
// Protected Route Component
|
||||
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
function App() {
|
||||
const { checkAuth } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, [checkAuth]);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<LanguageProvider>
|
||||
|
|
@ -60,53 +36,6 @@ function App() {
|
|||
<Route path="/blog" element={<Blog />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
<Route path="/blog/:id" element={<BlogDetail />} />
|
||||
<Route path="/login" element={<LoginWithTenant />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
||||
{/* Protected Routes */}
|
||||
<Route path="/profile" element={
|
||||
<ProtectedRoute>
|
||||
<Profile />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
{/* Forum Routes */}
|
||||
<Route path="/forum" element={
|
||||
<ProtectedRoute>
|
||||
<Forum />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/forum/new-topic" element={
|
||||
<ProtectedRoute>
|
||||
<Forum />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/forum/search" element={
|
||||
<ProtectedRoute>
|
||||
<Forum />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/forum/my-topics" element={
|
||||
<ProtectedRoute>
|
||||
<Forum />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/forum/category/:slug" element={
|
||||
<ProtectedRoute>
|
||||
<ForumCategory />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/forum/topic/:topicId" element={
|
||||
<ProtectedRoute>
|
||||
<Forum />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/forum/tag/:tag" element={
|
||||
<ProtectedRoute>
|
||||
<Forum />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Menu, X, Globe, LogIn, LogOut, User, MessageSquare, Home, Info, Package, Briefcase, BookOpen, Phone } from "lucide-react";
|
||||
import {
|
||||
Menu,
|
||||
X,
|
||||
Globe,
|
||||
Home,
|
||||
Info,
|
||||
Package,
|
||||
Briefcase,
|
||||
BookOpen,
|
||||
Phone,
|
||||
} from "lucide-react";
|
||||
import Logo from "./Logo";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useLanguage } from "../../context/LanguageContext";
|
||||
import { useAuthStore } from "../../store/authStore";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const { language, setLanguage, t } = useLanguage();
|
||||
const { isAuthenticated, user, logout } = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -27,19 +35,14 @@ const Header: React.FC = () => {
|
|||
const toggleMenu = () => setIsOpen(!isOpen);
|
||||
const toggleLanguage = () => setLanguage(language === "en" ? "tr" : "en");
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const navLinks = [
|
||||
{ name: t("nav.home"), path: "/", icon: Home },
|
||||
{ name: t("nav.about"), path: "/about", icon: Info },
|
||||
{ name: t("nav.products"), path: "/products", icon: Package },
|
||||
{ name: t("nav.services"), path: "/services", icon: Briefcase },
|
||||
{ name: t("nav.blog"), path: "/blog", icon: BookOpen },
|
||||
{ name: t("nav.forum"), path: "/forum", icon: MessageSquare, protected: true },
|
||||
{ name: t("nav.contact"), path: "/contact", icon: Phone },
|
||||
{ name: t("nav.demo"), path: import.meta.env.VITE_KURS_URL },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -57,20 +60,17 @@ const Header: React.FC = () => {
|
|||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center space-x-6">
|
||||
{navLinks.map((link) => {
|
||||
if (link.protected && !isAuthenticated) return null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className="flex items-center space-x-1 font-medium text-sm text-white hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{link.icon && <link.icon size={16} />}
|
||||
<span>{link.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className={`font-medium text-sm text-white hover:text-blue-400 transition-colors ${
|
||||
link.name === "Giriş" || link.name === "Login" ? "bg-blue-600 rounded px-2 py-1" : ""
|
||||
}`}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
|
|
@ -79,42 +79,6 @@ const Header: React.FC = () => {
|
|||
<Globe size={16} />
|
||||
<span>{language.toUpperCase()}</span>
|
||||
</button>
|
||||
|
||||
{/* Auth Buttons */}
|
||||
{isAuthenticated ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
to="/profile"
|
||||
className="flex items-center space-x-2 text-white hover:text-blue-400 transition-colors"
|
||||
>
|
||||
<User size={16} />
|
||||
<span className="text-sm">{user?.name}</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center space-x-1 bg-red-600 text-white px-3 py-1 rounded hover:bg-red-700 transition-colors text-sm"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
<span>Çıkış</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Link
|
||||
to="/login"
|
||||
className="flex items-center space-x-1 text-white hover:text-blue-400 transition-colors text-sm"
|
||||
>
|
||||
<LogIn size={16} />
|
||||
<span>Giriş</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/register"
|
||||
className="bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700 transition-colors text-sm"
|
||||
>
|
||||
Kayıt Ol
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
|
|
@ -132,21 +96,18 @@ const Header: React.FC = () => {
|
|||
<div className="md:hidden bg-gray-900/95 backdrop-blur-sm shadow-lg">
|
||||
<div className="container mx-auto px-4 py-2">
|
||||
<nav className="flex flex-col space-y-4 py-4">
|
||||
{navLinks.map((link) => {
|
||||
if (link.protected && !isAuthenticated) return null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className="flex items-center space-x-2 font-medium text-white hover:text-blue-400 transition-colors"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
{link.icon && <link.icon size={16} />}
|
||||
<span>{link.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
className={`font-medium text-white hover:text-blue-400 transition-colors ${
|
||||
link.name === "Giriş" || link.name === "Login" ? "bg-blue-600 rounded px-2 py-1" : ""
|
||||
}`}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
|
|
@ -155,50 +116,6 @@ const Header: React.FC = () => {
|
|||
<Globe size={16} />
|
||||
<span>{language.toUpperCase()}</span>
|
||||
</button>
|
||||
|
||||
{/* Mobile Auth Buttons */}
|
||||
<div className="pt-4 border-t border-gray-700">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link
|
||||
to="/profile"
|
||||
className="flex items-center space-x-2 text-white hover:text-blue-400 transition-colors mb-3"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<User size={16} />
|
||||
<span>{user?.name}</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
toggleMenu();
|
||||
}}
|
||||
className="flex items-center space-x-1 bg-red-600 text-white px-3 py-1 rounded hover:bg-red-700 transition-colors w-full justify-center"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
<span>Çıkış</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to="/login"
|
||||
className="flex items-center space-x-1 text-white hover:text-blue-400 transition-colors mb-3"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<LogIn size={16} />
|
||||
<span>Giriş</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/register"
|
||||
className="bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700 transition-colors block text-center"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
Kayıt Ol
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,284 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { forumService, ForumCategory, ForumStats } from '../services/api/forum.service';
|
||||
import { MessageSquare, TrendingUp, Lock, Search, Plus, User } from 'lucide-react';
|
||||
import { useLanguage } from '../context/LanguageContext';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
|
||||
const Forum: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
const [categories, setCategories] = useState<ForumCategory[]>([]);
|
||||
const [stats, setStats] = useState<ForumStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showSearchModal, setShowSearchModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadForumData();
|
||||
}, []);
|
||||
|
||||
const loadForumData = async () => {
|
||||
try {
|
||||
const [categoriesData, statsData] = await Promise.all([
|
||||
forumService.getCategories(),
|
||||
forumService.getStats(),
|
||||
]);
|
||||
setCategories(categoriesData);
|
||||
setStats(statsData);
|
||||
} catch (error) {
|
||||
console.error('Forum verileri yüklenemedi:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Forum yükleniyor...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Hero Section */}
|
||||
<div className="relative bg-blue-900 text-white py-12">
|
||||
<div
|
||||
className="absolute inset-0 opacity-20"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url("https://images.pexels.com/photos/3183161/pexels-photo-3183161.jpeg?auto=compress&cs=tinysrgb&w=1920")',
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
></div>
|
||||
<div className="container mx-auto pt-16 px-4 relative">
|
||||
<h1 className="text-5xl font-bold mb-6">{t("forum.title")}</h1>
|
||||
<p className="text-xl max-w-3xl">{t("forum.subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Section */}
|
||||
{stats && (
|
||||
<div className="bg-white shadow-sm border-b">
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.totalTopics}</div>
|
||||
<div className="text-sm text-gray-600">Konu</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.totalPosts}</div>
|
||||
<div className="text-sm text-gray-600">Mesaj</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900">{stats.totalUsers}</div>
|
||||
<div className="text-sm text-gray-600">Üye</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">{stats.onlineUsers}</div>
|
||||
<div className="text-sm text-gray-600">Çevrimiçi</div>
|
||||
</div>
|
||||
</div>
|
||||
{stats.latestMember && (
|
||||
<div className="mt-4 text-center text-sm text-gray-600">
|
||||
En yeni üyemiz: <span className="font-semibold">{stats.latestMember.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Categories */}
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">Kategoriler</h2>
|
||||
|
||||
{categories.length === 0 ? (
|
||||
<div className="bg-white rounded-lg shadow p-8 text-center">
|
||||
<p className="text-gray-500">Henüz kategori bulunmuyor.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id} className="bg-white rounded-xl shadow-sm hover:shadow-md transition-all duration-200 overflow-hidden group">
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
{category.icon && (
|
||||
<span className="text-3xl mr-3">{category.icon}</span>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||
{category.name}
|
||||
</h3>
|
||||
{category.isLocked && (
|
||||
<span className="inline-flex items-center text-xs text-gray-500 mt-1">
|
||||
<Lock className="w-3 h-3 mr-1" />
|
||||
Kilitli
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
|
||||
{category.description || 'Bu kategori için açıklama bulunmuyor.'}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center space-x-4 text-gray-500">
|
||||
<span className="flex items-center">
|
||||
<MessageSquare className="w-4 h-4 mr-1" />
|
||||
{category.topicCount || 0}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<TrendingUp className="w-4 h-4 mr-1" />
|
||||
{category.postCount || 0}
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
to={`/forum/category/${category.slug}`}
|
||||
className="text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
Görüntüle →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{category.lastPost && (
|
||||
<div className="bg-gray-50 px-6 py-3 border-t">
|
||||
<p className="text-xs text-gray-500">Son mesaj:</p>
|
||||
<p className="text-sm font-medium text-gray-700 truncate">
|
||||
{category.lastPost.title}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="mt-8 bg-blue-50 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Hızlı İşlemler</h3>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!isAuthenticated) {
|
||||
navigate('/login');
|
||||
} else if (categories.length > 0) {
|
||||
navigate(`/forum/category/${categories[0].slug}`);
|
||||
}
|
||||
}}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors flex items-center"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Yeni Konu Aç
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowSearchModal(true)}
|
||||
className="bg-white text-gray-700 px-4 py-2 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors flex items-center"
|
||||
>
|
||||
<Search className="w-4 h-4 mr-2" />
|
||||
Forum'da Ara
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!isAuthenticated) {
|
||||
navigate('/login');
|
||||
} else {
|
||||
navigate('/profile?tab=topics');
|
||||
}
|
||||
}}
|
||||
className="bg-white text-gray-700 px-4 py-2 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors flex items-center"
|
||||
>
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Konularım
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Popular Tags */}
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Popüler Etiketler</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['react', 'javascript', 'api', 'authentication', 'devexpress', 'abp-framework', 'ddd'].map((tag) => (
|
||||
<Link
|
||||
key={tag}
|
||||
to={`/forum/tag/${tag}`}
|
||||
className="bg-gray-200 text-gray-700 px-3 py-1 rounded-full text-sm hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
#{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Modal */}
|
||||
{showSearchModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg max-w-2xl w-full">
|
||||
<div className="p-6 border-b">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Forum'da Ara</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter' && searchQuery.trim()) {
|
||||
navigate(`/forum/search?q=${encodeURIComponent(searchQuery)}`);
|
||||
setShowSearchModal(false);
|
||||
}
|
||||
}}
|
||||
className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Arama yapmak için bir şeyler yazın..."
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (searchQuery.trim()) {
|
||||
navigate(`/forum/search?q=${encodeURIComponent(searchQuery)}`);
|
||||
setShowSearchModal(false);
|
||||
}
|
||||
}}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<Search className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-gray-600">
|
||||
<p>İpucu: Başlık, içerik veya etiketlerde arama yapabilirsiniz.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 border-t flex justify-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowSearchModal(false);
|
||||
setSearchQuery('');
|
||||
}}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Forum;
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { forumService, ForumCategory, ForumPost } from '../services/api/forum.service';
|
||||
import { ArrowLeft, MessageSquare, User, Calendar, Eye, Heart, Reply, Plus } from 'lucide-react';
|
||||
import { useLanguage } from '../context/LanguageContext';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { tr } from 'date-fns/locale';
|
||||
|
||||
const ForumCategoryPage: React.FC = () => {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useLanguage();
|
||||
const [category, setCategory] = useState<ForumCategory | null>(null);
|
||||
const [posts, setPosts] = useState<ForumPost[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showNewPostModal, setShowNewPostModal] = useState(false);
|
||||
const [newPost, setNewPost] = useState({ title: '', content: '' });
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (slug) {
|
||||
loadCategoryData();
|
||||
}
|
||||
}, [slug]);
|
||||
|
||||
const loadCategoryData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const categoryData = await forumService.getCategoryBySlug(slug!);
|
||||
setCategory(categoryData);
|
||||
|
||||
const postsData = await forumService.getPostsByCategory(categoryData.id);
|
||||
setPosts(postsData);
|
||||
} catch (error) {
|
||||
console.error('Kategori verileri yüklenemedi:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreatePost = async () => {
|
||||
if (!newPost.title.trim() || !newPost.content.trim()) {
|
||||
alert('Lütfen başlık ve içerik alanlarını doldurun.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await forumService.createPost({
|
||||
categoryId: category!.id,
|
||||
title: newPost.title,
|
||||
content: newPost.content
|
||||
});
|
||||
|
||||
setShowNewPostModal(false);
|
||||
setNewPost({ title: '', content: '' });
|
||||
loadCategoryData(); // Yeni post eklendiğinde listeyi yenile
|
||||
} catch (error) {
|
||||
console.error('Post oluşturulamadı:', error);
|
||||
alert('Post oluşturulurken bir hata oluştu.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Yükleniyor...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!category) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600">Kategori bulunamadı.</p>
|
||||
<Link to="/forum" className="text-blue-600 hover:underline mt-4 inline-block">
|
||||
Forum'a Dön
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<div className="bg-white shadow-sm border-b">
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => navigate('/forum')}
|
||||
className="text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center">
|
||||
{category.icon && <span className="mr-2">{category.icon}</span>}
|
||||
{category.name}
|
||||
</h1>
|
||||
{category.description && (
|
||||
<p className="text-gray-600 mt-1">{category.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowNewPostModal(true)}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors flex items-center"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Yeni Konu
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Posts List */}
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{posts.length === 0 ? (
|
||||
<div className="bg-white rounded-lg shadow p-8 text-center">
|
||||
<MessageSquare className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500 mb-4">Bu kategoride henüz konu bulunmuyor.</p>
|
||||
<button
|
||||
onClick={() => setShowNewPostModal(true)}
|
||||
className="text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
İlk konuyu sen oluştur!
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{posts.map((post) => (
|
||||
<div key={post.id} className="bg-white rounded-lg shadow-sm hover:shadow-md transition-all duration-200">
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<Link
|
||||
to={`/forum/post/${post.id}`}
|
||||
className="text-lg font-semibold text-gray-900 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
<div className="flex items-center space-x-4 mt-2 text-sm text-gray-500">
|
||||
<span className="flex items-center">
|
||||
<User className="w-4 h-4 mr-1" />
|
||||
{post.author?.name || 'Anonim'}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
{formatDistanceToNow(new Date(post.createdAt), {
|
||||
addSuffix: true,
|
||||
locale: tr
|
||||
})}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
{post.viewCount || 0}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<MessageSquare className="w-4 h-4 mr-1" />
|
||||
{post.replyCount || 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
<button className="text-gray-400 hover:text-red-500 transition-colors">
|
||||
<Heart className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="text-sm text-gray-500">{post.likeCount || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{post.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{post.lastReply && (
|
||||
<div className="bg-gray-50 px-6 py-3 border-t flex items-center justify-between">
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Reply className="w-4 h-4 mr-2" />
|
||||
<span>Son yanıt: </span>
|
||||
<span className="font-medium ml-1">{post.lastReply.author?.name}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>
|
||||
{formatDistanceToNow(new Date(post.lastReply.createdAt), {
|
||||
addSuffix: true,
|
||||
locale: tr
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* New Post Modal */}
|
||||
{showNewPostModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6 border-b">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Yeni Konu Oluştur</h2>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Başlık
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newPost.title}
|
||||
onChange={(e) => setNewPost({ ...newPost, title: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Konunuzun başlığını girin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
İçerik
|
||||
</label>
|
||||
<textarea
|
||||
value={newPost.content}
|
||||
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
rows={8}
|
||||
placeholder="Konunuzun içeriğini yazın"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 border-t flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowNewPostModal(false);
|
||||
setNewPost({ title: '', content: '' });
|
||||
}}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||
disabled={submitting}
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreatePost}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? 'Gönderiliyor...' : 'Gönder'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForumCategoryPage;
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { LogIn, Mail, Lock } from 'lucide-react';
|
||||
|
||||
interface LoginFormData {
|
||||
userNameOrEmailAddress: string;
|
||||
password: string;
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { login, isLoading } = useAuthStore();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormData>();
|
||||
|
||||
const onSubmit = async (data: LoginFormData) => {
|
||||
try {
|
||||
// UI uygulaması ile aynı formatta login isteği gönder
|
||||
await login({
|
||||
username: data.userNameOrEmailAddress,
|
||||
password: data.password,
|
||||
});
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
// Error is handled in the store
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Hesabınıza giriş yapın
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Veya{' '}
|
||||
<Link
|
||||
to="/register"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
yeni hesap oluşturun
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
E-posta veya Kullanıcı Adı
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Mail className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('userNameOrEmailAddress', {
|
||||
required: 'E-posta veya kullanıcı adı gereklidir',
|
||||
})}
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 pl-10 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="E-posta veya Kullanıcı Adı"
|
||||
/>
|
||||
</div>
|
||||
{errors.userNameOrEmailAddress && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{errors.userNameOrEmailAddress.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Şifre
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('password', {
|
||||
required: 'Şifre gereklidir',
|
||||
})}
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 pl-10 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Şifre"
|
||||
/>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
{...register('rememberMe')}
|
||||
id="remember-me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
|
||||
Beni hatırla
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<a href="#" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
Şifrenizi mi unuttunuz?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<LogIn className="h-5 w-5 text-blue-500 group-hover:text-blue-400" />
|
||||
</span>
|
||||
{isLoading ? 'Giriş yapılıyor...' : 'Giriş Yap'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { LogIn, Mail, Lock, Building } from 'lucide-react';
|
||||
import { apiClient } from '../services/api/config';
|
||||
import { useLanguage } from '../context/LanguageContext';
|
||||
|
||||
interface LoginFormData {
|
||||
tenantName?: string;
|
||||
userNameOrEmailAddress: string;
|
||||
password: string;
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
|
||||
interface TenantInfo {
|
||||
success: boolean;
|
||||
tenantId?: string;
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
const LoginWithTenant: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { login, isLoading } = useAuthStore();
|
||||
const { t } = useLanguage();
|
||||
const [isMultiTenancyEnabled, setIsMultiTenancyEnabled] = useState(false);
|
||||
const [tenantInfo, setTenantInfo] = useState<TenantInfo | null>(null);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
watch,
|
||||
} = useForm<LoginFormData>({
|
||||
defaultValues: {
|
||||
rememberMe: true
|
||||
}
|
||||
});
|
||||
|
||||
const tenantName = watch('tenantName');
|
||||
|
||||
useEffect(() => {
|
||||
checkMultiTenancy();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantName) {
|
||||
checkTenant(tenantName);
|
||||
} else {
|
||||
setTenantInfo(null);
|
||||
localStorage.removeItem('tenant_id');
|
||||
}
|
||||
}, [tenantName]);
|
||||
|
||||
const checkMultiTenancy = async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/abp/application-configuration');
|
||||
setIsMultiTenancyEnabled(response.data.multiTenancy?.isEnabled || false);
|
||||
} catch (error) {
|
||||
console.error('Failed to check multi-tenancy:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkTenant = async (name: string) => {
|
||||
try {
|
||||
const response = await apiClient.post<TenantInfo>('/api/abp/multi-tenancy/tenants/by-name/' + name);
|
||||
if (response.data.success && response.data.tenantId) {
|
||||
setTenantInfo(response.data);
|
||||
localStorage.setItem('tenant_id', response.data.tenantId);
|
||||
} else {
|
||||
setTenantInfo({ success: false });
|
||||
localStorage.removeItem('tenant_id');
|
||||
}
|
||||
} catch (error) {
|
||||
setTenantInfo({ success: false });
|
||||
localStorage.removeItem('tenant_id');
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: LoginFormData) => {
|
||||
try {
|
||||
await login({
|
||||
username: data.userNameOrEmailAddress,
|
||||
password: data.password,
|
||||
});
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
// Error is handled in the store
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center py-24 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="text-center mb-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-4">
|
||||
<LogIn className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900">
|
||||
{t('login.welcome')}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
{t('login.subtitle')}{' '}
|
||||
<Link
|
||||
to="/register"
|
||||
className="font-medium text-blue-600 hover:text-blue-500 transition-colors"
|
||||
>
|
||||
{t('login.createAccount')}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
{/* Tenant Field - Only show if multi-tenancy is enabled */}
|
||||
{isMultiTenancyEnabled && (
|
||||
<div>
|
||||
<label htmlFor="tenant" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('login.organization')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Building className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('tenantName')}
|
||||
type="text"
|
||||
autoComplete="organization"
|
||||
className="appearance-none relative block w-full px-3 py-3 pl-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder={t('login.organizationPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
{tenantInfo && !tenantInfo.success && tenantName && (
|
||||
<p className="mt-2 text-sm text-red-600">
|
||||
{t('login.organizationNotFound')}
|
||||
</p>
|
||||
)}
|
||||
{tenantInfo && tenantInfo.success && (
|
||||
<p className="mt-2 text-sm text-green-600">
|
||||
✓ {tenantInfo.name} {t('login.organizationFound')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Username/Email Field */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('login.email')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Mail className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('userNameOrEmailAddress', {
|
||||
required: t('login.emailRequired'),
|
||||
})}
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
className="appearance-none relative block w-full px-3 py-3 pl-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder={t('login.emailPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
{errors.userNameOrEmailAddress && (
|
||||
<p className="mt-2 text-sm text-red-600">
|
||||
{errors.userNameOrEmailAddress.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{t('login.password')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('password', {
|
||||
required: t('login.passwordRequired'),
|
||||
})}
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
className="appearance-none relative block w-full px-3 py-3 pl-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="mt-2 text-sm text-red-600">
|
||||
{errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
{...register('rememberMe')}
|
||||
id="remember-me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-700">
|
||||
{t('login.rememberMe')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<a href="#" className="font-medium text-blue-600 hover:text-blue-500 transition-colors">
|
||||
{t('login.forgotPassword')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || (tenantInfo !== null && !tenantInfo.success && !!tenantName)}
|
||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<LogIn className="h-5 w-5 text-blue-500 group-hover:text-blue-400" />
|
||||
</span>
|
||||
{isLoading ? t('login.signingIn') : t('login.signIn')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginWithTenant;
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
import React from "react";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import { User, Mail, Calendar, Shield } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { tr } from "date-fns/locale";
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const { user } = useAuthStore();
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<p className="text-gray-600">Kullanıcı bilgileri yüklenemedi.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12">
|
||||
<div className="container mx-auto pt-16 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-8">Profilim</h1>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-8">
|
||||
<div className="flex items-center mb-8">
|
||||
{user.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.name}
|
||||
className="w-24 h-24 rounded-full object-cover border-4 border-gray-200"
|
||||
onError={(e) => {
|
||||
e.currentTarget.onerror = null; // sonsuz döngüyü önlemek için
|
||||
e.currentTarget.src = "/img/default-profile.png"; // bu senin varsayılan avatar görselin
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-24 h-24 bg-blue-500 rounded-full flex items-center justify-center text-white text-3xl font-bold">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-900">
|
||||
{user.fullName || user.name}
|
||||
</h2>
|
||||
<p className="text-gray-600">@{user.userName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<User className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Kullanıcı Adı</p>
|
||||
<p className="font-medium">{user.userName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Mail className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">E-posta</p>
|
||||
<p className="font-medium">{user.emailAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Calendar className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Kayıt Tarihi</p>
|
||||
<p className="font-medium">
|
||||
{user.creationTime
|
||||
? format(new Date(user.creationTime), "dd MMMM yyyy", {
|
||||
locale: tr,
|
||||
})
|
||||
: "Bilinmiyor"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Shield className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Roller</p>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{user.roles && user.roles.length > 0 ? (
|
||||
user.roles.map((role, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-blue-100 text-blue-700 text-sm rounded"
|
||||
>
|
||||
{role}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-gray-500">Rol atanmamış</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Hesap Durumu
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${
|
||||
user.isActive ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
></div>
|
||||
<span
|
||||
className={`font-medium ${
|
||||
user.isActive ? "text-green-700" : "text-red-700"
|
||||
}`}
|
||||
>
|
||||
{user.isActive ? "Aktif" : "Pasif"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{user.lastLoginTime && (
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
Son giriş:{" "}
|
||||
{format(new Date(user.lastLoginTime), "dd MMMM yyyy HH:mm", {
|
||||
locale: tr,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { RegisterRequest } from '../services/api/auth.service';
|
||||
import { UserPlus, Mail, Lock, User } from 'lucide-react';
|
||||
|
||||
const Register: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { register: registerUser, isLoading } = useAuthStore();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<RegisterRequest & { confirmPassword: string }>();
|
||||
|
||||
const password = watch('password');
|
||||
|
||||
const onSubmit = async (data: RegisterRequest & { confirmPassword: string }) => {
|
||||
try {
|
||||
const { confirmPassword, ...registerData } = data;
|
||||
await registerUser(registerData);
|
||||
navigate('/login');
|
||||
} catch (error) {
|
||||
// Error is handled in the store
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Yeni hesap oluşturun
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Veya{' '}
|
||||
<Link
|
||||
to="/login"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
mevcut hesabınızla giriş yapın
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
Ad
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<User className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('name', {
|
||||
required: 'Ad gereklidir',
|
||||
})}
|
||||
type="text"
|
||||
autoComplete="given-name"
|
||||
className="appearance-none block w-full px-3 py-2 pl-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Ad"
|
||||
/>
|
||||
</div>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="surname" className="block text-sm font-medium text-gray-700">
|
||||
Soyad
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<User className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('surname', {
|
||||
required: 'Soyad gereklidir',
|
||||
})}
|
||||
type="text"
|
||||
autoComplete="family-name"
|
||||
className="appearance-none block w-full px-3 py-2 pl-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Soyad"
|
||||
/>
|
||||
</div>
|
||||
{errors.surname && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.surname.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="userName" className="block text-sm font-medium text-gray-700">
|
||||
Kullanıcı Adı
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<User className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('userName', {
|
||||
required: 'Kullanıcı adı gereklidir',
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: 'Kullanıcı adı en az 3 karakter olmalıdır',
|
||||
},
|
||||
})}
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
className="appearance-none block w-full px-3 py-2 pl-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Kullanıcı Adı"
|
||||
/>
|
||||
</div>
|
||||
{errors.userName && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.userName.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="emailAddress" className="block text-sm font-medium text-gray-700">
|
||||
E-posta
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Mail className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('emailAddress', {
|
||||
required: 'E-posta gereklidir',
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: 'Geçerli bir e-posta adresi giriniz',
|
||||
},
|
||||
})}
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
className="appearance-none block w-full px-3 py-2 pl-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="E-posta"
|
||||
/>
|
||||
</div>
|
||||
{errors.emailAddress && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.emailAddress.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Şifre
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('password', {
|
||||
required: 'Şifre gereklidir',
|
||||
minLength: {
|
||||
value: 6,
|
||||
message: 'Şifre en az 6 karakter olmalıdır',
|
||||
},
|
||||
})}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className="appearance-none block w-full px-3 py-2 pl-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Şifre"
|
||||
/>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
|
||||
Şifre Tekrar
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
{...register('confirmPassword', {
|
||||
required: 'Şifre tekrarı gereklidir',
|
||||
validate: (value) =>
|
||||
value === password || 'Şifreler eşleşmiyor',
|
||||
})}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className="appearance-none block w-full px-3 py-2 pl-10 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Şifre Tekrar"
|
||||
/>
|
||||
</div>
|
||||
{errors.confirmPassword && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.confirmPassword.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<UserPlus className="h-5 w-5 text-blue-500 group-hover:text-blue-400" />
|
||||
</span>
|
||||
{isLoading ? 'Kayıt yapılıyor...' : 'Kayıt Ol'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
import { apiClient } from './config';
|
||||
const { VITE_CDN_URL } = import.meta.env
|
||||
|
||||
export const AVATAR_URL = (id?: string, tenantId?: string) =>
|
||||
`${VITE_CDN_URL}/${tenantId ? 'tenants/' + tenantId : 'host'}/Avatar/${id ?? 'default'}.jpg`
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
grant_type?: string;
|
||||
scope?: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
userName: string;
|
||||
emailAddress: string;
|
||||
password: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
userName: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
emailAddress: string;
|
||||
isActive: boolean;
|
||||
fullName: string;
|
||||
lastLoginTime?: string;
|
||||
creationTime: string;
|
||||
roles: string[];
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export interface CurrentUser {
|
||||
id: string;
|
||||
tenantId?: string;
|
||||
userName: string;
|
||||
name?: string;
|
||||
surName?: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
phoneNumber?: string;
|
||||
phoneNumberVerified: boolean;
|
||||
isAuthenticated: boolean;
|
||||
roles: string[];
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
class AuthService {
|
||||
private readonly TOKEN_KEY = 'access_token';
|
||||
private readonly REFRESH_TOKEN_KEY = 'refresh_token';
|
||||
private readonly USER_KEY = 'current_user';
|
||||
private readonly TENANT_KEY = 'tenant_id';
|
||||
private readonly AVATAR = 'avatar';
|
||||
private readonly TOKEN_EXPIRY_KEY = 'token_expiry';
|
||||
|
||||
async login(data: LoginRequest): Promise<LoginResponse> {
|
||||
// UI uygulaması ile aynı OAuth2 endpoint'ini kullan
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', data.username);
|
||||
formData.append('password', data.password);
|
||||
formData.append('grant_type', data.grant_type || 'password');
|
||||
formData.append('scope', data.scope || 'offline_access Platform');
|
||||
formData.append('client_id', data.client_id || 'Platform_App');
|
||||
|
||||
const response = await apiClient.post<LoginResponse>('/connect/token', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
|
||||
const { access_token, refresh_token, expires_in } = response.data;
|
||||
|
||||
this.setToken(access_token);
|
||||
this.setRefreshToken(refresh_token);
|
||||
this.setTokenExpiry(expires_in);
|
||||
|
||||
await this.fetchCurrentUser();
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async refreshToken(): Promise<LoginResponse> {
|
||||
const refreshToken = this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('grant_type', 'refresh_token');
|
||||
formData.append('refresh_token', refreshToken);
|
||||
formData.append('client_id', 'Platform_App');
|
||||
formData.append('client_secret', '1q2w3e*');
|
||||
|
||||
const response = await apiClient.post<LoginResponse>('/connect/token', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
|
||||
const { access_token, refresh_token: newRefreshToken, expires_in } = response.data;
|
||||
|
||||
this.setToken(access_token);
|
||||
this.setRefreshToken(newRefreshToken);
|
||||
this.setTokenExpiry(expires_in);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async register(data: RegisterRequest): Promise<void> {
|
||||
await apiClient.post('/api/account/register', data);
|
||||
}
|
||||
|
||||
async fetchCurrentUser(): Promise<User | null> {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
currentUser?: CurrentUser;
|
||||
}>('/api/abp/application-configuration');
|
||||
|
||||
const currentUser = response.data.currentUser;
|
||||
|
||||
if (currentUser && currentUser.isAuthenticated) {
|
||||
const user: User = {
|
||||
id: currentUser.id,
|
||||
userName: currentUser.userName,
|
||||
name: currentUser.name || '',
|
||||
surname: currentUser.surName || '',
|
||||
emailAddress: currentUser.email,
|
||||
isActive: true,
|
||||
fullName: `${currentUser.name || ''} ${currentUser.surName || ''}`.trim(),
|
||||
lastLoginTime: new Date().toISOString(),
|
||||
creationTime: new Date().toISOString(),
|
||||
roles: currentUser.roles || [],
|
||||
avatar: AVATAR_URL(currentUser?.id, currentUser.tenantId ?? ''),
|
||||
};
|
||||
|
||||
this.setUser(user);
|
||||
if (currentUser.tenantId) {
|
||||
this.setTenantId(currentUser.tenantId);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch current user:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
localStorage.removeItem(this.TOKEN_KEY);
|
||||
localStorage.removeItem(this.REFRESH_TOKEN_KEY);
|
||||
localStorage.removeItem(this.USER_KEY);
|
||||
localStorage.removeItem(this.TENANT_KEY);
|
||||
localStorage.removeItem(this.TOKEN_EXPIRY_KEY);
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem(this.TOKEN_KEY);
|
||||
}
|
||||
|
||||
setToken(token: string): void {
|
||||
localStorage.setItem(this.TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
getRefreshToken(): string | null {
|
||||
return localStorage.getItem(this.REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
setRefreshToken(token: string): void {
|
||||
localStorage.setItem(this.REFRESH_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
getUser(): User | null {
|
||||
const userStr = localStorage.getItem(this.USER_KEY);
|
||||
return userStr ? JSON.parse(userStr) : null;
|
||||
}
|
||||
|
||||
setUser(user: User): void {
|
||||
localStorage.setItem(this.USER_KEY, JSON.stringify(user));
|
||||
}
|
||||
|
||||
getTenantId(): string | null {
|
||||
return localStorage.getItem(this.TENANT_KEY);
|
||||
}
|
||||
|
||||
setTenantId(tenantId: string): void {
|
||||
localStorage.setItem(this.TENANT_KEY, tenantId);
|
||||
}
|
||||
|
||||
getAvatar(): string | null {
|
||||
return localStorage.getItem(this.AVATAR);
|
||||
}
|
||||
|
||||
setAvatar(avatar: string): void {
|
||||
localStorage.setItem(this.AVATAR, avatar);
|
||||
}
|
||||
|
||||
setTokenExpiry(expiresIn: number): void {
|
||||
const expiryTime = Date.now() + expiresIn * 1000;
|
||||
localStorage.setItem(this.TOKEN_EXPIRY_KEY, expiryTime.toString());
|
||||
}
|
||||
|
||||
isTokenExpired(): boolean {
|
||||
const expiryTime = localStorage.getItem(this.TOKEN_EXPIRY_KEY);
|
||||
if (!expiryTime) return true;
|
||||
return Date.now() > parseInt(expiryTime);
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return !!this.getToken() && !this.isTokenExpired();
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService();
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
import { apiClient } from './config';
|
||||
import type { PaginatedResponse } from './blog.service';
|
||||
|
||||
export interface ForumCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
topicCount: number;
|
||||
postCount: number;
|
||||
lastPost?: {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
createdAt: string;
|
||||
};
|
||||
order: number;
|
||||
isLocked: boolean;
|
||||
}
|
||||
|
||||
export interface ForumTopic {
|
||||
id: string;
|
||||
categoryId: string;
|
||||
category: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
title: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
author: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
reputation: number;
|
||||
};
|
||||
isPinned: boolean;
|
||||
isLocked: boolean;
|
||||
isSolved: boolean;
|
||||
viewCount: number;
|
||||
replyCount: number;
|
||||
likeCount: number;
|
||||
lastReply?: {
|
||||
id: string;
|
||||
author: string;
|
||||
createdAt: string;
|
||||
};
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ForumPost {
|
||||
id: string;
|
||||
topicId?: string;
|
||||
categoryId?: string;
|
||||
title?: string;
|
||||
content: string;
|
||||
author?: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
reputation?: number;
|
||||
postCount?: number;
|
||||
joinedAt?: string;
|
||||
};
|
||||
parentId?: string;
|
||||
replies?: ForumPost[];
|
||||
likeCount?: number;
|
||||
viewCount?: number;
|
||||
replyCount?: number;
|
||||
isLiked?: boolean;
|
||||
isBestAnswer?: boolean;
|
||||
isEdited?: boolean;
|
||||
editedAt?: string;
|
||||
tags?: string[];
|
||||
lastReply?: {
|
||||
id: string;
|
||||
author?: {
|
||||
name: string;
|
||||
};
|
||||
createdAt: string;
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateTopicRequest {
|
||||
categoryId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface CreatePostRequest {
|
||||
topicId?: string;
|
||||
categoryId?: string;
|
||||
title?: string;
|
||||
content: string;
|
||||
parentId?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface ForumListParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
categoryId?: string;
|
||||
search?: string;
|
||||
tag?: string;
|
||||
authorId?: string;
|
||||
sortBy?: 'latest' | 'popular' | 'unanswered' | 'solved';
|
||||
filter?: 'all' | 'pinned' | 'solved' | 'unsolved' | 'locked';
|
||||
}
|
||||
|
||||
export interface ForumStats {
|
||||
totalTopics: number;
|
||||
totalPosts: number;
|
||||
totalUsers: number;
|
||||
onlineUsers: number;
|
||||
latestMember: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
class ForumService {
|
||||
async getCategories(): Promise<ForumCategory[]> {
|
||||
const response = await apiClient.get<ForumCategory[]>('/api/app/forum/categories');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getCategory(idOrSlug: string): Promise<ForumCategory> {
|
||||
const response = await apiClient.get<ForumCategory>(`/api/app/forum/categories/${idOrSlug}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getTopics(params: ForumListParams = {}): Promise<PaginatedResponse<ForumTopic>> {
|
||||
const response = await apiClient.get<PaginatedResponse<ForumTopic>>('/api/app/forum/topics', { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getTopic(idOrSlug: string): Promise<ForumTopic> {
|
||||
const response = await apiClient.get<ForumTopic>(`/api/app/forum/topics/${idOrSlug}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createTopic(data: CreateTopicRequest): Promise<ForumTopic> {
|
||||
const response = await apiClient.post<ForumTopic>('/api/app/forum/topics', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateTopic(id: string, data: Partial<CreateTopicRequest>): Promise<ForumTopic> {
|
||||
const response = await apiClient.put<ForumTopic>(`/api/app/forum/topics/${id}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteTopic(id: string): Promise<void> {
|
||||
await apiClient.delete(`/api/app/forum/topics/${id}`);
|
||||
}
|
||||
|
||||
async getPosts(topicId: string, page = 1, pageSize = 20): Promise<PaginatedResponse<ForumPost>> {
|
||||
const response = await apiClient.get<PaginatedResponse<ForumPost>>(`/api/app/forum/topics/${topicId}/posts`, {
|
||||
params: { page, pageSize }
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createPost(data: CreatePostRequest): Promise<ForumPost> {
|
||||
const response = await apiClient.post<ForumPost>('/api/app/forum/posts', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updatePost(id: string, content: string): Promise<ForumPost> {
|
||||
const response = await apiClient.put<ForumPost>(`/api/app/forum/posts/${id}`, { content });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deletePost(id: string): Promise<void> {
|
||||
await apiClient.delete(`/api/app/forum/posts/${id}`);
|
||||
}
|
||||
|
||||
async likeTopic(topicId: string): Promise<void> {
|
||||
await apiClient.post(`/api/app/forum/topics/${topicId}/like`);
|
||||
}
|
||||
|
||||
async unlikeTopic(topicId: string): Promise<void> {
|
||||
await apiClient.delete(`/api/app/forum/topics/${topicId}/like`);
|
||||
}
|
||||
|
||||
async likePost(postId: string): Promise<void> {
|
||||
await apiClient.post(`/api/app/forum/posts/${postId}/like`);
|
||||
}
|
||||
|
||||
async unlikePost(postId: string): Promise<void> {
|
||||
await apiClient.delete(`/api/app/forum/posts/${postId}/like`);
|
||||
}
|
||||
|
||||
async markAsBestAnswer(postId: string): Promise<void> {
|
||||
await apiClient.post(`/api/app/forum/posts/${postId}/best-answer`);
|
||||
}
|
||||
|
||||
async pinTopic(topicId: string): Promise<void> {
|
||||
await apiClient.post(`/api/app/forum/topics/${topicId}/pin`);
|
||||
}
|
||||
|
||||
async unpinTopic(topicId: string): Promise<void> {
|
||||
await apiClient.delete(`/api/app/forum/topics/${topicId}/pin`);
|
||||
}
|
||||
|
||||
async lockTopic(topicId: string): Promise<void> {
|
||||
await apiClient.post(`/api/app/forum/topics/${topicId}/lock`);
|
||||
}
|
||||
|
||||
async unlockTopic(topicId: string): Promise<void> {
|
||||
await apiClient.delete(`/api/app/forum/topics/${topicId}/lock`);
|
||||
}
|
||||
|
||||
async markAsSolved(topicId: string): Promise<void> {
|
||||
await apiClient.post(`/api/app/forum/topics/${topicId}/solve`);
|
||||
}
|
||||
|
||||
async getStats(): Promise<ForumStats> {
|
||||
const response = await apiClient.get<ForumStats>('/api/app/forum/stats');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async searchTopics(query: string, page = 1, pageSize = 20): Promise<PaginatedResponse<ForumTopic>> {
|
||||
const response = await apiClient.get<PaginatedResponse<ForumTopic>>('/api/app/forum/search', {
|
||||
params: { query, page, pageSize }
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getTags(): Promise<string[]> {
|
||||
const response = await apiClient.get<string[]>('/api/app/forum/tags');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getCategoryBySlug(slug: string): Promise<ForumCategory> {
|
||||
const response = await apiClient.get<ForumCategory>(`/api/app/forum/categories/by-slug/${slug}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getPostsByCategory(categoryId: string, page = 1, pageSize = 20): Promise<ForumPost[]> {
|
||||
const response = await apiClient.get<ForumPost[]>(`/api/app/forum/categories/${categoryId}/posts`, {
|
||||
params: { page, pageSize }
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const forumService = new ForumService();
|
||||
|
||||
// Re-export PaginatedResponse from blog service
|
||||
export type { PaginatedResponse } from './blog.service';
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { authService, type User, type LoginRequest, type RegisterRequest } from '../services/api/auth.service';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
tenantId?: string | null;
|
||||
avatar: string | null;
|
||||
login: (data: LoginRequest) => Promise<void>;
|
||||
register: (data: RegisterRequest) => Promise<void>;
|
||||
logout: () => void;
|
||||
checkAuth: () => Promise<void>;
|
||||
refreshToken: () => Promise<void>;
|
||||
}
|
||||
|
||||
const { VITE_CDN_URL } = import.meta.env
|
||||
|
||||
export const AVATAR_URL = (id?: string, tenantId?: string) =>
|
||||
`${VITE_CDN_URL}/${tenantId ? 'tenants/' + tenantId : 'host'}/Avatar/${id ?? 'default'}.jpg`
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
tenantId: null,
|
||||
avatar: null,
|
||||
|
||||
login: async (data: LoginRequest) => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
await authService.login(data);
|
||||
const user = authService.getUser();
|
||||
const tenantId = authService.getTenantId();
|
||||
console.log(AVATAR_URL(user?.id, tenantId ?? ''))
|
||||
set({ user, isAuthenticated: true, isLoading: false, tenantId, avatar: AVATAR_URL(user?.id, tenantId ?? ''), });
|
||||
console.log(user)
|
||||
toast.success('Giriş başarılı!', { position:'top-center' });
|
||||
} catch (error: any) {
|
||||
set({ isLoading: false });
|
||||
const errorMessage = error.response?.data?.error_description ||
|
||||
error.response?.data?.error?.message ||
|
||||
error.response?.data?.message ||
|
||||
'Giriş başarısız!';
|
||||
toast.error(errorMessage, { position:'top-center' });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
register: async (data: RegisterRequest) => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
await authService.register(data);
|
||||
set({ isLoading: false });
|
||||
toast.success('Kayıt başarılı! Lütfen giriş yapın.', { position:'top-center' });
|
||||
} catch (error: any) {
|
||||
set({ isLoading: false });
|
||||
toast.error(error.response?.data?.error?.message || 'Kayıt başarısız!', { position:'top-center' });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
authService.logout();
|
||||
set({ user: null, isAuthenticated: false, tenantId: null, avatar: null });
|
||||
toast.success('Çıkış yapıldı', { position:'top-center' });
|
||||
},
|
||||
|
||||
checkAuth: async () => {
|
||||
if (!authService.isAuthenticated()) {
|
||||
set({ user: null, isAuthenticated: false, tenantId: null, avatar: null });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await authService.fetchCurrentUser();
|
||||
const tenantId = authService.getTenantId();
|
||||
set({ user, isAuthenticated: !!user, tenantId, avatar: AVATAR_URL(user?.id, tenantId ?? ''), });
|
||||
} catch (error) {
|
||||
authService.logout();
|
||||
set({ user: null, isAuthenticated: false, tenantId: null, avatar: null });
|
||||
}
|
||||
},
|
||||
|
||||
refreshToken: async () => {
|
||||
try {
|
||||
await authService.refreshToken();
|
||||
const user = await authService.fetchCurrentUser();
|
||||
const tenantId = authService.getTenantId();
|
||||
set({ user, isAuthenticated: !!user, tenantId, avatar: AVATAR_URL(user?.id, tenantId ?? ''), });
|
||||
} catch (error) {
|
||||
authService.logout();
|
||||
set({ user: null, isAuthenticated: false, tenantId: null, avatar: null });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
partialize: (state) => ({
|
||||
user: state.user,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
tenantId: state.tenantId,
|
||||
avatar: AVATAR_URL(state.user?.id, state.tenantId ?? ''),
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
@ -66,7 +66,7 @@ const adminRoutes: Routes = [
|
|||
{
|
||||
key: ROUTES_ENUM.admin.forum.management,
|
||||
path: ROUTES_ENUM.admin.forum.management,
|
||||
component: lazy(() => import('@/views/forum/ForumManagement')),
|
||||
component: lazy(() => import('@/views/forum/AdminView')),
|
||||
authority: [],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,291 +0,0 @@
|
|||
import api from '@/services/api.service';
|
||||
|
||||
export interface ForumCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
displayOrder: number;
|
||||
isActive: boolean;
|
||||
isLocked: boolean;
|
||||
topicCount: number;
|
||||
postCount: number;
|
||||
lastPost?: {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
createdAt: string;
|
||||
};
|
||||
creationTime: string;
|
||||
}
|
||||
|
||||
export interface ForumTopic {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
categoryId: string;
|
||||
category: ForumCategory;
|
||||
authorId: string;
|
||||
author: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
};
|
||||
viewCount: number;
|
||||
replyCount: number;
|
||||
likeCount: number;
|
||||
isPinned: boolean;
|
||||
isLocked: boolean;
|
||||
isSolved: boolean;
|
||||
lastActivityAt: string;
|
||||
tags: string[];
|
||||
creationTime: string;
|
||||
}
|
||||
|
||||
export interface ForumPost {
|
||||
id: string;
|
||||
topicId: string;
|
||||
content: string;
|
||||
authorId: string;
|
||||
author: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
};
|
||||
likeCount: number;
|
||||
isAcceptedAnswer: boolean;
|
||||
creationTime: string;
|
||||
}
|
||||
|
||||
export interface CreateUpdateForumCategoryDto {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
displayOrder: number;
|
||||
isActive: boolean;
|
||||
isLocked: boolean;
|
||||
}
|
||||
|
||||
export interface CreateUpdateForumTopicDto {
|
||||
title: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
categoryId: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface CreateForumPostDto {
|
||||
topicId: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface GetForumTopicsInput {
|
||||
categoryId?: string;
|
||||
search?: string;
|
||||
tag?: string;
|
||||
authorId?: string;
|
||||
isPinned?: boolean;
|
||||
isLocked?: boolean;
|
||||
isSolved?: boolean;
|
||||
sortBy?: 'latest' | 'popular' | 'trending';
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface ForumStats {
|
||||
totalCategories: number;
|
||||
totalTopics: number;
|
||||
totalPosts: number;
|
||||
totalUsers: number;
|
||||
onlineUsers: number;
|
||||
latestMember?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
class ForumService {
|
||||
// Category methods
|
||||
async getCategories(): Promise<ForumCategory[]> {
|
||||
const response = await api.fetchData({ url: '/api/app/forum/categories', method: 'GET' });
|
||||
return response.data as ForumCategory[];
|
||||
}
|
||||
|
||||
async getCategory(id: string): Promise<ForumCategory> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/categories/${id}`, method: 'GET' });
|
||||
return response.data as ForumCategory;
|
||||
}
|
||||
|
||||
async createCategory(data: CreateUpdateForumCategoryDto): Promise<ForumCategory> {
|
||||
const response = await api.fetchData({ url: '/api/app/forum/categories', method: 'POST', data });
|
||||
return response.data as ForumCategory;
|
||||
}
|
||||
|
||||
async updateCategory(id: string, data: CreateUpdateForumCategoryDto): Promise<ForumCategory> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/categories/${id}`, method: 'PUT', data });
|
||||
return response.data as ForumCategory;
|
||||
}
|
||||
|
||||
async deleteCategory(id: string): Promise<void> {
|
||||
await api.fetchData({ url: `/api/app/forum/categories/${id}`, method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Topic methods
|
||||
async getTopics(input?: GetForumTopicsInput): Promise<{ items: ForumTopic[]; totalCount: number }> {
|
||||
const params = {
|
||||
categoryId: input?.categoryId,
|
||||
filter: input?.search,
|
||||
tag: input?.tag,
|
||||
authorId: input?.authorId,
|
||||
isPinned: input?.isPinned,
|
||||
isLocked: input?.isLocked,
|
||||
isSolved: input?.isSolved,
|
||||
sortBy: input?.sortBy || 'latest',
|
||||
skipCount: ((input?.page || 1) - 1) * (input?.pageSize || 20),
|
||||
maxResultCount: input?.pageSize || 20
|
||||
};
|
||||
|
||||
const response = await api.fetchData({ url: '/api/app/forum/topics', method: 'GET', params });
|
||||
return response.data as { items: ForumTopic[]; totalCount: number };
|
||||
}
|
||||
|
||||
async getTopic(id: string): Promise<ForumTopic> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/topics/${id}`, method: 'GET' });
|
||||
return response.data as ForumTopic;
|
||||
}
|
||||
|
||||
async getTopicBySlug(slug: string): Promise<ForumTopic> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/topics/by-slug/${slug}`, method: 'GET' });
|
||||
return response.data as ForumTopic;
|
||||
}
|
||||
|
||||
async createTopic(data: CreateUpdateForumTopicDto): Promise<ForumTopic> {
|
||||
const response = await api.fetchData({ url: '/api/app/forum/topics', method: 'POST', data });
|
||||
return response.data as ForumTopic;
|
||||
}
|
||||
|
||||
async updateTopic(id: string, data: CreateUpdateForumTopicDto): Promise<ForumTopic> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/topics/${id}`, method: 'PUT', data });
|
||||
return response.data as ForumTopic;
|
||||
}
|
||||
|
||||
async deleteTopic(id: string): Promise<void> {
|
||||
await api.fetchData({ url: `/api/app/forum/topics/${id}`, method: 'DELETE' });
|
||||
}
|
||||
|
||||
async pinTopic(id: string): Promise<ForumTopic> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/topics/${id}/pin`, method: 'POST' });
|
||||
return response.data as ForumTopic;
|
||||
}
|
||||
|
||||
async unpinTopic(id: string): Promise<ForumTopic> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/topics/${id}/unpin`, method: 'POST' });
|
||||
return response.data as ForumTopic;
|
||||
}
|
||||
|
||||
async lockTopic(id: string): Promise<ForumTopic> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/topics/${id}/lock`, method: 'POST' });
|
||||
return response.data as ForumTopic;
|
||||
}
|
||||
|
||||
async unlockTopic(id: string): Promise<ForumTopic> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/topics/${id}/unlock`, method: 'POST' });
|
||||
return response.data as ForumTopic;
|
||||
}
|
||||
|
||||
async markTopicAsSolved(id: string): Promise<ForumTopic> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/topics/${id}/mark-as-solved`, method: 'POST' });
|
||||
return response.data as ForumTopic;
|
||||
}
|
||||
|
||||
async likeTopic(id: string): Promise<void> {
|
||||
await api.fetchData({ url: `/api/app/forum/topics/${id}/like`, method: 'POST' });
|
||||
}
|
||||
|
||||
async unlikeTopic(id: string): Promise<void> {
|
||||
await api.fetchData({ url: `/api/app/forum/topics/${id}/unlike`, method: 'POST' });
|
||||
}
|
||||
|
||||
async incrementViewCount(id: string): Promise<void> {
|
||||
await api.fetchData({ url: `/api/app/forum/topics/${id}/increment-view-count`, method: 'POST' });
|
||||
}
|
||||
|
||||
// Post methods
|
||||
async getPosts(topicId: string, page: number = 1, pageSize: number = 20): Promise<{ items: ForumPost[]; totalCount: number }> {
|
||||
const params = {
|
||||
skipCount: (page - 1) * pageSize,
|
||||
maxResultCount: pageSize
|
||||
};
|
||||
|
||||
const response = await api.fetchData({ url: `/api/app/forum/topics/${topicId}/posts`, method: 'GET', params });
|
||||
return response.data as { items: ForumPost[]; totalCount: number };
|
||||
}
|
||||
|
||||
async createPost(data: CreateForumPostDto): Promise<ForumPost> {
|
||||
const response = await api.fetchData({ url: '/api/app/forum/posts', method: 'POST', data });
|
||||
return response.data as ForumPost;
|
||||
}
|
||||
|
||||
async updatePost(id: string, content: string): Promise<ForumPost> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/posts/${id}`, method: 'PUT', data: { content } });
|
||||
return response.data as ForumPost;
|
||||
}
|
||||
|
||||
async deletePost(id: string): Promise<void> {
|
||||
await api.fetchData({ url: `/api/app/forum/posts/${id}`, method: 'DELETE' });
|
||||
}
|
||||
|
||||
async likePost(id: string): Promise<void> {
|
||||
await api.fetchData({ url: `/api/app/forum/posts/${id}/like`, method: 'POST' });
|
||||
}
|
||||
|
||||
async unlikePost(id: string): Promise<void> {
|
||||
await api.fetchData({ url: `/api/app/forum/posts/${id}/unlike`, method: 'POST' });
|
||||
}
|
||||
|
||||
async acceptAnswer(id: string): Promise<ForumPost> {
|
||||
const response = await api.fetchData({ url: `/api/app/forum/posts/${id}/accept-answer`, method: 'POST' });
|
||||
return response.data as ForumPost;
|
||||
}
|
||||
|
||||
// Search and filters
|
||||
async searchTopics(query: string, page: number = 1, pageSize: number = 20): Promise<{ items: ForumTopic[]; totalCount: number }> {
|
||||
const params = {
|
||||
query,
|
||||
skipCount: (page - 1) * pageSize,
|
||||
maxResultCount: pageSize
|
||||
};
|
||||
|
||||
const response = await api.fetchData({ url: '/api/app/forum/search', method: 'GET', params });
|
||||
return response.data as { items: ForumTopic[]; totalCount: number };
|
||||
}
|
||||
|
||||
async getTopicsByCategory(categoryId: string, page: number = 1, pageSize: number = 20): Promise<{ items: ForumTopic[]; totalCount: number }> {
|
||||
return this.getTopics({ categoryId, page, pageSize });
|
||||
}
|
||||
|
||||
async getTopicsByTag(tag: string, page: number = 1, pageSize: number = 20): Promise<{ items: ForumTopic[]; totalCount: number }> {
|
||||
return this.getTopics({ tag, page, pageSize });
|
||||
}
|
||||
|
||||
async getTopicsByAuthor(authorId: string, page: number = 1, pageSize: number = 20): Promise<{ items: ForumTopic[]; totalCount: number }> {
|
||||
return this.getTopics({ authorId, page, pageSize });
|
||||
}
|
||||
|
||||
// Tags
|
||||
async getPopularTags(count: number = 20): Promise<string[]> {
|
||||
const response = await api.fetchData({ url: '/api/app/forum/popular-tags', method: 'GET', params: { count } });
|
||||
return response.data as string[];
|
||||
}
|
||||
|
||||
// Stats
|
||||
async getStats(): Promise<ForumStats> {
|
||||
const response = await api.fetchData({ url: '/api/app/forum/stats', method: 'GET' });
|
||||
return response.data as ForumStats;
|
||||
}
|
||||
}
|
||||
|
||||
export const forumService = new ForumService();
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// import { HttpClient, HttpParameterCodec, HttpParams, HttpRequest } from '@angular/common/http'
|
||||
// import { Inject, Injectable } from '@angular/core'
|
||||
// import { Observable, throwError } from 'rxjs'
|
||||
// import { catchError } from 'rxjs/operators'
|
||||
// import { ExternalHttpClient } from '../clients/http.client'
|
||||
// import { ABP } from '../models/common'
|
||||
// import { Rest } from '../models/rest'
|
||||
// import { CORE_OPTIONS } from '../tokens/options.token'
|
||||
// import { isUndefinedOrEmptyString } from '../utils/common-utils'
|
||||
// import { EnvironmentService } from './environment.service'
|
||||
// import { HttpErrorReporterService } from './http-error-reporter.service'
|
||||
|
||||
// export class RestService {
|
||||
// constructor(
|
||||
// protected options: ABP.Root,
|
||||
// protected http: HttpClient,
|
||||
// protected externalHttp: ExternalHttpClient,
|
||||
// protected environment: EnvironmentService,
|
||||
// protected httpErrorReporter: HttpErrorReporterService,
|
||||
// ) {}
|
||||
|
||||
// protected getApiFromStore(apiName: string | undefined): string {
|
||||
// return this.environment.getApiUrl(apiName)
|
||||
// }
|
||||
|
||||
// handleError(err: any): Observable<any> {
|
||||
// this.httpErrorReporter.reportError(err)
|
||||
// return throwError(err)
|
||||
// }
|
||||
|
||||
// request<T, R>(request: HttpRequest<T> | Rest.Request<T>, config?: Rest.Config, api?: string): Observable<R> {
|
||||
// config = config || ({} as Rest.Config)
|
||||
// api = api || this.getApiFromStore(config.apiName)
|
||||
// const { method, params, ...options } = request
|
||||
// const { observe = Rest.Observe.Body, skipHandleError } = config
|
||||
// const url = this.removeDuplicateSlashes(api + request.url)
|
||||
|
||||
// const httpClient: HttpClient = this.getHttpClient(config.skipAddingHeader)
|
||||
// return httpClient
|
||||
// .request<R>(method, url, {
|
||||
// observe,
|
||||
// ...(params && {
|
||||
// params: this.getParams(params, config.httpParamEncoder),
|
||||
// }),
|
||||
// ...options,
|
||||
// } as any)
|
||||
// .pipe(catchError((err) => (skipHandleError ? throwError(err) : this.handleError(err))))
|
||||
// }
|
||||
// private getHttpClient(isExternal: boolean) {
|
||||
// return isExternal ? this.externalHttp : this.http
|
||||
// }
|
||||
|
||||
// private getParams(params: Rest.Params, encoder?: HttpParameterCodec): HttpParams {
|
||||
// const filteredParams = Object.entries(params).reduce((acc, [key, value]) => {
|
||||
// if (isUndefinedOrEmptyString(value)) return acc
|
||||
// if (value === null && !this.options.sendNullsAsQueryParam) return acc
|
||||
// acc[key] = value
|
||||
// return acc
|
||||
// }, {} as any)
|
||||
// return encoder
|
||||
// ? new HttpParams({ encoder, fromObject: filteredParams })
|
||||
// : new HttpParams({ fromObject: filteredParams })
|
||||
// }
|
||||
|
||||
// private removeDuplicateSlashes(url: string): string {
|
||||
// return url.replace(/([^:]\/)\/+/g, '$1')
|
||||
// }
|
||||
// }
|
||||
|
|
@ -84,8 +84,8 @@ const BlogManagement = () => {
|
|||
blogService.getPosts({ pageSize: 100 }),
|
||||
blogService.getCategories(),
|
||||
])
|
||||
setPosts(postsData.items)
|
||||
setCategories(categoriesData)
|
||||
setCategories(categoriesData.filter(a=>a.name.startsWith("blog")))
|
||||
setPosts(postsData.items.filter(a=> a.title.startsWith("blog")))
|
||||
} catch (error) {
|
||||
toast.push(
|
||||
<Notification title="Hata" type="danger">
|
||||
|
|
|
|||
|
|
@ -1,541 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import Card from '@/components/ui/Card'
|
||||
import Button from '@/components/ui/Button'
|
||||
import Table from '@/components/ui/Table'
|
||||
import Tag from '@/components/ui/Tag'
|
||||
import Dialog from '@/components/ui/Dialog'
|
||||
import { FormContainer, FormItem } from '@/components/ui/Form'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Switcher from '@/components/ui/Switcher'
|
||||
import { HiPlus, HiPencil, HiTrash, HiEye, HiLockClosed, HiLockOpen } from 'react-icons/hi'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { format } from 'date-fns'
|
||||
import { tr } from 'date-fns/locale'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import toast from '@/components/ui/toast'
|
||||
import Notification from '@/components/ui/Notification'
|
||||
import {
|
||||
CreateUpdateForumCategoryDto,
|
||||
ForumCategory,
|
||||
forumService,
|
||||
ForumTopic,
|
||||
} from '@/services/forum.service'
|
||||
import THead from '@/components/ui/Table/THead'
|
||||
import Tr from '@/components/ui/Table/Tr'
|
||||
import Th from '@/components/ui/Table/Th'
|
||||
import TBody from '@/components/ui/Table/TBody'
|
||||
import Td from '@/components/ui/Table/Td'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
const categoryValidationSchema = Yup.object().shape({
|
||||
name: Yup.string().required('İsim gereklidir'),
|
||||
slug: Yup.string().required('Slug gereklidir'),
|
||||
description: Yup.string().required('Açıklama gereklidir'),
|
||||
})
|
||||
|
||||
const ForumManagement = () => {
|
||||
const { translate } = useLocalization()
|
||||
const navigate = useNavigate()
|
||||
const [activeTab, setActiveTab] = useState<'topics' | 'categories'>('topics')
|
||||
const [categories, setCategories] = useState<ForumCategory[]>([])
|
||||
const [topics, setTopics] = useState<ForumTopic[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [categoryModalVisible, setCategoryModalVisible] = useState(false)
|
||||
const [editingCategory, setEditingCategory] = useState<ForumCategory | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [activeTab])
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
if (activeTab === 'categories') {
|
||||
const data = await forumService.getCategories()
|
||||
setCategories(data)
|
||||
} else {
|
||||
const data = await forumService.getTopics({ pageSize: 100 })
|
||||
setTopics(data.items)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.push(
|
||||
<Notification title="Hata" type="danger">
|
||||
Veriler yüklenirken hata oluştu
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateCategory = () => {
|
||||
setEditingCategory(null)
|
||||
setCategoryModalVisible(true)
|
||||
}
|
||||
|
||||
const handleEditCategory = (category: ForumCategory) => {
|
||||
setEditingCategory(category)
|
||||
setCategoryModalVisible(true)
|
||||
}
|
||||
|
||||
const handleDeleteCategory = async (id: string) => {
|
||||
try {
|
||||
await forumService.deleteCategory(id)
|
||||
toast.push(
|
||||
<Notification title="Başarılı" type="success">
|
||||
Kategori silindi
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
loadData()
|
||||
} catch (error) {
|
||||
toast.push(
|
||||
<Notification title="Hata" type="danger">
|
||||
Silme işlemi başarısız
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitCategory = async (values: any, { setSubmitting }: any) => {
|
||||
try {
|
||||
const data: CreateUpdateForumCategoryDto = {
|
||||
name: values.name,
|
||||
slug: values.slug,
|
||||
description: values.description,
|
||||
icon: values.icon,
|
||||
displayOrder: values.displayOrder,
|
||||
isActive: values.isActive,
|
||||
isLocked: values.isLocked,
|
||||
}
|
||||
|
||||
if (editingCategory) {
|
||||
await forumService.updateCategory(editingCategory.id, data)
|
||||
toast.push(
|
||||
<Notification title="Başarılı" type="success">
|
||||
Kategori güncellendi
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
} else {
|
||||
await forumService.createCategory(data)
|
||||
toast.push(
|
||||
<Notification title="Başarılı" type="success">
|
||||
Kategori oluşturuldu
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
setCategoryModalVisible(false)
|
||||
loadData()
|
||||
} catch (error) {
|
||||
toast.push(
|
||||
<Notification title="Hata" type="danger">
|
||||
İşlem başarısız
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleLock = async (topic: ForumTopic) => {
|
||||
try {
|
||||
if (topic.isLocked) {
|
||||
await forumService.unlockTopic(topic.id)
|
||||
toast.push(
|
||||
<Notification title="Başarılı" type="success">
|
||||
Konu kilidi açıldı
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
} else {
|
||||
await forumService.lockTopic(topic.id)
|
||||
toast.push(
|
||||
<Notification title="Başarılı" type="success">
|
||||
Konu kilitlendi
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
}
|
||||
loadData()
|
||||
} catch (error) {
|
||||
toast.push(
|
||||
<Notification title="Hata" type="danger">
|
||||
İşlem başarısız
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTogglePin = async (topic: ForumTopic) => {
|
||||
try {
|
||||
if (topic.isPinned) {
|
||||
await forumService.unpinTopic(topic.id)
|
||||
toast.push(
|
||||
<Notification title="Başarılı" type="success">
|
||||
Sabitleme kaldırıldı
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
} else {
|
||||
await forumService.pinTopic(topic.id)
|
||||
toast.push(
|
||||
<Notification title="Başarılı" type="success">
|
||||
Konu sabitlendi
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
}
|
||||
loadData()
|
||||
} catch (error) {
|
||||
toast.push(
|
||||
<Notification title="Hata" type="danger">
|
||||
İşlem başarısız
|
||||
</Notification>,
|
||||
{
|
||||
placement: 'top-center',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const initialCategoryValues = editingCategory
|
||||
? {
|
||||
name: editingCategory.name,
|
||||
slug: editingCategory.slug,
|
||||
description: editingCategory.description,
|
||||
icon: editingCategory.icon || '',
|
||||
displayOrder: editingCategory.displayOrder,
|
||||
isActive: editingCategory.isActive,
|
||||
isLocked: editingCategory.isLocked,
|
||||
}
|
||||
: {
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
displayOrder: 0,
|
||||
isActive: true,
|
||||
isLocked: false,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet
|
||||
titleTemplate="%s | Kurs Platform"
|
||||
title={translate('::' + 'Forum Management')}
|
||||
defaultTitle="Kurs Platform"
|
||||
></Helmet>
|
||||
<Card>
|
||||
<div className="mb-4">
|
||||
<div className="flex gap-4 border-b">
|
||||
<button
|
||||
className={`pb-2 px-1 ${activeTab === 'topics' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
|
||||
onClick={() => setActiveTab('topics')}
|
||||
>
|
||||
<b>Konular</b>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`pb-2 px-1 ${activeTab === 'categories' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
|
||||
onClick={() => setActiveTab('categories')}
|
||||
>
|
||||
<b>Kategoriler</b>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === 'categories' ? (
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>İsim</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th>Açıklama</Th>
|
||||
<Th>Konu Sayısı</Th>
|
||||
<Th>Mesaj Sayısı</Th>
|
||||
<Th>Durum</Th>
|
||||
<Th>Kilit</Th>
|
||||
<Th>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="xs"
|
||||
icon={<HiPlus />}
|
||||
onClick={handleCreateCategory}
|
||||
>
|
||||
Yeni
|
||||
</Button>
|
||||
</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{loading ? (
|
||||
<Tr>
|
||||
<Td colSpan={8} className="text-center">
|
||||
Yükleniyor...
|
||||
</Td>
|
||||
</Tr>
|
||||
) : (
|
||||
categories.map((category) => (
|
||||
<Tr key={category.id}>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
{category.icon && <span className="mr-2">{category.icon}</span>}
|
||||
<span className="font-medium">{category.name}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{category.slug}</Td>
|
||||
<Td>{category.description}</Td>
|
||||
<Td>{category.topicCount}</Td>
|
||||
<Td>{category.postCount}</Td>
|
||||
<Td>
|
||||
<Tag
|
||||
className={
|
||||
category.isActive
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}
|
||||
>
|
||||
{category.isActive ? 'Aktif' : 'Pasif'}
|
||||
</Tag>
|
||||
</Td>
|
||||
<Td>
|
||||
{category.isLocked ? (
|
||||
<HiLockClosed className="text-red-500" />
|
||||
) : (
|
||||
<HiLockOpen className="text-green-500" />
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
icon={<HiPencil />}
|
||||
onClick={() => handleEditCategory(category)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="solid"
|
||||
color="red-600"
|
||||
icon={<HiTrash />}
|
||||
onClick={() => handleDeleteCategory(category.id)}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
))
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
) : (
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Başlık</Th>
|
||||
<Th>Kategori</Th>
|
||||
<Th>Yazar</Th>
|
||||
<Th>Görüntülenme</Th>
|
||||
<Th>Cevap</Th>
|
||||
<Th>Son Aktivite</Th>
|
||||
<Th>İşlemler</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{loading ? (
|
||||
<Tr>
|
||||
<Td colSpan={7} className="text-center">
|
||||
Yükleniyor...
|
||||
</Td>
|
||||
</Tr>
|
||||
) : (
|
||||
topics.map((topic) => (
|
||||
<Tr key={topic.id}>
|
||||
<Td>
|
||||
<div>
|
||||
<a
|
||||
className="text-blue-600 hover:underline cursor-pointer font-medium"
|
||||
onClick={() => navigate(`/forum/topic/${topic.slug || topic.id}`)}
|
||||
>
|
||||
{topic.title}
|
||||
</a>
|
||||
<div className="flex gap-2 mt-1">
|
||||
{topic.isPinned && (
|
||||
<Tag className="bg-yellow-100 text-yellow-800 text-xs">Sabit</Tag>
|
||||
)}
|
||||
{topic.isLocked && (
|
||||
<Tag className="bg-red-100 text-red-800 text-xs">Kilitli</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{topic.category?.name}</Td>
|
||||
<Td>{topic.author?.name}</Td>
|
||||
<Td>{topic.viewCount}</Td>
|
||||
<Td>{topic.replyCount}</Td>
|
||||
<Td>
|
||||
{topic.lastActivityAt
|
||||
? format(new Date(topic.lastActivityAt), 'dd MMM yyyy HH:mm', {
|
||||
locale: tr,
|
||||
})
|
||||
: '-'}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
icon={<HiEye />}
|
||||
onClick={() => navigate(`/forum/topic/${topic.slug || topic.id}`)}
|
||||
/>
|
||||
<Switcher
|
||||
checked={topic.isPinned}
|
||||
onChange={() => handleTogglePin(topic)}
|
||||
checkedContent="📌"
|
||||
unCheckedContent="📌"
|
||||
/>
|
||||
<Switcher
|
||||
checked={topic.isLocked}
|
||||
onChange={() => handleToggleLock(topic)}
|
||||
checkedContent="🔒"
|
||||
unCheckedContent="🔓"
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
))
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Dialog
|
||||
isOpen={categoryModalVisible}
|
||||
onClose={() => setCategoryModalVisible(false)}
|
||||
onRequestClose={() => setCategoryModalVisible(false)}
|
||||
width={600}
|
||||
>
|
||||
<h5 className="mb-4">{editingCategory ? 'Kategoriyi Düzenle' : 'Yeni Kategori'}</h5>
|
||||
|
||||
<Formik
|
||||
initialValues={initialCategoryValues}
|
||||
validationSchema={categoryValidationSchema}
|
||||
onSubmit={handleSubmitCategory}
|
||||
enableReinitialize
|
||||
>
|
||||
{({ values, touched, errors, isSubmitting }) => (
|
||||
<Form>
|
||||
<FormContainer>
|
||||
<FormItem
|
||||
label="İsim"
|
||||
invalid={errors.name && touched.name}
|
||||
errorMessage={errors.name}
|
||||
>
|
||||
<Field type="text" name="name" placeholder="Kategori ismi" component={Input} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label="Slug"
|
||||
invalid={errors.slug && touched.slug}
|
||||
errorMessage={errors.slug}
|
||||
>
|
||||
<Field type="text" name="slug" placeholder="kategori-slug" component={Input} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label="Açıklama"
|
||||
invalid={errors.description && touched.description}
|
||||
errorMessage={errors.description}
|
||||
>
|
||||
<Field
|
||||
name="description"
|
||||
placeholder="Kategori açıklaması"
|
||||
component={Input}
|
||||
textArea={true}
|
||||
rows={3}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="İkon (Emoji)">
|
||||
<Field type="text" name="icon" placeholder="📚" component={Input} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="Sıralama">
|
||||
<Field type="number" name="displayOrder" placeholder="0" component={Input} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Field name="isActive">
|
||||
{({ field, form }: any) => (
|
||||
<Switcher
|
||||
{...field}
|
||||
onChange={(checked) => form.setFieldValue(field.name, checked)}
|
||||
checkedContent="Aktif"
|
||||
unCheckedContent="Pasif"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Field name="isLocked">
|
||||
{({ field, form }: any) => (
|
||||
<Switcher
|
||||
{...field}
|
||||
onChange={(checked) => form.setFieldValue(field.name, checked)}
|
||||
checkedContent="Kilitli"
|
||||
unCheckedContent="Açık"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="solid" type="submit" loading={isSubmitting}>
|
||||
{editingCategory ? 'Güncelle' : 'Oluştur'}
|
||||
</Button>
|
||||
<Button variant="plain" onClick={() => setCategoryModalVisible(false)}>
|
||||
İptal
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForumManagement
|
||||
Loading…
Reference in a new issue