Forum sistemi kaldırıldı

This commit is contained in:
Sedat Öztürk 2025-06-22 14:54:30 +03:00
parent ddf2eac23e
commit a663cc0079
36 changed files with 177 additions and 5712 deletions

View file

@ -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; }
}
}

View file

@ -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; }
}
}

View file

@ -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>();
}
}
}

View file

@ -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; }
}
}

View file

@ -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>();
}
}

View file

@ -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();
}
}
}

View file

@ -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));
}
}

View file

@ -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",

View file

@ -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
);
}
}
}
}

View file

@ -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--;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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
@ -436,94 +427,5 @@ public class PlatformDbContext :
.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);
});
}
}

View file

@ -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");
}
}
}

View file

@ -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");

View file

@ -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");
}
}
}

View file

@ -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");

View file

@ -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>

View file

@ -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 (
{navLinks.map((link) => (
<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"
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.icon && <link.icon size={16} />}
<span>{link.name}</span>
{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 (
{navLinks.map((link) => (
<Link
key={link.path}
to={link.path}
className="flex items-center space-x-2 font-medium text-white hover:text-blue-400 transition-colors"
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.icon && <link.icon size={16} />}
<span>{link.name}</span>
{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>

View file

@ -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
</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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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();

View file

@ -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';

View file

@ -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 ?? ''),
}),
}
)
);

View file

@ -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: [],
},
]

View file

@ -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();

View file

@ -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')
// }
// }

View file

@ -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">

View file

@ -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 ı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>ı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