Forum ve Blog Componentleri

This commit is contained in:
Sedat ÖZTÜRK 2025-06-19 17:51:10 +03:00
parent eb23174f7a
commit 2baff7538f
59 changed files with 15840 additions and 322 deletions

View file

@ -0,0 +1,26 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Kurs.Platform.Blog
{
public class BlogCategoryDto : 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 int PostCount { get; set; }
}
public class CreateUpdateBlogCategoryDto
{
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; }
}
}

View file

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Application.Dtos;
namespace Kurs.Platform.Blog
{
public class BlogPostDto : FullAuditedEntityDto<Guid>
{
public string Title { get; set; }
public string Slug { get; set; }
public string Content { get; set; }
public string Summary { get; set; }
public string CoverImage { get; set; }
public Guid CategoryId { get; set; }
public BlogCategoryDto Category { get; set; }
public AuthorDto Author { get; set; }
public int ViewCount { get; set; }
public int LikeCount { get; set; }
public int CommentCount { get; set; }
public bool IsPublished { get; set; }
public DateTime? PublishedAt { get; set; }
public List<string> Tags { get; set; }
public bool IsLiked { get; set; }
public BlogPostDto()
{
Tags = new List<string>();
}
}
public class AuthorDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Avatar { get; set; }
}
public class CreateUpdateBlogPostDto
{
public string Title { get; set; }
public string Slug { get; set; }
public string Content { get; set; }
public string Summary { get; set; }
public string CoverImage { get; set; }
public Guid CategoryId { get; set; }
public List<string> Tags { get; set; }
public bool IsPublished { get; set; }
public CreateUpdateBlogPostDto()
{
Tags = new List<string>();
}
}
public class BlogPostListDto : EntityDto<Guid>
{
public string Title { get; set; }
public string Slug { get; set; }
public string Summary { get; set; }
public string CoverImage { get; set; }
public BlogCategoryDto Category { get; set; }
public AuthorDto Author { get; set; }
public int ViewCount { get; set; }
public int LikeCount { get; set; }
public int CommentCount { get; set; }
public bool IsPublished { get; set; }
public DateTime? PublishedAt { get; set; }
public DateTime CreatedAt { get; set; }
public List<string> Tags { get; set; }
public BlogPostListDto()
{
Tags = new List<string>();
}
}
}

View file

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Kurs.Platform.Blog
{
public interface IBlogAppService : IApplicationService
{
// Blog Post methods
Task<PagedResultDto<BlogPostListDto>> GetPostsAsync(GetBlogPostsInput input);
Task<BlogPostDto> GetPostAsync(Guid id);
Task<BlogPostDto> GetPostBySlugAsync(string slug);
Task<BlogPostDto> CreatePostAsync(CreateUpdateBlogPostDto input);
Task<BlogPostDto> UpdatePostAsync(Guid id, CreateUpdateBlogPostDto input);
Task DeletePostAsync(Guid id);
Task<BlogPostDto> PublishPostAsync(Guid id);
Task<BlogPostDto> UnpublishPostAsync(Guid id);
Task LikePostAsync(Guid id);
Task UnlikePostAsync(Guid id);
Task IncrementViewCountAsync(Guid id);
// Blog Category methods
Task<List<BlogCategoryDto>> GetCategoriesAsync();
Task<BlogCategoryDto> GetCategoryAsync(Guid id);
Task<BlogCategoryDto> CreateCategoryAsync(CreateUpdateBlogCategoryDto input);
Task<BlogCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateBlogCategoryDto input);
Task DeleteCategoryAsync(Guid id);
// Search and filters
Task<PagedResultDto<BlogPostListDto>> SearchPostsAsync(SearchBlogPostsInput input);
Task<PagedResultDto<BlogPostListDto>> GetPostsByCategoryAsync(Guid categoryId, PagedAndSortedResultRequestDto input);
Task<PagedResultDto<BlogPostListDto>> GetPostsByTagAsync(string tag, PagedAndSortedResultRequestDto input);
Task<PagedResultDto<BlogPostListDto>> GetPostsByAuthorAsync(Guid authorId, PagedAndSortedResultRequestDto input);
// Tags
Task<List<string>> GetPopularTagsAsync(int count = 20);
// Stats
Task<BlogStatsDto> GetStatsAsync();
}
public class GetBlogPostsInput : PagedAndSortedResultRequestDto
{
public string Filter { get; set; }
public Guid? CategoryId { get; set; }
public string Tag { get; set; }
public Guid? AuthorId { get; set; }
public bool? IsPublished { get; set; }
public string SortBy { get; set; } = "latest"; // latest, popular, trending
}
public class SearchBlogPostsInput : PagedAndSortedResultRequestDto
{
public string Query { get; set; }
public Guid? CategoryId { get; set; }
public string Tag { get; set; }
public Guid? AuthorId { get; set; }
public bool? IsPublished { get; set; }
}
public class BlogStatsDto
{
public int TotalPosts { get; set; }
public int PublishedPosts { get; set; }
public int DraftPosts { get; set; }
public int TotalCategories { get; set; }
public int TotalViews { get; set; }
public int TotalLikes { get; set; }
public int TotalComments { get; set; }
public List<string> PopularTags { get; set; }
public BlogPostListDto LatestPost { get; set; }
public BlogPostListDto MostViewedPost { get; set; }
public BlogStatsDto()
{
PopularTags = new List<string>();
}
}
}

View file

@ -0,0 +1,34 @@
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

@ -0,0 +1,44 @@
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

@ -0,0 +1,114 @@
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

@ -0,0 +1,66 @@
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,600 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Users;
namespace Kurs.Platform.Blog
{
[Authorize]
public class BlogAppService : ApplicationService, IBlogAppService
{
private readonly IRepository<BlogPost, Guid> _postRepository;
private readonly IRepository<BlogCategory, Guid> _categoryRepository;
private readonly IRepository<BlogPostTag, Guid> _tagRepository;
private readonly IRepository<BlogPostLike, Guid> _postLikeRepository;
private readonly ICurrentUser _currentUser;
public BlogAppService(
IRepository<BlogPost, Guid> postRepository,
IRepository<BlogCategory, Guid> categoryRepository,
IRepository<BlogPostTag, Guid> tagRepository,
IRepository<BlogPostLike, Guid> postLikeRepository,
ICurrentUser currentUser)
{
_postRepository = postRepository;
_categoryRepository = categoryRepository;
_tagRepository = tagRepository;
_postLikeRepository = postLikeRepository;
_currentUser = currentUser;
}
// Blog Post methods
public async Task<PagedResultDto<BlogPostListDto>> GetPostsAsync(GetBlogPostsInput input)
{
var query = await _postRepository.GetQueryableAsync();
if (!string.IsNullOrWhiteSpace(input.Filter))
{
query = query.Where(x => x.Title.Contains(input.Filter) || x.Summary.Contains(input.Filter));
}
if (input.CategoryId.HasValue)
{
query = query.Where(x => x.CategoryId == input.CategoryId.Value);
}
if (!string.IsNullOrWhiteSpace(input.Tag))
{
var postIds = await _tagRepository
.GetListAsync(x => x.Tag == input.Tag)
.ContinueWith(t => t.Result.Select(x => x.PostId).ToList());
query = query.Where(x => postIds.Contains(x.Id));
}
if (input.AuthorId.HasValue)
{
query = query.Where(x => x.AuthorId == input.AuthorId.Value);
}
if (input.IsPublished.HasValue)
{
query = query.Where(x => x.IsPublished == input.IsPublished.Value);
}
// Sorting
if (input.SortBy == "popular")
{
query = query.OrderByDescending(x => x.LikeCount);
}
else if (input.SortBy == "trending")
{
query = query.OrderByDescending(x => x.ViewCount);
}
else // latest
{
query = query.OrderByDescending(x => x.CreationTime);
}
var totalCount = await AsyncExecuter.CountAsync(query);
var posts = await AsyncExecuter.ToListAsync(
query.Skip(input.SkipCount).Take(input.MaxResultCount)
);
var postDtos = new List<BlogPostListDto>();
foreach (var post in posts)
{
var dto = ObjectMapper.Map<BlogPost, BlogPostListDto>(post);
// Get category
var category = await _categoryRepository.GetAsync(post.CategoryId);
dto.Category = ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
// Get tags
var tags = await _tagRepository.GetListAsync(x => x.PostId == post.Id);
dto.Tags = tags.Select(x => x.Tag).ToList();
// Get author info
dto.Author = new AuthorDto
{
Id = post.AuthorId,
Name = post.CreatorId.HasValue ? "User" : "Unknown" // You should get actual user name
};
postDtos.Add(dto);
}
return new PagedResultDto<BlogPostListDto>(totalCount, postDtos);
}
public async Task<BlogPostDto> GetPostAsync(Guid id)
{
var post = await _postRepository.GetAsync(id);
var dto = ObjectMapper.Map<BlogPost, BlogPostDto>(post);
// Get category
dto.Category = ObjectMapper.Map<BlogCategory, BlogCategoryDto>(
await _categoryRepository.GetAsync(post.CategoryId)
);
// Get tags
var tags = await _tagRepository.GetListAsync(x => x.PostId == post.Id);
dto.Tags = tags.Select(x => x.Tag).ToList();
// Get author info
dto.Author = new AuthorDto
{
Id = post.AuthorId,
Name = post.CreatorId.HasValue ? "User" : "Unknown"
};
// Check if current user liked this post
if (_currentUser.IsAuthenticated)
{
dto.IsLiked = await _postLikeRepository.AnyAsync(
x => x.PostId == id && x.UserId == _currentUser.Id.Value
);
}
return dto;
}
public async Task<BlogPostDto> GetPostBySlugAsync(string slug)
{
var post = await _postRepository.FirstOrDefaultAsync(x => x.Slug == slug);
if (post == null)
{
throw new EntityNotFoundException(typeof(BlogPost));
}
return await GetPostAsync(post.Id);
}
public async Task<BlogPostDto> CreatePostAsync(CreateUpdateBlogPostDto input)
{
var post = new BlogPost(
GuidGenerator.Create(),
input.Title,
input.Slug,
input.Content,
input.Summary,
input.CategoryId,
_currentUser.Id.Value,
CurrentTenant.Id
);
post.CoverImage = input.CoverImage;
if (input.IsPublished)
{
post.Publish();
}
await _postRepository.InsertAsync(post);
// Add tags
foreach (var tag in input.Tags)
{
await _tagRepository.InsertAsync(new BlogPostTag(
GuidGenerator.Create(),
post.Id,
tag,
CurrentTenant.Id
));
}
// Update category post count
var category = await _categoryRepository.GetAsync(input.CategoryId);
category.IncrementPostCount();
await _categoryRepository.UpdateAsync(category);
return await GetPostAsync(post.Id);
}
public async Task<BlogPostDto> UpdatePostAsync(Guid id, CreateUpdateBlogPostDto input)
{
var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Blog.Posts.Update"))
{
throw new Volo.Abp.Authorization.AbpAuthorizationException();
}
post.Title = input.Title;
post.Slug = input.Slug;
post.Content = input.Content;
post.Summary = input.Summary;
post.CoverImage = input.CoverImage;
// Update category if changed
if (post.CategoryId != input.CategoryId)
{
var oldCategory = await _categoryRepository.GetAsync(post.CategoryId);
oldCategory.DecrementPostCount();
await _categoryRepository.UpdateAsync(oldCategory);
var newCategory = await _categoryRepository.GetAsync(input.CategoryId);
newCategory.IncrementPostCount();
await _categoryRepository.UpdateAsync(newCategory);
post.CategoryId = input.CategoryId;
}
await _postRepository.UpdateAsync(post);
// Update tags
await _tagRepository.DeleteAsync(x => x.PostId == id);
foreach (var tag in input.Tags)
{
await _tagRepository.InsertAsync(new BlogPostTag(
GuidGenerator.Create(),
post.Id,
tag,
CurrentTenant.Id
));
}
return await GetPostAsync(post.Id);
}
public async Task DeletePostAsync(Guid id)
{
var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Blog.Posts.Delete"))
{
throw new Volo.Abp.Authorization.AbpAuthorizationException();
}
// Update category post count
var category = await _categoryRepository.GetAsync(post.CategoryId);
category.DecrementPostCount();
await _categoryRepository.UpdateAsync(category);
await _postRepository.DeleteAsync(id);
}
public async Task<BlogPostDto> PublishPostAsync(Guid id)
{
var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Blog.Posts.Publish"))
{
throw new Volo.Abp.Authorization.AbpAuthorizationException();
}
post.Publish();
await _postRepository.UpdateAsync(post);
return await GetPostAsync(id);
}
public async Task<BlogPostDto> UnpublishPostAsync(Guid id)
{
var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("Blog.Posts.Publish"))
{
throw new Volo.Abp.Authorization.AbpAuthorizationException();
}
post.Unpublish();
await _postRepository.UpdateAsync(post);
return await GetPostAsync(id);
}
public async Task LikePostAsync(Guid id)
{
var existingLike = await _postLikeRepository.FirstOrDefaultAsync(
x => x.PostId == id && x.UserId == _currentUser.Id.Value
);
if (existingLike == null)
{
await _postLikeRepository.InsertAsync(new BlogPostLike(
GuidGenerator.Create(),
id,
_currentUser.Id.Value,
CurrentTenant.Id
));
// Update like count
var post = await _postRepository.GetAsync(id);
var likeCount = await _postLikeRepository.CountAsync(x => x.PostId == id);
post.UpdateLikeCount((int)likeCount);
await _postRepository.UpdateAsync(post);
}
}
public async Task UnlikePostAsync(Guid id)
{
var existingLike = await _postLikeRepository.FirstOrDefaultAsync(
x => x.PostId == id && x.UserId == _currentUser.Id.Value
);
if (existingLike != null)
{
await _postLikeRepository.DeleteAsync(existingLike);
// Update like count
var post = await _postRepository.GetAsync(id);
var likeCount = await _postLikeRepository.CountAsync(x => x.PostId == id);
post.UpdateLikeCount((int)likeCount);
await _postRepository.UpdateAsync(post);
}
}
[AllowAnonymous]
public async Task IncrementViewCountAsync(Guid id)
{
var post = await _postRepository.GetAsync(id);
post.IncrementViewCount();
await _postRepository.UpdateAsync(post);
}
// Blog Category methods
[AllowAnonymous]
public async Task<List<BlogCategoryDto>> GetCategoriesAsync()
{
var categories = await _categoryRepository.GetListAsync(x => x.IsActive);
return ObjectMapper.Map<List<BlogCategory>, List<BlogCategoryDto>>(categories);
}
public async Task<BlogCategoryDto> GetCategoryAsync(Guid id)
{
var category = await _categoryRepository.GetAsync(id);
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
}
[Authorize("Blog.Categories.Create")]
public async Task<BlogCategoryDto> CreateCategoryAsync(CreateUpdateBlogCategoryDto input)
{
var category = new BlogCategory(
GuidGenerator.Create(),
input.Name,
input.Slug,
input.Description,
CurrentTenant.Id
);
category.Icon = input.Icon;
category.DisplayOrder = input.DisplayOrder;
category.IsActive = input.IsActive;
await _categoryRepository.InsertAsync(category);
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
}
[Authorize("Blog.Categories.Update")]
public async Task<BlogCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateBlogCategoryDto 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;
await _categoryRepository.UpdateAsync(category);
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
}
[Authorize("Blog.Categories.Delete")]
public async Task DeleteCategoryAsync(Guid id)
{
// Check if category has posts
var hasPost = await _postRepository.AnyAsync(x => x.CategoryId == id);
if (hasPost)
{
throw new Volo.Abp.BusinessException("Cannot delete category with posts");
}
await _categoryRepository.DeleteAsync(id);
}
// Search and filters
public async Task<PagedResultDto<BlogPostListDto>> SearchPostsAsync(SearchBlogPostsInput input)
{
var query = await _postRepository.GetQueryableAsync();
if (!string.IsNullOrWhiteSpace(input.Query))
{
query = query.Where(x =>
x.Title.Contains(input.Query) ||
x.Content.Contains(input.Query) ||
x.Summary.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.IsPublished.HasValue)
{
query = query.Where(x => x.IsPublished == input.IsPublished.Value);
}
// Search by tag
if (!string.IsNullOrWhiteSpace(input.Tag))
{
var postIds = await _tagRepository
.GetListAsync(x => x.Tag == input.Tag)
.ContinueWith(t => t.Result.Select(x => x.PostId).ToList());
query = query.Where(x => postIds.Contains(x.Id));
}
var totalCount = await AsyncExecuter.CountAsync(query);
var posts = await AsyncExecuter.ToListAsync(
query.OrderByDescending(x => x.CreationTime)
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
);
var postDtos = new List<BlogPostListDto>();
foreach (var post in posts)
{
var dto = ObjectMapper.Map<BlogPost, BlogPostListDto>(post);
// Get category
var category = await _categoryRepository.GetAsync(post.CategoryId);
dto.Category = ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
// Get tags
var tags = await _tagRepository.GetListAsync(x => x.PostId == post.Id);
dto.Tags = tags.Select(x => x.Tag).ToList();
// Get author info
dto.Author = new AuthorDto
{
Id = post.AuthorId,
Name = post.CreatorId.HasValue ? "User" : "Unknown"
};
postDtos.Add(dto);
}
return new PagedResultDto<BlogPostListDto>(totalCount, postDtos);
}
public async Task<PagedResultDto<BlogPostListDto>> GetPostsByCategoryAsync(Guid categoryId, PagedAndSortedResultRequestDto input)
{
var searchInput = new GetBlogPostsInput
{
CategoryId = categoryId,
MaxResultCount = input.MaxResultCount,
SkipCount = input.SkipCount,
Sorting = input.Sorting
};
return await GetPostsAsync(searchInput);
}
public async Task<PagedResultDto<BlogPostListDto>> GetPostsByTagAsync(string tag, PagedAndSortedResultRequestDto input)
{
var searchInput = new GetBlogPostsInput
{
Tag = tag,
MaxResultCount = input.MaxResultCount,
SkipCount = input.SkipCount,
Sorting = input.Sorting
};
return await GetPostsAsync(searchInput);
}
public async Task<PagedResultDto<BlogPostListDto>> GetPostsByAuthorAsync(Guid authorId, PagedAndSortedResultRequestDto input)
{
var searchInput = new GetBlogPostsInput
{
AuthorId = authorId,
MaxResultCount = input.MaxResultCount,
SkipCount = input.SkipCount,
Sorting = input.Sorting
};
return await GetPostsAsync(searchInput);
}
// Tags
[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();
}
// Stats
[AllowAnonymous]
public async Task<BlogStatsDto> GetStatsAsync()
{
var stats = new BlogStatsDto
{
TotalPosts = (int)await _postRepository.CountAsync(),
PublishedPosts = (int)await _postRepository.CountAsync(x => x.IsPublished),
DraftPosts = (int)await _postRepository.CountAsync(x => !x.IsPublished),
TotalCategories = (int)await _categoryRepository.CountAsync(x => x.IsActive),
TotalViews = (await _postRepository.GetListAsync()).Sum(x => x.ViewCount),
TotalLikes = (await _postRepository.GetListAsync()).Sum(x => x.LikeCount),
TotalComments = 0, // You should implement comment count
PopularTags = await GetPopularTagsAsync(10)
};
// Get latest post
var latestPost = await _postRepository
.GetQueryableAsync()
.ContinueWith(async t =>
{
var query = await t;
return await AsyncExecuter.FirstOrDefaultAsync(
query.Where(x => x.IsPublished).OrderByDescending(x => x.CreationTime)
);
})
.Unwrap();
if (latestPost != null)
{
var dto = ObjectMapper.Map<BlogPost, BlogPostListDto>(latestPost);
// Get category
var category = await _categoryRepository.GetAsync(latestPost.CategoryId);
dto.Category = ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
stats.LatestPost = dto;
}
// Get most viewed post
var mostViewedPost = await _postRepository
.GetQueryableAsync()
.ContinueWith(async t =>
{
var query = await t;
return await AsyncExecuter.FirstOrDefaultAsync(
query.Where(x => x.IsPublished).OrderByDescending(x => x.ViewCount)
);
})
.Unwrap();
if (mostViewedPost != null)
{
var dto = ObjectMapper.Map<BlogPost, BlogPostListDto>(mostViewedPost);
// Get category
var category = await _categoryRepository.GetAsync(mostViewedPost.CategoryId);
dto.Category = ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
stats.MostViewedPost = dto;
}
return stats;
}
}
}

View file

@ -0,0 +1,648 @@
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 = topic.CreatorId.HasValue ? "User" : "Unknown"
};
// 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 = topic.CreatorId.HasValue ? "User" : "Unknown" // You should get actual user name
};
// 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 = topic.CreatorId.HasValue ? "User" : "Unknown" // You should get actual user name
};
// 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 = topic.CreatorId.HasValue ? "User" : "Unknown"
};
// 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

@ -7,7 +7,6 @@ using Kurs.Platform.Entities;
using Kurs.Platform.Extensions; using Kurs.Platform.Extensions;
using Kurs.Platform.Identity.Dto; using Kurs.Platform.Identity.Dto;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids; using Volo.Abp.Guids;

View file

@ -1,13 +1,6 @@
using AutoMapper; using AutoMapper;
using Kurs.Platform.Contacts; using Kurs.Platform.Blog;
using Kurs.Platform.Currencies; using Kurs.Platform.Forum;
using Kurs.Platform.Entities;
using Kurs.Platform.GlobalSearchs;
using Kurs.Platform.OrganizationUnits;
using Kurs.Platform.Sectors;
using Kurs.Platform.Skills;
using Kurs.Platform.Uoms;
using Volo.Abp.Identity;
namespace Kurs.Platform; namespace Kurs.Platform;
@ -15,28 +8,19 @@ public class PlatformApplicationAutoMapperProfile : Profile
{ {
public PlatformApplicationAutoMapperProfile() public PlatformApplicationAutoMapperProfile()
{ {
CreateMap<OrganizationUnit, OrganizationUnitDto>(); /* You can configure your AutoMapper mapping configuration here.
CreateMap<OrganizationUnitRole, OuIdentityRoleDto>(); * Alternatively, you can split your mapping configurations
CreateMap<CreateUpdateOrganizationUnitDto, OrganizationUnit>(); * into multiple profile classes for a better organization. */
CreateMap<GlobalSearch, GlobalSearchResultDto>(); // Blog mappings
CreateMap<BlogCategory, BlogCategoryDto>();
CreateMap<BlogPost, BlogPostDto>();
CreateMap<BlogPost, BlogPostListDto>();
CreateMap<Sector, SectorDto>(); // Forum mappings
CreateMap<UomCategory, UomCategoryDto>(); CreateMap<ForumCategory, ForumCategoryDto>();
CreateMap<Uom, UomDto>() CreateMap<ForumTopic, ForumTopicDto>();
.ForMember(dest => dest.UomCategoryName, opt => opt.MapFrom(src => src.UomCategory.Name)); CreateMap<ForumTopic, ForumTopicListDto>()
CreateMap<Currency, CurrencyDto>(); .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => src.CreationTime));
CreateMap<Bank, BankDto>();
CreateMap<BankAccount, BankAccountDto>()
.ForMember(dest => dest.BankName, opt => opt.MapFrom(src => src.Bank.Name))
.ForMember(dest => dest.CurrencyCode, opt => opt.MapFrom(src => src.Currency.Code));
CreateMap<CountryGroup, CountryGroupDto>();
CreateMap<Country, CountryDto>();
CreateMap<State, StateDto>();
CreateMap<SkillType, SkillTypeDto>();
CreateMap<SkillLevel, SkillLevelDto>();
CreateMap<Skill, SkillDto>();
CreateMap<ContactTag, ContactTagDto>();
CreateMap<ContactTitle, ContactTitleDto>();
} }
} }

View file

@ -522,6 +522,18 @@
"en": "Abp Settings", "en": "Abp Settings",
"tr": "Abp Ayarları" "tr": "Abp Ayarları"
}, },
{
"resourceName": "Platform",
"key": "App.Blog",
"en": "Blog",
"tr": "Blog"
},
{
"resourceName": "Platform",
"key": "App.Forum",
"en": "Forum",
"tr": "Forum"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Home", "key": "App.Home",
@ -651,8 +663,8 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "AbpTenantManagement", "key": "AbpTenantManagement",
"en": "Tenants", "en": "Organizations",
"tr": "Tenants" "tr": "Kurumlar"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
@ -663,8 +675,8 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "AbpTenantManagement.Tenants", "key": "AbpTenantManagement.Tenants",
"en": "Tenants", "en": "Organizations",
"tr": "Şirketler" "tr": "Kurumlar"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
@ -2409,8 +2421,8 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "Sirket", "key": "Sirket",
"en": "Tenant", "en": "Organization",
"tr": "Şirket" "tr": "Kurum"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
@ -5878,96 +5890,6 @@
"RequiredPermissionName": "App.Setting", "RequiredPermissionName": "App.Setting",
"IsDisabled": false "IsDisabled": false
}, },
{
"ParentCode": "App.Administration",
"Code": "Abp.Identity",
"DisplayName": "Abp.Identity",
"Order": 2,
"Url": null,
"Icon": "FcConferenceCall",
"RequiredPermissionName": null,
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "Abp.Identity.PermissionGroups",
"DisplayName": "Abp.Identity.PermissionGroups",
"Order": 1,
"Url": "/list/list-permissiongroup",
"Icon": "FcEngineering",
"RequiredPermissionName": "Abp.Identity.PermissionGroups",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "Abp.Identity.Permissions",
"DisplayName": "Abp.Identity.Permissions",
"Order": 2,
"Url": "/list/list-permission",
"Icon": "FcSupport",
"RequiredPermissionName": "Abp.Identity.Permissions",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "AbpIdentity.Roles",
"DisplayName": "AbpIdentity.Roles",
"Order": 3,
"Url": "/list/list-role",
"Icon": "FcFlowChart",
"RequiredPermissionName": "AbpIdentity.Roles",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "AbpIdentity.Users",
"DisplayName": "AbpIdentity.Users",
"Order": 4,
"Url": "/list/list-user",
"Icon": "FcBusinessman",
"RequiredPermissionName": "AbpIdentity.Users",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "Abp.Identity.OrganizationUnits",
"DisplayName": "Abp.Identity.OrganizationUnits",
"Order": 5,
"Url": "/admin/ous",
"Icon": "FcOrganization",
"RequiredPermissionName": "Abp.Identity.OrganizationUnits",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "AbpIdentity.Users.ClaimType",
"DisplayName": "AbpIdentity.Users.ClaimType",
"Order": 6,
"Url": "/list/list-claimtype",
"Icon": "FcOrganization",
"RequiredPermissionName": "AbpIdentity.Users.ClaimType",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "App.IpRestrictions",
"DisplayName": "App.IpRestrictions",
"Order": 7,
"Url": "/list/list-iprestriction",
"Icon": "FcNfcSign",
"RequiredPermissionName": "App.IpRestrictions",
"IsDisabled": false
},
{
"ParentCode": "AbpIdentity.Users",
"Code": "AbpIdentity.Users.SecurityLog",
"DisplayName": "AbpIdentity.Users.SecurityLog",
"Order": 8,
"Url": "/list/list-securitylog",
"Icon": "FcPrivacy",
"RequiredPermissionName": "AbpIdentity.Users.SecurityLog",
"IsDisabled": false
},
{ {
"ParentCode": "App.Saas", "ParentCode": "App.Saas",
"Code": "App.Listforms", "Code": "App.Listforms",
@ -6068,6 +5990,116 @@
"RequiredPermissionName": "App.PublicApis", "RequiredPermissionName": "App.PublicApis",
"IsDisabled": false "IsDisabled": false
}, },
{
"ParentCode": "App.Saas",
"Code": "App.Blog",
"DisplayName": "App.Blog",
"Order": 10,
"Url": "/admin/blog/management",
"Icon": "FcTemplate",
"RequiredPermissionName": "App.Blog",
"IsDisabled": false
},
{
"ParentCode": "App.Saas",
"Code": "App.Forum",
"DisplayName": "App.Forum",
"Order": 11,
"Url": "/admin/forum/management",
"Icon": "FcReading",
"RequiredPermissionName": "App.Forum",
"IsDisabled": false
},
{
"ParentCode": "App.Administration",
"Code": "Abp.Identity",
"DisplayName": "Abp.Identity",
"Order": 2,
"Url": null,
"Icon": "FcConferenceCall",
"RequiredPermissionName": null,
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "Abp.Identity.PermissionGroups",
"DisplayName": "Abp.Identity.PermissionGroups",
"Order": 1,
"Url": "/list/list-permissiongroup",
"Icon": "FcEngineering",
"RequiredPermissionName": "Abp.Identity.PermissionGroups",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "Abp.Identity.Permissions",
"DisplayName": "Abp.Identity.Permissions",
"Order": 2,
"Url": "/list/list-permission",
"Icon": "FcSupport",
"RequiredPermissionName": "Abp.Identity.Permissions",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "AbpIdentity.Roles",
"DisplayName": "AbpIdentity.Roles",
"Order": 3,
"Url": "/list/list-role",
"Icon": "FcFlowChart",
"RequiredPermissionName": "AbpIdentity.Roles",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "AbpIdentity.Users",
"DisplayName": "AbpIdentity.Users",
"Order": 4,
"Url": "/list/list-user",
"Icon": "FcBusinessman",
"RequiredPermissionName": "AbpIdentity.Users",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "Abp.Identity.OrganizationUnits",
"DisplayName": "Abp.Identity.OrganizationUnits",
"Order": 5,
"Url": "/admin/ous",
"Icon": "FcOrganization",
"RequiredPermissionName": "Abp.Identity.OrganizationUnits",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "AbpIdentity.Users.ClaimType",
"DisplayName": "AbpIdentity.Users.ClaimType",
"Order": 6,
"Url": "/list/list-claimtype",
"Icon": "FcOrganization",
"RequiredPermissionName": "AbpIdentity.Users.ClaimType",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "App.IpRestrictions",
"DisplayName": "App.IpRestrictions",
"Order": 7,
"Url": "/list/list-iprestriction",
"Icon": "FcNfcSign",
"RequiredPermissionName": "App.IpRestrictions",
"IsDisabled": false
},
{
"ParentCode": "AbpIdentity.Users",
"Code": "AbpIdentity.Users.SecurityLog",
"DisplayName": "AbpIdentity.Users.SecurityLog",
"Order": 8,
"Url": "/list/list-securitylog",
"Icon": "FcPrivacy",
"RequiredPermissionName": "AbpIdentity.Users.SecurityLog",
"IsDisabled": false
},
{ {
"ParentCode": "App.Administration", "ParentCode": "App.Administration",
"Code": "App.AuditLogs", "Code": "App.AuditLogs",
@ -6135,6 +6167,14 @@
{ {
"Name": "App.AuditLogs", "Name": "App.AuditLogs",
"DisplayName": "App.AuditLogs" "DisplayName": "App.AuditLogs"
},
{
"Name": "App.Blog",
"DisplayName": "App.Blog"
},
{
"Name": "App.Forum",
"DisplayName": "App.Forum"
} }
], ],
"PermissionDefinitionRecords": [ "PermissionDefinitionRecords": [
@ -6330,6 +6370,22 @@
"IsEnabled": true, "IsEnabled": true,
"MultiTenancySide": 2 "MultiTenancySide": 2
}, },
{
"GroupName": "App.Blog",
"Name": "App.Blog",
"ParentName": null,
"DisplayName": "App.Blog",
"IsEnabled": true,
"MultiTenancySide": 2
},
{
"GroupName": "App.Forum",
"Name": "App.Forum",
"ParentName": null,
"DisplayName": "App.Forum",
"IsEnabled": true,
"MultiTenancySide": 2
},
{ {
"GroupName": "App.Setting", "GroupName": "App.Setting",
"Name": "Abp.Account", "Name": "Abp.Account",
@ -7329,6 +7385,70 @@
"DisplayName": "App.SiteManagement.Theme", "DisplayName": "App.SiteManagement.Theme",
"IsEnabled": true, "IsEnabled": true,
"MultiTenancySide": 3 "MultiTenancySide": 3
},
{
"GroupName": "App.Blog",
"Name": "App.Blog.Create",
"ParentName": "App.Blog",
"DisplayName": "Create",
"IsEnabled": true,
"MultiTenancySide": 2
},
{
"GroupName": "App.Blog",
"Name": "App.Blog.Delete",
"ParentName": "App.Blog",
"DisplayName": "Delete",
"IsEnabled": true,
"MultiTenancySide": 2
},
{
"GroupName": "App.Blog",
"Name": "App.Blog.Export",
"ParentName": "App.Blog",
"DisplayName": "Export",
"IsEnabled": true,
"MultiTenancySide": 2
},
{
"GroupName": "App.Blog",
"Name": "App.Blog.Update",
"ParentName": "App.Blog",
"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": [ "Sectors": [
@ -19531,5 +19651,39 @@
{ "Title": "Bayan", "Abbreviation": "Bayan" }, { "Title": "Bayan", "Abbreviation": "Bayan" },
{ "Title": "Doktora", "Abbreviation": "Dr." }, { "Title": "Doktora", "Abbreviation": "Dr." },
{ "Title": "Profesör", "Abbreviation": "Prof." } { "Title": "Profesör", "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
}
] ]
} }

View file

@ -302,7 +302,9 @@ public static class PlatformConsts
} }
public const string AuditLogs = Prefix.App + ".AuditLogs"; public const string AuditLogs = Prefix.App + ".AuditLogs";
public const string EntityChanges = Prefix.App + ".EntityChanges"; public const string Branches = Prefix.App + ".Branches";
public const string Forum = Prefix.App + ".Forum";
public const string Blog = Prefix.App + ".Blog";
} }
public static class ListFormCodes public static class ListFormCodes

View file

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Blog
{
public class BlogCategory : 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 int PostCount { get; set; }
public virtual ICollection<BlogPost> Posts { get; set; }
protected BlogCategory()
{
Posts = new HashSet<BlogPost>();
}
public BlogCategory(
Guid id,
string name,
string slug,
string description = null,
Guid? tenantId = null) : base(id)
{
Name = name;
Slug = slug;
Description = description;
TenantId = tenantId;
Icon = null;
DisplayOrder = 0;
IsActive = true;
PostCount = 0;
Posts = new HashSet<BlogPost>();
}
public void IncrementPostCount()
{
PostCount++;
}
public void DecrementPostCount()
{
if (PostCount > 0)
PostCount--;
}
}
}

View file

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Blog
{
public class BlogComment : FullAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid PostId { get; set; }
public virtual BlogPost Post { get; set; }
public string Content { get; set; }
public Guid AuthorId { get; set; }
public Guid? ParentId { get; set; }
public virtual BlogComment Parent { get; set; }
public virtual ICollection<BlogComment> Replies { get; set; }
public virtual ICollection<BlogCommentLike> Likes { get; set; }
public int LikeCount { get; set; }
protected BlogComment()
{
Replies = new HashSet<BlogComment>();
Likes = new HashSet<BlogCommentLike>();
}
public BlogComment(
Guid id,
Guid postId,
string content,
Guid authorId,
Guid? parentId = null,
Guid? tenantId = null) : base(id)
{
PostId = postId;
Content = content;
AuthorId = authorId;
ParentId = parentId;
TenantId = tenantId;
LikeCount = 0;
Replies = new HashSet<BlogComment>();
Likes = new HashSet<BlogCommentLike>();
}
public void UpdateLikeCount(int count)
{
LikeCount = count;
}
}
}

View file

@ -0,0 +1,31 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Blog
{
public class BlogCommentLike : CreationAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid CommentId { get; set; }
public virtual BlogComment Comment { get; set; }
public Guid UserId { get; set; }
protected BlogCommentLike()
{
}
public BlogCommentLike(
Guid id,
Guid commentId,
Guid userId,
Guid? tenantId = null) : base(id)
{
CommentId = commentId;
UserId = userId;
TenantId = tenantId;
}
}
}

View file

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Blog
{
public class BlogPost : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public string Title { get; set; }
public string Slug { get; set; }
public string Content { get; set; }
public string Summary { get; set; }
public string CoverImage { get; set; }
public Guid CategoryId { get; set; }
public virtual BlogCategory Category { get; set; }
public Guid AuthorId { get; set; }
public int ViewCount { get; set; }
public int LikeCount { get; set; }
public int CommentCount { get; set; }
public bool IsPublished { get; set; }
public DateTime? PublishedAt { get; set; }
public virtual ICollection<BlogPostTag> Tags { get; set; }
public virtual ICollection<BlogComment> Comments { get; set; }
public virtual ICollection<BlogPostLike> Likes { get; set; }
protected BlogPost()
{
Tags = new HashSet<BlogPostTag>();
Comments = new HashSet<BlogComment>();
Likes = new HashSet<BlogPostLike>();
}
public BlogPost(
Guid id,
string title,
string slug,
string content,
string summary,
Guid categoryId,
Guid authorId,
Guid? tenantId = null) : base(id)
{
Title = title;
Slug = slug;
Content = content;
Summary = summary;
CategoryId = categoryId;
AuthorId = authorId;
TenantId = tenantId;
ViewCount = 0;
LikeCount = 0;
CommentCount = 0;
IsPublished = false;
Tags = new HashSet<BlogPostTag>();
Comments = new HashSet<BlogComment>();
Likes = new HashSet<BlogPostLike>();
}
public void Publish()
{
IsPublished = true;
PublishedAt = DateTime.UtcNow;
}
public void Unpublish()
{
IsPublished = false;
PublishedAt = null;
}
public void IncrementViewCount()
{
ViewCount++;
}
public void UpdateLikeCount(int count)
{
LikeCount = count;
}
public void UpdateCommentCount(int count)
{
CommentCount = count;
}
}
}

View file

@ -0,0 +1,31 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Blog
{
public class BlogPostLike : CreationAuditedEntity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid PostId { get; set; }
public virtual BlogPost Post { get; set; }
public Guid UserId { get; set; }
protected BlogPostLike()
{
}
public BlogPostLike(
Guid id,
Guid postId,
Guid userId,
Guid? tenantId = null) : base(id)
{
PostId = postId;
UserId = userId;
TenantId = tenantId;
}
}
}

View file

@ -0,0 +1,31 @@
using System;
using Volo.Abp.Domain.Entities;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Blog
{
public class BlogPostTag : Entity<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
public Guid PostId { get; set; }
public virtual BlogPost Post { get; set; }
public string Tag { get; set; }
protected BlogPostTag()
{
}
public BlogPostTag(
Guid id,
Guid postId,
string tag,
Guid? tenantId = null) : base(id)
{
PostId = postId;
Tag = tag;
TenantId = tenantId;
}
}
}

View file

@ -0,0 +1,58 @@
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

@ -363,6 +363,8 @@ public static class SeedConsts
public const string PublicApis = Prefix.App + ".PublicApis"; public const string PublicApis = Prefix.App + ".PublicApis";
public const string AuditLogs = Prefix.App + ".AuditLogs"; public const string AuditLogs = Prefix.App + ".AuditLogs";
public const string Branches = Prefix.App + ".Branches"; public const string Branches = Prefix.App + ".Branches";
public const string Forum = Prefix.App + ".Forum";
public const string Blog = Prefix.App + ".Blog";
} }
public static class DataSources public static class DataSources

View file

@ -0,0 +1,86 @@
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

@ -0,0 +1,62 @@
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

@ -0,0 +1,31 @@
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

@ -0,0 +1,117 @@
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

@ -0,0 +1,31 @@
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

@ -0,0 +1,31 @@
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,5 +1,7 @@
using Kurs.Languages.EntityFrameworkCore; using Kurs.Languages.EntityFrameworkCore;
using Kurs.Platform.Entities; using Kurs.Platform.Entities;
using Kurs.Platform.Blog;
using Kurs.Platform.Forum;
using Kurs.Settings.EntityFrameworkCore; using Kurs.Settings.EntityFrameworkCore;
using Kurs.MailQueue.EntityFrameworkCore; using Kurs.MailQueue.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -57,6 +59,22 @@ public class PlatformDbContext :
public DbSet<ContactTag> ContactTags { get; set; } public DbSet<ContactTag> ContactTags { get; set; }
public DbSet<ContactTitle> ContactTitles { get; set; } public DbSet<ContactTitle> ContactTitles { get; set; }
// Blog Entities
public DbSet<BlogPost> BlogPosts { get; set; }
public DbSet<BlogCategory> BlogCategories { get; set; }
public DbSet<BlogComment> BlogComments { get; set; }
public DbSet<BlogPostTag> BlogPostTags { get; set; }
public DbSet<BlogPostLike> BlogPostLikes { get; set; }
public DbSet<BlogCommentLike> BlogCommentLikes { 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 #region Entities from the modules
/* Notice: We only implemented IIdentityDbContext and ITenantManagementDbContext /* Notice: We only implemented IIdentityDbContext and ITenantManagementDbContext
@ -391,6 +409,187 @@ public class PlatformDbContext :
b.Property(x => x.Abbreviation).HasMaxLength(64); b.Property(x => x.Abbreviation).HasMaxLength(64);
}); });
// Blog Entity Configurations
builder.Entity<BlogCategory>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + "BlogCategories", PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
b.Property(x => x.Slug).IsRequired().HasMaxLength(128);
b.Property(x => x.Description).HasMaxLength(512);
b.HasIndex(x => x.Slug);
});
builder.Entity<BlogPost>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + "BlogPosts", PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
b.Property(x => x.Slug).IsRequired().HasMaxLength(256);
b.Property(x => x.Summary).IsRequired().HasMaxLength(512);
b.Property(x => x.Content).IsRequired();
b.Property(x => x.CoverImage).HasMaxLength(512);
b.HasIndex(x => x.Slug);
b.HasIndex(x => x.IsPublished);
b.HasIndex(x => x.PublishedAt);
b.HasOne(x => x.Category)
.WithMany(x => x.Posts)
.HasForeignKey(x => x.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<BlogComment>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + "BlogComments", PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Content).IsRequired().HasMaxLength(2048);
b.HasOne(x => x.Post)
.WithMany(x => x.Comments)
.HasForeignKey(x => x.PostId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.Parent)
.WithMany(x => x.Replies)
.HasForeignKey(x => x.ParentId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<BlogPostTag>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + "BlogPostTags", PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Tag).IsRequired().HasMaxLength(64);
b.HasIndex(x => new { x.PostId, x.Tag }).IsUnique();
b.HasIndex(x => x.Tag);
b.HasOne(x => x.Post)
.WithMany(x => x.Tags)
.HasForeignKey(x => x.PostId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<BlogPostLike>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + "BlogPostLikes", 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);
});
builder.Entity<BlogCommentLike>(b =>
{
b.ToTable(PlatformConsts.DbTablePrefix + "BlogCommentLikes", PlatformConsts.DbSchema);
b.ConfigureByConvention();
b.HasIndex(x => new { x.CommentId, x.UserId }).IsUnique();
b.HasOne(x => x.Comment)
.WithMany(x => x.Likes)
.HasForeignKey(x => x.CommentId)
.OnDelete(DeleteBehavior.Cascade);
});
// 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

@ -0,0 +1,491 @@
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),
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: "PBlogComments",
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),
Content = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
AuthorId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ParentId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LikeCount = 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_PBlogComments", x => x.Id);
table.ForeignKey(
name: "FK_PBlogComments_PBlogComments_ParentId",
column: x => x.ParentId,
principalTable: "PBlogComments",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PBlogComments_PBlogPosts_PostId",
column: x => x.PostId,
principalTable: "PBlogPosts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PBlogPostLikes",
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_PBlogPostLikes", x => x.Id);
table.ForeignKey(
name: "FK_PBlogPostLikes_PBlogPosts_PostId",
column: x => x.PostId,
principalTable: "PBlogPosts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PBlogPostTags",
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),
Tag = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PBlogPostTags", x => x.Id);
table.ForeignKey(
name: "FK_PBlogPostTags_PBlogPosts_PostId",
column: x => x.PostId,
principalTable: "PBlogPosts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
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: "PBlogCommentLikes",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
CommentId = 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_PBlogCommentLikes", x => x.Id);
table.ForeignKey(
name: "FK_PBlogCommentLikes_PBlogComments_CommentId",
column: x => x.CommentId,
principalTable: "PBlogComments",
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_PBlogCommentLikes_CommentId_UserId",
table: "PBlogCommentLikes",
columns: new[] { "CommentId", "UserId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PBlogComments_ParentId",
table: "PBlogComments",
column: "ParentId");
migrationBuilder.CreateIndex(
name: "IX_PBlogComments_PostId",
table: "PBlogComments",
column: "PostId");
migrationBuilder.CreateIndex(
name: "IX_PBlogPostLikes_PostId_UserId",
table: "PBlogPostLikes",
columns: new[] { "PostId", "UserId" },
unique: true);
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_PBlogPostTags_PostId_Tag",
table: "PBlogPostTags",
columns: new[] { "PostId", "Tag" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PBlogPostTags_Tag",
table: "PBlogPostTags",
column: "Tag");
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: "PBlogCommentLikes");
migrationBuilder.DropTable(
name: "PBlogPostLikes");
migrationBuilder.DropTable(
name: "PBlogPostTags");
migrationBuilder.DropTable(
name: "PForumPostLikes");
migrationBuilder.DropTable(
name: "PForumTopicLikes");
migrationBuilder.DropTable(
name: "PForumTopicTags");
migrationBuilder.DropTable(
name: "PBlogComments");
migrationBuilder.DropTable(
name: "PForumPosts");
migrationBuilder.DropTable(
name: "PBlogPosts");
migrationBuilder.DropTable(
name: "PForumTopics");
migrationBuilder.DropTable(
name: "PBlogCategories");
migrationBuilder.DropTable(
name: "PForumCategories");
}
}
}

View file

@ -650,6 +650,340 @@ namespace Kurs.Platform.Migrations
b.ToTable("PNotificationRule", (string)null); b.ToTable("PNotificationRule", (string)null);
}); });
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", 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")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.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<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<int>("PostCount")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("Slug");
b.ToTable("PBlogCategories", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogComment", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Content")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
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>("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?>("ParentId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("PostId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("PostId");
b.ToTable("PBlogComments", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogCommentLike", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("CommentId")
.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>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("CommentId", "UserId")
.IsUnique();
b.ToTable("PBlogCommentLikes", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPost", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<int>("CommentCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.IsRequired()
.HasMaxLength(40)
.HasColumnType("nvarchar(40)")
.HasColumnName("ConcurrencyStamp");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("CoverImage")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
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>("IsPublished")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int>("LikeCount")
.HasColumnType("int");
b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime2");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
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("IsPublished");
b.HasIndex("PublishedAt");
b.HasIndex("Slug");
b.ToTable("PBlogPosts", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPostLike", 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("PBlogPostLikes", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPostTag", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("PostId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Tag")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("Tag");
b.HasIndex("PostId", "Tag")
.IsUnique();
b.ToTable("PBlogPostTags", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b => modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -2249,6 +2583,347 @@ namespace Kurs.Platform.Migrations
b.ToTable("PUomCategory", (string)null); 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 => modelBuilder.Entity("Kurs.Settings.Entities.SettingDefinition", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -4322,6 +4997,68 @@ namespace Kurs.Platform.Migrations
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
}); });
modelBuilder.Entity("Kurs.Platform.Blog.BlogComment", b =>
{
b.HasOne("Kurs.Platform.Blog.BlogComment", "Parent")
.WithMany("Replies")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Kurs.Platform.Blog.BlogPost", "Post")
.WithMany("Comments")
.HasForeignKey("PostId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Parent");
b.Navigation("Post");
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogCommentLike", b =>
{
b.HasOne("Kurs.Platform.Blog.BlogComment", "Comment")
.WithMany("Likes")
.HasForeignKey("CommentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Comment");
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPost", b =>
{
b.HasOne("Kurs.Platform.Blog.BlogCategory", "Category")
.WithMany("Posts")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPostLike", b =>
{
b.HasOne("Kurs.Platform.Blog.BlogPost", "Post")
.WithMany("Likes")
.HasForeignKey("PostId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Post");
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPostTag", b =>
{
b.HasOne("Kurs.Platform.Blog.BlogPost", "Post")
.WithMany("Tags")
.HasForeignKey("PostId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Post");
});
modelBuilder.Entity("Kurs.Platform.Entities.BankAccount", b => modelBuilder.Entity("Kurs.Platform.Entities.BankAccount", b =>
{ {
b.HasOne("Kurs.Platform.Entities.Bank", "Bank") b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
@ -4391,6 +5128,61 @@ namespace Kurs.Platform.Migrations
b.Navigation("UomCategory"); 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 => modelBuilder.Entity("Skill", b =>
{ {
b.HasOne("SkillType", null) b.HasOne("SkillType", null)
@ -4552,6 +5344,27 @@ namespace Kurs.Platform.Migrations
b.Navigation("Texts"); b.Navigation("Texts");
}); });
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
{
b.Navigation("Posts");
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogComment", b =>
{
b.Navigation("Likes");
b.Navigation("Replies");
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPost", b =>
{
b.Navigation("Comments");
b.Navigation("Likes");
b.Navigation("Tags");
});
modelBuilder.Entity("Kurs.Platform.Entities.Country", b => modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
{ {
b.Navigation("States"); b.Navigation("States");
@ -4562,6 +5375,25 @@ namespace Kurs.Platform.Migrations
b.Navigation("Units"); 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 => modelBuilder.Entity("SkillType", b =>
{ {
b.Navigation("Levels"); b.Navigation("Levels");

View file

@ -0,0 +1,159 @@
# Company Uygulaması API Entegrasyonu
Bu dokümantasyon, Company uygulamasına yapılan API entegrasyonu, üyelik sistemi, forum ve dinamik blog özelliklerini açıklamaktadır.
## 🚀 Yapılan Değişiklikler
### 1. API Entegrasyonu
- **Axios** ve **React Query** kullanılarak API yapısı kuruldu
- API servis katmanı oluşturuldu (`/src/services/api/`)
- Interceptor'lar ile otomatik token yönetimi eklendi
### 2. Üyelik Sistemi
- **Zustand** ile state management kuruldu
- Login ve Register sayfaları oluşturuldu
- JWT token tabanlı authentication sistemi entegre edildi
- Protected route yapısı kuruldu
### 3. Forum Sistemi
- Kapsamlı forum sayfası oluşturuldu
- Kategori, konu ve mesaj yönetimi
- Like, pin, lock gibi özellikler
- Etiket sistemi
### 4. Dinamik Blog
- Static blog yapısı dinamik hale getirildi
- Kategori filtreleme
- Arama özelliği
- Sayfalama (pagination)
- Etiket sistemi
## 📦 Yeni Eklenen Paketler
```json
{
"axios": "^1.6.5",
"@tanstack/react-query": "^5.17.9",
"zustand": "^4.4.7",
"react-hook-form": "^7.48.2",
"date-fns": "^3.2.0",
"react-markdown": "^9.0.1",
"react-hot-toast": "^2.4.1"
}
```
## 🛠️ Kurulum
1. Paketleri yükleyin:
```bash
cd company
npm install
```
2. Environment değişkenlerini ayarlayın:
```env
VITE_API_URL=http://localhost:44328
```
## 📁 Yeni Dosya Yapısı
```
company/src/
├── services/
│ └── api/
│ ├── config.ts # Axios yapılandırması
│ ├── auth.service.ts # Authentication servisi
│ ├── blog.service.ts # Blog API servisi
│ └── forum.service.ts # Forum API servisi
├── store/
│ └── authStore.ts # Zustand auth store
├── pages/
│ ├── Login.tsx # Giriş sayfası
│ ├── Register.tsx # Kayıt sayfası
│ ├── Forum.tsx # Forum ana sayfası
│ └── Blog.tsx # Dinamik blog sayfası (güncellendi)
└── components/
└── layout/
└── Header.tsx # Auth butonları eklendi
```
## 🔐 Authentication Akışı (UI Uygulaması ile Aynı)
1. Kullanıcı login sayfasından giriş yapar
2. OAuth2 `/connect/token` endpoint'i kullanılarak token alınır
3. Access token ve refresh token localStorage'a kaydedilir
4. `/api/abp/application-configuration` endpoint'inden kullanıcı bilgileri çekilir
5. Axios interceptor ile her istekte:
- Authorization header'a Bearer token eklenir
- `__tenant` header'a tenant ID eklenir (multi-tenancy desteği)
6. Protected route'lar için authentication kontrolü yapılır
7. Token expire olduğunda refresh token ile yenilenir
8. Refresh token da expire olduysa otomatik logout
### OAuth2 Parametreleri
- **grant_type**: password
- **scope**: offline_access Platform
- **client_id**: Platform_App
- **client_secret**: 1q2w3e*
## 🌟 Özellikler
### Forum
- Kategori bazlı konu listeleme
- Yeni konu açma
- Konulara cevap yazma
- Like/Unlike
- Pin/Unpin (moderatör)
- Lock/Unlock (moderatör)
- Çözüldü olarak işaretleme
- Etiket sistemi
- Arama özelliği
### Blog
- Dinamik içerik yönetimi
- Kategori filtreleme
- Arama
- Sayfalama
- Yorum sistemi
- Like sistemi
- Etiketler
## 🔄 API Endpoints
### Authentication (UI ile Aynı Endpoint'ler)
- `POST /connect/token` - OAuth2 token endpoint (Login & Refresh)
- `POST /api/account/register` - Register
- `GET /api/abp/application-configuration` - Current user & app config
### Blog
- `GET /api/app/blog/posts` - Blog listesi
- `GET /api/app/blog/posts/:id` - Blog detay
- `GET /api/app/blog/categories` - Kategoriler
- `POST /api/app/blog/comments` - Yorum ekle
### Forum
- `GET /api/app/forum/categories` - Forum kategorileri
- `GET /api/app/forum/topics` - Konu listesi
- `POST /api/app/forum/topics` - Yeni konu
- `GET /api/app/forum/topics/:id/posts` - Konu mesajları
- `POST /api/app/forum/posts` - Yeni mesaj
## ⚠️ Dikkat Edilmesi Gerekenler
1. **UI uygulaması ile aynı auth yapısı kullanılıyor** - Aynı token'lar ve endpoint'ler
2. Blog ve Forum API endpoint'leri henüz backend'de implement edilmemiş olabilir
3. CORS ayarlarının yapılması gerekebilir (Company domain'i için)
4. Production için environment değişkenlerinin güncellenmesi gerekir
5. Error handling mekanizmaları geliştirilmeli
6. Multi-tenancy desteği aktif - Tenant header'ı otomatik ekleniyor
## 🚧 Yapılacaklar
- [ ] Backend API endpoint'lerinin implement edilmesi
- [ ] Profil sayfası oluşturulması
- [ ] Forum moderasyon paneli
- [ ] Blog admin paneli
- [ ] Bildirim sistemi
- [ ] Real-time özellikler (WebSocket)
- [ ] Dosya yükleme sistemi
- [ ] Gelişmiş arama özellikleri

1422
company/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,14 +14,21 @@
"format": "npm run prettier:fix && npm run lint:fix" "format": "npm run prettier:fix && npm run lint:fix"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.17.9",
"axios": "^1.6.5",
"date-fns": "^3.6.0",
"glob": "^10.4.5", "glob": "^10.4.5",
"lucide-react": "^0.344.0", "lucide-react": "^0.344.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
"react-hot-toast": "^2.4.1",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.22.2", "react-router-dom": "^6.22.2",
"react-slick": "^0.30.3", "react-slick": "^0.30.3",
"rimraf": "^4.4.1", "rimraf": "^4.4.1",
"slick-carousel": "^1.8.1" "slick-carousel": "^1.8.1",
"zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.55", "@types/react": "^18.2.55",

View file

@ -1,5 +1,7 @@
import React from 'react'; import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from 'react-hot-toast';
import Layout from './components/layout/Layout'; import Layout from './components/layout/Layout';
import Home from './pages/Home'; import Home from './pages/Home';
import Products from './pages/Products'; import Products from './pages/Products';
@ -8,27 +10,110 @@ import About from './pages/About';
import Blog from './pages/Blog'; import Blog from './pages/Blog';
import Contact from './pages/Contact'; import Contact from './pages/Contact';
import BlogDetail from './pages/BlogDetail'; import BlogDetail from './pages/BlogDetail';
import NotFound from './pages/NotFound'; // 404 bileşenini import et 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 { LanguageProvider } from './context/LanguageContext';
import { useAuthStore } from './store/authStore';
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
});
// 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() { function App() {
const { checkAuth } = useAuthStore();
useEffect(() => {
checkAuth();
}, [checkAuth]);
return ( return (
<LanguageProvider> <QueryClientProvider client={queryClient}>
<Router> <LanguageProvider>
<Layout> <Router>
<Routes> <Layout>
<Route path="/" element={<Home />} /> <Routes>
<Route path="/products" element={<Products />} /> <Route path="/" element={<Home />} />
<Route path="/services" element={<Services />} /> <Route path="/products" element={<Products />} />
<Route path="/about" element={<About />} /> <Route path="/services" element={<Services />} />
<Route path="/blog" element={<Blog />} /> <Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} /> <Route path="/blog" element={<Blog />} />
<Route path="/blog/:id" element={<BlogDetail />} /> <Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} /> <Route path="/blog/:id" element={<BlogDetail />} />
</Routes> <Route path="/login" element={<LoginWithTenant />} />
</Layout> <Route path="/register" element={<Register />} />
</Router>
</LanguageProvider> {/* 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>
<Toaster position="top-right" />
</Router>
</LanguageProvider>
</QueryClientProvider>
); );
} }

View file

@ -1,13 +1,16 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Menu, X, Globe } from "lucide-react"; import { Menu, X, Globe, LogIn, LogOut, User, MessageSquare, Home, Info, Package, Briefcase, BookOpen, Phone } from "lucide-react";
import Logo from "./Logo"; import Logo from "./Logo";
import { Link } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useLanguage } from "../../context/LanguageContext"; import { useLanguage } from "../../context/LanguageContext";
import { useAuthStore } from "../../store/authStore";
const Header: React.FC = () => { const Header: React.FC = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const { language, setLanguage, t } = useLanguage(); const { language, setLanguage, t } = useLanguage();
const { isAuthenticated, user, logout } = useAuthStore();
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
@ -24,14 +27,19 @@ const Header: React.FC = () => {
const toggleMenu = () => setIsOpen(!isOpen); const toggleMenu = () => setIsOpen(!isOpen);
const toggleLanguage = () => setLanguage(language === "en" ? "tr" : "en"); const toggleLanguage = () => setLanguage(language === "en" ? "tr" : "en");
const handleLogout = () => {
logout();
navigate('/');
};
const navLinks = [ const navLinks = [
{ name: t("nav.home"), path: "/" }, { name: t("nav.home"), path: "/", icon: Home },
{ name: t("nav.about"), path: "/about" }, { name: t("nav.about"), path: "/about", icon: Info },
{ name: t("nav.products"), path: "/products" }, { name: t("nav.products"), path: "/products", icon: Package },
{ name: t("nav.services"), path: "/services" }, { name: t("nav.services"), path: "/services", icon: Briefcase },
{ name: t("nav.blog"), path: "/blog" }, { name: t("nav.blog"), path: "/blog", icon: BookOpen },
{ name: t("nav.contact"), path: "/contact" }, { name: t("nav.forum") || "Forum", path: "/forum", icon: MessageSquare, protected: true },
{ name: t("nav.demo"), path: import.meta.env.VITE_KURS_URL }, { name: t("nav.contact"), path: "/contact", icon: Phone },
]; ];
return ( return (
@ -48,18 +56,21 @@ const Header: React.FC = () => {
</Link> </Link>
{/* Desktop Navigation */} {/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-8"> <nav className="hidden md:flex items-center space-x-6">
{navLinks.map((link) => ( {navLinks.map((link) => {
<Link if (link.protected && !isAuthenticated) return null;
key={link.path}
to={link.path} return (
className={`font-medium text-sm text-white hover:text-blue-400 transition-colors ${ <Link
link.name === "Giriş" || link.name === "Login" ? "bg-blue-600 rounded px-2 py-1" : "" key={link.path}
}`} to={link.path}
> className="flex items-center space-x-1 font-medium text-sm text-white hover:text-blue-400 transition-colors"
{link.name} >
</Link> {link.icon && <link.icon size={16} />}
))} <span>{link.name}</span>
</Link>
);
})}
<button <button
onClick={toggleLanguage} onClick={toggleLanguage}
@ -68,6 +79,42 @@ const Header: React.FC = () => {
<Globe size={16} /> <Globe size={16} />
<span>{language.toUpperCase()}</span> <span>{language.toUpperCase()}</span>
</button> </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> </nav>
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
@ -85,18 +132,21 @@ const Header: React.FC = () => {
<div className="md:hidden bg-gray-900/95 backdrop-blur-sm shadow-lg"> <div className="md:hidden bg-gray-900/95 backdrop-blur-sm shadow-lg">
<div className="container mx-auto px-4 py-2"> <div className="container mx-auto px-4 py-2">
<nav className="flex flex-col space-y-4 py-4"> <nav className="flex flex-col space-y-4 py-4">
{navLinks.map((link) => ( {navLinks.map((link) => {
<Link if (link.protected && !isAuthenticated) return null;
key={link.path}
to={link.path} return (
className={`font-medium text-white hover:text-blue-400 transition-colors ${ <Link
link.name === "Giriş" || link.name === "Login" ? "bg-blue-600 rounded px-2 py-1" : "" key={link.path}
}`} to={link.path}
onClick={toggleMenu} className="flex items-center space-x-2 font-medium text-white hover:text-blue-400 transition-colors"
> onClick={toggleMenu}
{link.name} >
</Link> {link.icon && <link.icon size={16} />}
))} <span>{link.name}</span>
</Link>
);
})}
<button <button
onClick={toggleLanguage} onClick={toggleLanguage}
@ -105,6 +155,50 @@ const Header: React.FC = () => {
<Globe size={16} /> <Globe size={16} />
<span>{language.toUpperCase()}</span> <span>{language.toUpperCase()}</span>
</button> </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> </nav>
</div> </div>
</div> </div>

View file

@ -17,6 +17,7 @@ const translations = {
"nav.services": "Hizmetler", "nav.services": "Hizmetler",
"nav.demo": "Giriş", "nav.demo": "Giriş",
"nav.blog": "Blog", "nav.blog": "Blog",
"nav.forum": "Forum",
"nav.contact": "İletişim", "nav.contact": "İletişim",
// Hero Section // Hero Section
@ -353,6 +354,11 @@ const translations = {
"Teknoloji ve yazılım dünyasındaki güncel gelişmelerden haberdar olmak için bültenimize abone olun.", "Teknoloji ve yazılım dünyasındaki güncel gelişmelerden haberdar olmak için bültenimize abone olun.",
"blog.backToBlog": "Bloglar", "blog.backToBlog": "Bloglar",
// Forum Page
"forum.title": "Forum",
"forum.subtitle":
"Sorularınızı sorun, deneyimlerinizi paylaşın ve birlikte öğrenin",
// Testimonials Section // Testimonials Section
"testimonials.title": "Müşteri Yorumları", "testimonials.title": "Müşteri Yorumları",
@ -375,10 +381,31 @@ const translations = {
"common.message": "Mesaj", "common.message": "Mesaj",
"common.address": "Adres", "common.address": "Adres",
"common.city": "Şehir", "common.city": "Şehir",
"common.company": "Şirket Adı", "common.company": "Kurum",
"common.fullName": "Adınız Soyadınız", "common.fullName": "Adınız Soyadınız",
"common.branchCount": "Şube Adedi", "common.branchCount": "Şube Adedi",
"common.userCount": "Kullanıcı Adedi", "common.userCount": "Kullanıcı Adedi",
"common.optional": "Opsiyonel",
// Login
"login.welcome": "Hoş Geldiniz",
"login.subtitle": "Hesabınıza giriş yapın veya",
"login.createAccount": "yeni hesap oluşturun",
"login.organization": "Kurum",
"login.organizationPlaceholder": "Kurum",
"login.organizationNotFound": "Kurum bulunamadı",
"login.organizationFound": "kurumu bulundu",
"login.email": "Email Adresi",
"login.emailOrUsername": "E-posta veya Kullanıcı Adı",
"login.emailPlaceholder": "E-posta veya Kullanıcı Adı",
"login.emailRequired": "E-posta veya kullanıcı adı gereklidir",
"login.password": "Parola",
"login.passwordPlaceholder": "Parola",
"login.passwordRequired": "Parola gereklidir",
"login.rememberMe": "Beni hatırla",
"login.forgotPassword": "Parolanızı mı unuttunuz?",
"login.signIn": "Giriş Yap",
"login.signingIn": "Giriş yapılıyor...",
// Footer // Footer
"footer.companyInfo": "footer.companyInfo":
@ -769,6 +796,27 @@ const translations = {
"common.fullName": "Full Name", "common.fullName": "Full Name",
"common.branchCount": "Number of Branches", "common.branchCount": "Number of Branches",
"common.userCount": "Number of Users", "common.userCount": "Number of Users",
"common.optional": "Optional",
// Login
"login.welcome": "Welcome",
"login.subtitle": "Sign in to your account or",
"login.createAccount": "create a new account",
"login.organization": "Organization",
"login.organizationPlaceholder": "Organization name",
"login.organizationNotFound": "Organization not found",
"login.organizationFound": "organization found",
"login.email": "Email Address",
"login.emailOrUsername": "Email or Username",
"login.emailPlaceholder": "Email or Username",
"login.emailRequired": "Email or username is required",
"login.password": "Password",
"login.passwordPlaceholder": "Password",
"login.passwordRequired": "Password is required",
"login.rememberMe": "Remember me",
"login.forgotPassword": "Forgot your password?",
"login.signIn": "Sign In",
"login.signingIn": "Signing in...",
// Footer // Footer
"footer.companyInfo": "footer.companyInfo":

View file

@ -1,96 +1,279 @@
import React from 'react'; import React, { useEffect, useState } from "react";
import { Calendar, Clock, User } from 'lucide-react'; import { Link } from "react-router-dom";
import { useLanguage } from '../context/LanguageContext'; import { Calendar, Clock, User, Tag, Search } from "lucide-react";
import { Link } from 'react-router-dom'; import { useLanguage } from "../context/LanguageContext";
import { blogPostsList, BlogPostContent } from '../locales/blogContent'; // blogPostsList ve BlogPostContent interface'ini import et import {
blogService,
BlogPost,
BlogCategory,
} from "../services/api/blog.service";
import { format } from "date-fns";
import { tr } from "date-fns/locale";
const Blog: React.FC = () => { const Blog = () => {
const { t } = useLanguage(); const { t } = useLanguage();
const [posts, setPosts] = useState<BlogPost[]>([]);
const [categories, setCategories] = useState<BlogCategory[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCategory, setSelectedCategory] = useState<string>("");
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
// Basit slug oluşturma fonksiyonu useEffect(() => {
const createSlug = (title: string) => { loadBlogData();
return title }, [currentPage, selectedCategory]);
.toLowerCase()
.replace(/ /g, '-') const loadBlogData = async () => {
.replace(/[^\w-]+/g, ''); try {
setLoading(true);
const [postsData, categoriesData] = await Promise.all([
blogService.getPosts({
page: currentPage,
pageSize: 9,
categoryId: selectedCategory,
search: searchQuery,
}),
blogService.getCategories(),
]);
setPosts(postsData.items);
setTotalPages(postsData.totalPages);
setCategories(categoriesData);
} catch (error) {
console.error("Blog verileri yüklenemedi:", error);
// Fallback to static data if API fails
setPosts([]);
} finally {
setLoading(false);
}
}; };
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setCurrentPage(1);
loadBlogData();
};
const handleCategoryChange = (categoryId: string) => {
setSelectedCategory(categoryId);
setCurrentPage(1);
};
if (loading && posts.length === 0) {
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">Blog yazıları yükleniyor...</p>
</div>
</div>
);
}
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Hero Section */} {/* Hero Section */}
<div className="relative bg-blue-900 text-white py-24"> <div className="relative bg-blue-900 text-white py-24">
<div className="absolute inset-0 opacity-20" style={{ <div
backgroundImage: 'url("https://images.pexels.com/photos/3183160/pexels-photo-3183160.jpeg?auto=compress&cs=tinysrgb&w=1920")', className="absolute inset-0 opacity-20"
backgroundSize: 'cover', style={{
backgroundPosition: 'center', backgroundImage:
}}></div> 'url("https://images.pexels.com/photos/3183160/pexels-photo-3183160.jpeg?auto=compress&cs=tinysrgb&w=1920")',
backgroundSize: "cover",
backgroundPosition: "center",
}}
></div>
<div className="container mx-auto pt-16 px-4 relative"> <div className="container mx-auto pt-16 px-4 relative">
<h1 className="text-5xl font-bold mb-6">{t('blog.title')}</h1> <h1 className="text-5xl font-bold mb-6">{t("blog.title")}</h1>
<p className="text-xl max-w-3xl"> <p className="text-xl max-w-3xl">{t("blog.subtitle")}</p>
{t('blog.subtitle')} </div>
</p> </div>
{/* Search and Filter Section */}
<div className="bg-white shadow-sm border-b">
<div className="container mx-auto px-4 py-6">
<div className="flex flex-col md:flex-row gap-4">
{/* Search */}
<form onSubmit={handleSearch} className="flex-1">
<div className="relative">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Blog yazılarında ara..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<Search className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
</div>
</form>
{/* Category Filter */}
<div className="flex gap-2 flex-wrap">
<button
onClick={() => handleCategoryChange("")}
className={`px-4 py-2 rounded-lg transition-colors ${
selectedCategory === ""
? "bg-blue-600 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
Tümü
</button>
{categories.map((category) => (
<button
key={category.id}
onClick={() => handleCategoryChange(category.id)}
className={`px-4 py-2 rounded-lg transition-colors ${
selectedCategory === category.id
? "bg-blue-600 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
{category.name} ({category.postCount})
</button>
))}
</div>
</div>
</div> </div>
</div> </div>
{/* Blog Posts Grid */} {/* Blog Posts Grid */}
<div className="container mx-auto px-4 py-16"> <div className="container mx-auto px-4 py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> {!Array.isArray(posts) || posts.length === 0 ? (
{blogPostsList.map((post, index) => ( <div className="text-center py-12">
<Link to={`/blog/${post.slug}`} key={index} className="block"> <p className="text-gray-600 text-lg">
<article className="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-shadow"> Henüz blog yazısı bulunmuyor.
<div className="aspect-w-16 aspect-h-9 relative"> </p>
<img </div>
src={post.image} // Görsel bilgisi doğrudan post objesinden alınıyor ) : (
alt={t(post.title)} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
className="object-cover w-full h-48" {posts.map((post) => (
/> <Link
<div className="absolute top-4 right-4 bg-blue-600 text-white px-3 py-1 rounded-full text-sm"> to={`/blog/${post.slug || post.id}`}
{t(post.category)} key={post.id}
className="block"
>
<article className="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-shadow h-full flex flex-col">
<div className="aspect-w-16 aspect-h-9 relative">
<img
src={
post.coverImage || "https://via.placeholder.com/400x225"
}
alt={post.title}
className="object-cover w-full h-48"
/>
<div className="absolute top-4 right-4 bg-blue-600 text-white px-3 py-1 rounded-full text-sm">
{post.category.name}
</div>
</div> </div>
</div> <div className="p-6 flex-1 flex flex-col">
<div className="p-6"> <h2 className="text-xl font-bold text-gray-900 mb-3 hover:text-blue-600 transition-colors">
<h2 className="text-xl font-bold text-gray-900 mb-3 hover:text-blue-600 transition-colors"> {post.title}
{t(post.title)} </h2>
</h2> <p className="text-gray-600 mb-4 flex-1">{post.summary}</p>
<p className="text-gray-600 mb-4">
{t(post.excerpt)} {/* Tags */}
</p> {post.tags.length > 0 && (
<div className="flex items-center text-sm text-gray-500 space-x-4"> <div className="flex flex-wrap gap-2 mb-4">
{post.tags.slice(0, 3).map((tag, index) => (
<span
key={index}
className="inline-flex items-center text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded"
>
<Tag className="w-3 h-3 mr-1" />
{tag}
</span>
))}
</div>
)}
<div className="flex items-center text-sm text-gray-500 space-x-4">
<div className="flex items-center"> <div className="flex items-center">
<User size={16} className="mr-1" /> <User size={16} className="mr-1" />
{post.author} {/* Yazar bilgisi doğrudan post objesinden alınıyor */} {post.author.name}
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<Calendar size={16} className="mr-1" /> <Calendar size={16} className="mr-1" />
{t(post.date)} {format(
new Date(post.publishedAt || post.createdAt),
"dd MMM yyyy",
{ locale: tr }
)}
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<Clock size={16} className="mr-1" /> <Clock size={16} className="mr-1" />
{post.readTime} {typeof post.content === "string" &&
post.content.length > 0
? Math.ceil(post.content.length / 1000)
: "-"}{" "}
dk
</div> </div>
</div> </div>
</div> </div>
</article> </article>
</Link> </Link>
))} ))}
</div> </div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-12 flex justify-center">
<nav className="flex gap-2">
<button
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Önceki
</button>
{[...Array(totalPages)].map((_, i) => (
<button
key={i + 1}
onClick={() => setCurrentPage(i + 1)}
className={`px-4 py-2 rounded-lg ${
currentPage === i + 1
? "bg-blue-600 text-white"
: "border border-gray-300 hover:bg-gray-50"
}`}
>
{i + 1}
</button>
))}
<button
onClick={() =>
setCurrentPage(Math.min(totalPages, currentPage + 1))
}
disabled={currentPage === totalPages}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Sonraki
</button>
</nav>
</div>
)}
</div> </div>
{/* Newsletter Section */} {/* Newsletter Section */}
<div className="bg-white py-16"> <div className="bg-white py-16">
<div className="container mx-auto px-4 text-center"> <div className="container mx-auto px-4 text-center">
<h2 className="text-3xl font-bold text-gray-900 mb-4">{t('blog.subscribe')}</h2> <h2 className="text-3xl font-bold text-gray-900 mb-4">
{t("blog.subscribe")}
</h2>
<p className="text-gray-600 mb-8 max-w-2xl mx-auto"> <p className="text-gray-600 mb-8 max-w-2xl mx-auto">
{t('blog.subscribe.desc')} {t("blog.subscribe.desc")}
</p> </p>
<div className="max-w-md mx-auto flex gap-4"> <div className="max-w-md mx-auto flex gap-4">
<input <input
type="email" type="email"
placeholder={t('common.email')} placeholder={t("common.email")}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
<button className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors"> <button className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors">
{t('common.subscribe')} {t("common.subscribe")}
</button> </button>
</div> </div>
</div> </div>

View file

@ -210,7 +210,7 @@ const Contact: React.FC = () => {
htmlFor="company" htmlFor="company"
className="block text-sm font-medium text-gray-700 mb-2" className="block text-sm font-medium text-gray-700 mb-2"
> >
{t("common.company") || "Şirket Adı"} {t("common.company")}
</label> </label>
<input <input
type="text" type="text"

284
company/src/pages/Forum.tsx Normal file
View file

@ -0,0 +1,284 @@
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-24">
<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

@ -0,0 +1,278 @@
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;

142
company/src/pages/Login.tsx Normal file
View file

@ -0,0 +1,142 @@
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

@ -0,0 +1,237 @@
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-12 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-8">
<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

@ -0,0 +1,120 @@
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"
/>
) : (
<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

@ -0,0 +1,222 @@
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

@ -0,0 +1,214 @@
import { apiClient } from './config';
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[];
}
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 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');
// Client secret kaldırıldı - public client olarak çalışıyor
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 || [],
};
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);
}
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

@ -0,0 +1,155 @@
import { apiClient } from './config';
export interface BlogPost {
id: string;
title: string;
slug: string;
content?: string;
summary: string;
coverImage?: string;
author: {
id: string;
name: string;
avatar?: string;
};
category: {
id: string;
name: string;
slug: string;
};
tags: string[];
viewCount: number;
likeCount: number;
commentCount: number;
isPublished: boolean;
publishedAt?: string;
createdAt: string;
updatedAt: string;
}
export interface BlogCategory {
id: string;
name: string;
slug: string;
description?: string;
postCount: number;
}
export interface BlogComment {
id: string;
postId: string;
content: string;
author: {
id: string;
name: string;
avatar?: string;
};
parentId?: string;
replies?: BlogComment[];
likeCount: number;
isLiked?: boolean;
createdAt: string;
updatedAt: string;
}
export interface CreateBlogPostRequest {
title: string;
content: string;
summary: string;
categoryId: string;
tags: string[];
coverImage?: string;
isPublished: boolean;
}
export interface CreateCommentRequest {
postId: string;
content: string;
parentId?: string;
}
export interface BlogListParams {
page?: number;
pageSize?: number;
categoryId?: string;
tag?: string;
search?: string;
authorId?: string;
sortBy?: 'latest' | 'popular' | 'trending';
}
export interface PaginatedResponse<T> {
items: T[];
totalCount: number;
pageNumber: number;
pageSize: number;
totalPages: number;
}
class BlogService {
async getPosts(params: BlogListParams = {}): Promise<PaginatedResponse<BlogPost>> {
const response = await apiClient.get<PaginatedResponse<BlogPost>>('/api/app/blog/posts', { params });
return response.data;
}
async getPost(idOrSlug: string): Promise<BlogPost> {
const response = await apiClient.get<BlogPost>(`/api/app/blog/posts/${idOrSlug}`);
return response.data;
}
async createPost(data: CreateBlogPostRequest): Promise<BlogPost> {
const response = await apiClient.post<BlogPost>('/api/app/blog/posts', data);
return response.data;
}
async updatePost(id: string, data: Partial<CreateBlogPostRequest>): Promise<BlogPost> {
const response = await apiClient.put<BlogPost>(`/api/app/blog/posts/${id}`, data);
return response.data;
}
async deletePost(id: string): Promise<void> {
await apiClient.delete(`/api/app/blog/posts/${id}`);
}
async getCategories(): Promise<BlogCategory[]> {
const response = await apiClient.get<BlogCategory[]>('/api/app/blog/categories');
return response.data;
}
async getComments(postId: string): Promise<BlogComment[]> {
const response = await apiClient.get<BlogComment[]>(`/api/app/blog/posts/${postId}/comments`);
return response.data;
}
async createComment(data: CreateCommentRequest): Promise<BlogComment> {
const response = await apiClient.post<BlogComment>('/api/app/blog/comments', data);
return response.data;
}
async deleteComment(id: string): Promise<void> {
await apiClient.delete(`/api/app/blog/comments/${id}`);
}
async likePost(postId: string): Promise<void> {
await apiClient.post(`/api/app/blog/posts/${postId}/like`);
}
async unlikePost(postId: string): Promise<void> {
await apiClient.delete(`/api/app/blog/posts/${postId}/like`);
}
async likeComment(commentId: string): Promise<void> {
await apiClient.post(`/api/app/blog/comments/${commentId}/like`);
}
async unlikeComment(commentId: string): Promise<void> {
await apiClient.delete(`/api/app/blog/comments/${commentId}/like`);
}
async getTags(): Promise<string[]> {
const response = await apiClient.get<string[]>('/api/app/blog/tags');
return response.data;
}
}
export const blogService = new BlogService();

View file

@ -0,0 +1,47 @@
import axios from 'axios';
// API Base URL
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:44328';
// Axios instance
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor - Add auth token and tenant header
apiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// UI uygulaması gibi tenant header'ı ekle
const tenantId = localStorage.getItem('tenant_id');
if (tenantId) {
config.headers['__tenant'] = tenantId;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor - Handle errors
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Token expired or invalid
localStorage.removeItem('access_token');
localStorage.removeItem('current_user');
window.location.href = '/login';
}
return Promise.reject(error);
}
);

View file

@ -0,0 +1,257 @@
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

@ -0,0 +1,102 @@
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;
login: (data: LoginRequest) => Promise<void>;
register: (data: RegisterRequest) => Promise<void>;
logout: () => void;
checkAuth: () => Promise<void>;
refreshToken: () => Promise<void>;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
isAuthenticated: false,
isLoading: false,
tenantId: null,
login: async (data: LoginRequest) => {
set({ isLoading: true });
try {
await authService.login(data);
const user = authService.getUser();
const tenantId = authService.getTenantId();
set({ user, isAuthenticated: true, isLoading: false, tenantId });
toast.success('Giriş başarılı!');
} 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);
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.');
} catch (error: any) {
set({ isLoading: false });
toast.error(error.response?.data?.error?.message || 'Kayıt başarısız!');
throw error;
}
},
logout: () => {
authService.logout();
set({ user: null, isAuthenticated: false, tenantId: null });
toast.success(ıkış yapıldı');
},
checkAuth: async () => {
if (!authService.isAuthenticated()) {
set({ user: null, isAuthenticated: false, tenantId: null });
return;
}
try {
const user = await authService.fetchCurrentUser();
const tenantId = authService.getTenantId();
set({ user, isAuthenticated: !!user, tenantId });
} catch (error) {
authService.logout();
set({ user: null, isAuthenticated: false, tenantId: null });
}
},
refreshToken: async () => {
try {
await authService.refreshToken();
const user = await authService.fetchCurrentUser();
const tenantId = authService.getTenantId();
set({ user, isAuthenticated: !!user, tenantId });
} catch (error) {
authService.logout();
set({ user: null, isAuthenticated: false, tenantId: null });
throw error;
}
},
}),
{
name: 'auth-storage',
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
tenantId: state.tenantId
}),
}
)
);

View file

@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.ve4ie4jj2c8" "revision": "0.n85sh48g8go"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

369
ui/package-lock.json generated
View file

@ -18,6 +18,7 @@
"@tanstack/react-table": "^8.8.5", "@tanstack/react-table": "^8.8.5",
"axios": "^1.7.9", "axios": "^1.7.9",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"devextreme": "^23.2.11", "devextreme": "^23.2.11",
"devextreme-react": "^23.2.11", "devextreme-react": "^23.2.11",
@ -43,6 +44,7 @@
"react-i18next": "^15.2.0", "react-i18next": "^15.2.0",
"react-icons": "^5.4.0", "react-icons": "^5.4.0",
"react-modal": "^3.16.3", "react-modal": "^3.16.3",
"react-quill": "^2.0.0",
"react-router-dom": "^6.14.1", "react-router-dom": "^6.14.1",
"react-select": "^5.9.0", "react-select": "^5.9.0",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
@ -3341,6 +3343,21 @@
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true "dev": true
}, },
"node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
}
},
"node_modules/@types/quill/node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/@types/raf": { "node_modules/@types/raf": {
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
@ -4433,18 +4450,36 @@
} }
}, },
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.2", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true, "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "call-bind-apply-helpers": "^1.0.0",
"get-intrinsic": "^1.0.2" "es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -4602,6 +4637,15 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -5035,6 +5079,16 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true "dev": true
}, },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.13", "version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@ -5100,6 +5154,23 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-lazy-prop": { "node_modules/define-lazy-prop": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
@ -5111,11 +5182,12 @@
} }
}, },
"node_modules/define-properties": { "node_modules/define-properties": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true, "license": "MIT",
"dependencies": { "dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0", "has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1" "object-keys": "^1.1.1"
}, },
@ -5370,6 +5442,20 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/duplexer2": { "node_modules/duplexer2": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@ -5537,6 +5623,24 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-get-iterator": { "node_modules/es-get-iterator": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
@ -5564,6 +5668,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": { "node_modules/es-set-tostringtag": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
@ -6353,6 +6469,12 @@
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
"integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="
}, },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-csv": { "node_modules/fast-csv": {
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz",
@ -6758,7 +6880,6 @@
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"dev": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -6791,15 +6912,24 @@
} }
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.1", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true, "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "call-bind-apply-helpers": "^1.0.2",
"has": "^1.0.3", "es-define-property": "^1.0.1",
"has-proto": "^1.0.1", "es-errors": "^1.3.0",
"has-symbols": "^1.0.3" "es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -6811,6 +6941,19 @@
"integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
"dev": true "dev": true
}, },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stdin": { "node_modules/get-stdin": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz",
@ -6925,12 +7068,12 @@
} }
}, },
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true, "license": "MIT",
"dependencies": { "engines": {
"get-intrinsic": "^1.1.3" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -6977,12 +7120,12 @@
} }
}, },
"node_modules/has-property-descriptors": { "node_modules/has-property-descriptors": {
"version": "1.0.0", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true, "license": "MIT",
"dependencies": { "dependencies": {
"get-intrinsic": "^1.1.1" "es-define-property": "^1.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -7001,10 +7144,10 @@
} }
}, },
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.0.3", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true, "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -7016,7 +7159,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"dev": true,
"dependencies": { "dependencies": {
"has-symbols": "^1.0.2" "has-symbols": "^1.0.2"
}, },
@ -7303,7 +7445,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"dev": true,
"dependencies": { "dependencies": {
"call-bind": "^1.0.2", "call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0" "has-tostringtag": "^1.0.0"
@ -7404,7 +7545,6 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
"dev": true,
"dependencies": { "dependencies": {
"has-tostringtag": "^1.0.0" "has-tostringtag": "^1.0.0"
}, },
@ -7531,7 +7671,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
"dev": true,
"dependencies": { "dependencies": {
"call-bind": "^1.0.2", "call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0" "has-tostringtag": "^1.0.0"
@ -8150,6 +8289,15 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mdn-data": { "node_modules/mdn-data": {
"version": "2.0.30", "version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
@ -8382,7 +8530,6 @@
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
"dev": true,
"dependencies": { "dependencies": {
"call-bind": "^1.0.2", "call-bind": "^1.0.2",
"define-properties": "^1.1.3" "define-properties": "^1.1.3"
@ -8398,7 +8545,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@ -9737,6 +9883,20 @@
} }
] ]
}, },
"node_modules/quill": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"license": "BSD-3-Clause",
"dependencies": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"node_modules/quill-delta": { "node_modules/quill-delta": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
@ -9751,6 +9911,58 @@
"node": ">= 12.0.0" "node": ">= 12.0.0"
} }
}, },
"node_modules/quill/node_modules/deep-equal": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
"license": "MIT",
"dependencies": {
"is-arguments": "^1.1.1",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.5.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/quill/node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
"license": "MIT"
},
"node_modules/quill/node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
"license": "Apache-2.0"
},
"node_modules/quill/node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/quill/node_modules/quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"license": "MIT",
"dependencies": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/raf": { "node_modules/raf": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
@ -10002,6 +10214,21 @@
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19"
} }
}, },
"node_modules/react-quill": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
"integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
"license": "MIT",
"dependencies": {
"@types/quill": "^1.3.10",
"lodash": "^4.17.4",
"quill": "^1.3.7"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.14.2", "version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@ -10226,14 +10453,17 @@
} }
}, },
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.0", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
"integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
"dev": true, "license": "MIT",
"dependencies": { "dependencies": {
"call-bind": "^1.0.2", "call-bind": "^1.0.8",
"define-properties": "^1.2.0", "define-properties": "^1.2.1",
"functions-have-names": "^1.2.3" "es-errors": "^1.3.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"set-function-name": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -11276,6 +11506,38 @@
"randombytes": "^2.1.0" "randombytes": "^2.1.0"
} }
}, },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/set-function-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"functions-have-names": "^1.2.3",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setimmediate": { "node_modules/setimmediate": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@ -12506,19 +12768,6 @@
"base64-arraybuffer": "^1.0.2" "base64-arraybuffer": "^1.0.2"
} }
}, },
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.15", "version": "5.4.15",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",

View file

@ -25,6 +25,7 @@
"@tanstack/react-table": "^8.8.5", "@tanstack/react-table": "^8.8.5",
"axios": "^1.7.9", "axios": "^1.7.9",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"devextreme": "^23.2.11", "devextreme": "^23.2.11",
"devextreme-react": "^23.2.11", "devextreme-react": "^23.2.11",
@ -50,6 +51,7 @@
"react-i18next": "^15.2.0", "react-i18next": "^15.2.0",
"react-icons": "^5.4.0", "react-icons": "^5.4.0",
"react-modal": "^3.16.3", "react-modal": "^3.16.3",
"react-quill": "^2.0.0",
"react-router-dom": "^6.14.1", "react-router-dom": "^6.14.1",
"react-select": "^5.9.0", "react-select": "^5.9.0",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",

View file

@ -57,6 +57,18 @@ const adminRoutes: Routes = [
component: lazy(() => import('@/views/admin/chart/ChartEdit')), component: lazy(() => import('@/views/admin/chart/ChartEdit')),
authority: [], authority: [],
}, },
{
key: ROUTES_ENUM.admin.blog.management,
path: ROUTES_ENUM.admin.blog.management,
component: lazy(() => import('@/views/blog/BlogManagement')),
authority: [],
},
{
key: ROUTES_ENUM.admin.forum.management,
path: ROUTES_ENUM.admin.forum.management,
component: lazy(() => import('@/views/forum/ForumManagement')),
authority: [],
},
] ]
export { adminRoutes } export { adminRoutes }

View file

@ -35,6 +35,12 @@ export const ROUTES_ENUM = {
edit: '/admin/listform/edit/:listFormCode', edit: '/admin/listform/edit/:listFormCode',
}, },
chart: '/admin/chart/edit/:chartCode', chart: '/admin/chart/edit/:chartCode',
blog: {
management: '/admin/blog/management',
},
forum: {
management: '/admin/forum/management',
},
}, },
settings: '/settings', settings: '/settings',
list: '/list/:listFormCode', list: '/list/:listFormCode',

View file

@ -0,0 +1,260 @@
import apiService from "@/services/api.service";
export interface BlogPost {
id: string;
title: string;
slug: string;
content?: string;
summary: string;
coverImage?: string;
author: {
id: string;
name: string;
avatar?: string;
};
category: {
id: string;
name: string;
slug: string;
};
tags: string[];
viewCount: number;
likeCount: number;
commentCount: number;
isPublished: boolean;
publishedAt?: string;
createdAt: string;
updatedAt: string;
}
export interface BlogCategory {
id: string;
name: string;
slug: string;
description?: string;
postCount: number;
}
export interface BlogComment {
id: string;
postId: string;
content: string;
author: {
id: string;
name: string;
avatar?: string;
};
parentId?: string;
replies?: BlogComment[];
likeCount: number;
isLiked?: boolean;
createdAt: string;
updatedAt: string;
}
export interface CreateUpdateBlogPostDto {
title: string
content: string
summary: string
categoryId: string
tags: string[]
coverImage?: string
isPublished: boolean
}
export interface CreateUpdateBlogCategoryDto {
name: string
slug: string
description: string
icon?: string
displayOrder: number
isActive: boolean
}
export interface CreateCommentDto {
postId: string;
content: string;
parentId?: string;
}
export interface BlogListParams {
page?: number;
pageSize?: number;
categoryId?: string;
tag?: string;
search?: string;
authorId?: string;
sortBy?: 'latest' | 'popular' | 'trending';
}
export interface PaginatedResponse<T> {
items: T[];
totalCount: number;
pageNumber: number;
pageSize: number;
totalPages: number;
}
class BlogService {
async getPosts(params: BlogListParams = {}): Promise<PaginatedResponse<BlogPost>> {
const response = await apiService.fetchData<PaginatedResponse<BlogPost>>({
url: '/api/app/blog/posts',
method: 'GET',
params
});
return response.data;
}
async getPost(idOrSlug: string): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({
url: `/api/app/blog/posts/${idOrSlug}`,
method: 'GET'
});
return response.data;
}
async createPost(data: CreateUpdateBlogPostDto): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({
url: '/api/app/blog/posts',
method: 'POST',
data: data as any
});
return response.data;
}
async updatePost(id: string, data: CreateUpdateBlogPostDto): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({
url: `/api/app/blog/posts/${id}`,
method: 'PUT',
data: data as any
});
return response.data;
}
async deletePost(id: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/blog/posts/${id}`,
method: 'DELETE'
});
}
async publishPost(id: string): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({
url: `/api/app/blog/posts/${id}/publish`,
method: 'POST'
});
return response.data;
}
async unpublishPost(id: string): Promise<BlogPost> {
const response = await apiService.fetchData<BlogPost>({
url: `/api/app/blog/posts/${id}/unpublish`,
method: 'POST'
});
return response.data;
}
async getCategories(): Promise<BlogCategory[]> {
const response = await apiService.fetchData<BlogCategory[]>({
url: '/api/app/blog/categories',
method: 'GET'
});
return response.data;
}
async getComments(postId: string): Promise<BlogComment[]> {
const response = await apiService.fetchData<BlogComment[]>({
url: `/api/app/blog/posts/${postId}/comments`,
method: 'GET'
});
return response.data;
}
async createComment(data: CreateCommentDto): Promise<BlogComment> {
const response = await apiService.fetchData<BlogComment>({
url: '/api/app/blog/comments',
method: 'POST',
data: data as any
});
return response.data;
}
async deleteComment(id: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/blog/comments/${id}`,
method: 'DELETE'
});
}
async likePost(postId: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/blog/posts/${postId}/like`,
method: 'POST'
});
}
async unlikePost(postId: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/blog/posts/${postId}/like`,
method: 'DELETE'
});
}
async likeComment(commentId: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/blog/comments/${commentId}/like`,
method: 'POST'
});
}
async unlikeComment(commentId: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/blog/comments/${commentId}/like`,
method: 'DELETE'
});
}
async getTags(): Promise<string[]> {
const response = await apiService.fetchData<string[]>({
url: '/api/app/blog/tags',
method: 'GET'
});
return response.data;
}
// Category methods
async getCategory(id: string): Promise<BlogCategory> {
const response = await apiService.fetchData<BlogCategory>({
url: `/api/app/blog/categories/${id}`,
method: 'GET'
});
return response.data;
}
async createCategory(data: CreateUpdateBlogCategoryDto): Promise<BlogCategory> {
const response = await apiService.fetchData<BlogCategory>({
url: '/api/app/blog/categories',
method: 'POST',
data: data as any
});
return response.data;
}
async updateCategory(id: string, data: CreateUpdateBlogCategoryDto): Promise<BlogCategory> {
const response = await apiService.fetchData<BlogCategory>({
url: `/api/app/blog/categories/${id}`,
method: 'PUT',
data: data as any
});
return response.data;
}
async deleteCategory(id: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/blog/categories/${id}`,
method: 'DELETE'
});
}
}
export const blogService = new BlogService();

View file

@ -0,0 +1,291 @@
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

@ -0,0 +1,673 @@
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 Select from '@/components/ui/Select'
import Switcher from '@/components/ui/Switcher'
import { HiPlus, HiPencil, HiTrash, HiEye } from 'react-icons/hi'
import { useNavigate } from 'react-router-dom'
import {
blogService,
BlogPost,
BlogCategory,
CreateUpdateBlogPostDto,
CreateUpdateBlogCategoryDto,
} from '@/services/blog.service'
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 ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import Tr from '@/components/ui/Table/Tr'
import Th from '@/components/ui/Table/Th'
import THead from '@/components/ui/Table/THead'
import TBody from '@/components/ui/Table/TBody'
import Td from '@/components/ui/Table/Td'
const validationSchema = Yup.object().shape({
title: Yup.string().required('Başlık gereklidir'),
summary: Yup.string().required('Özet gereklidir'),
categoryId: Yup.string().required('Kategori seçiniz'),
content: Yup.string().required('İçerik gereklidir'),
})
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 BlogManagement = () => {
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<'posts' | 'categories'>('posts')
const [posts, setPosts] = useState<BlogPost[]>([])
const [categories, setCategories] = useState<BlogCategory[]>([])
const [loading, setLoading] = useState(false)
const [modalVisible, setModalVisible] = useState(false)
const [categoryModalVisible, setCategoryModalVisible] = useState(false)
const [editingPost, setEditingPost] = useState<BlogPost | null>(null)
const [editingCategory, setEditingCategory] = useState<BlogCategory | null>(null)
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
setLoading(true)
try {
const [postsData, categoriesData] = await Promise.all([
blogService.getPosts({ pageSize: 100 }),
blogService.getCategories(),
])
setPosts(postsData.items)
setCategories(categoriesData)
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
Veriler yüklenirken hata oluştu
</Notification>,
)
} finally {
setLoading(false)
}
}
const handleCreate = () => {
setEditingPost(null)
setModalVisible(true)
}
const handleEdit = (post: BlogPost) => {
setEditingPost(post)
setModalVisible(true)
}
const handleDelete = async (id: string) => {
try {
await blogService.deletePost(id)
toast.push(
<Notification title="Başarılı" type="success">
Blog yazısı silindi
</Notification>,
)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
Silme işlemi başarısız
</Notification>,
)
}
}
const handleSubmit = async (values: any, { setSubmitting }: any) => {
try {
const data: CreateUpdateBlogPostDto = {
title: values.title,
content: values.content,
summary: values.summary,
categoryId: values.categoryId,
tags: values.tags ? values.tags.split(',').map((t: string) => t.trim()) : [],
coverImage: values.coverImage,
isPublished: values.isPublished,
}
if (editingPost) {
await blogService.updatePost(editingPost.id, data)
toast.push(
<Notification title="Başarılı" type="success">
Blog yazısı güncellendi
</Notification>,
)
} else {
await blogService.createPost(data)
toast.push(
<Notification title="Başarılı" type="success">
Blog yazısı oluşturuldu
</Notification>,
)
}
setModalVisible(false)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
)
} finally {
setSubmitting(false)
}
}
const handlePublish = async (post: BlogPost) => {
try {
if (post.isPublished) {
await blogService.unpublishPost(post.id)
toast.push(
<Notification title="Başarılı" type="success">
Yayından kaldırıldı
</Notification>,
)
} else {
await blogService.publishPost(post.id)
toast.push(
<Notification title="Başarılı" type="success">
Yayınlandı
</Notification>,
)
}
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
)
}
}
// Category functions
const handleCreateCategory = () => {
setEditingCategory(null)
setCategoryModalVisible(true)
}
const handleEditCategory = (category: BlogCategory) => {
setEditingCategory(category)
setCategoryModalVisible(true)
}
const handleDeleteCategory = async (id: string) => {
try {
await blogService.deleteCategory(id)
toast.push(
<Notification title="Başarılı" type="success">
Kategori silindi
</Notification>,
)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
Silme işlemi başarısız
</Notification>,
)
}
}
const handleSubmitCategory = async (values: any, { setSubmitting }: any) => {
try {
const data: CreateUpdateBlogCategoryDto = {
name: values.name,
slug: values.slug,
description: values.description,
icon: values.icon,
displayOrder: values.displayOrder,
isActive: values.isActive
}
if (editingCategory) {
await blogService.updateCategory(editingCategory.id, data)
toast.push(
<Notification title="Başarılı" type="success">
Kategori güncellendi
</Notification>,
)
} else {
await blogService.createCategory(data)
toast.push(
<Notification title="Başarılı" type="success">
Kategori oluşturuldu
</Notification>,
)
}
setCategoryModalVisible(false)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
)
} finally {
setSubmitting(false)
}
}
const initialValues = editingPost
? {
title: editingPost.title,
summary: editingPost.summary,
content: editingPost.content,
categoryId: editingPost.category.id,
tags: editingPost.tags.join(', '),
coverImage: editingPost.coverImage || '',
isPublished: editingPost.isPublished,
}
: {
title: '',
summary: '',
content: '',
categoryId: '',
tags: '',
coverImage: '',
isPublished: false,
}
const initialCategoryValues = editingCategory
? {
name: editingCategory.name,
slug: editingCategory.slug,
description: editingCategory.description || '',
icon: '',
displayOrder: 0,
isActive: true
}
: {
name: '',
slug: '',
description: '',
icon: '',
displayOrder: 0,
isActive: true
}
return (
<>
<div className="flex justify-between items-center mb-4">
<h3>Blog Yönetimi</h3>
{activeTab === 'posts' ? (
<Button variant="solid" size='xs' icon={<HiPlus />} onClick={handleCreate}>
Yeni Blog Yazısı
</Button>
) : (
<Button variant="solid" size='xs' icon={<HiPlus />} onClick={handleCreateCategory}>
Yeni Kategori
</Button>
)}
</div>
<Card>
<div className="mb-4">
<div className="flex gap-4 border-b">
<button
className={`pb-2 px-1 ${activeTab === 'posts' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('posts')}
>
Blog Yazıları
</button>
<button
className={`pb-2 px-1 ${activeTab === 'categories' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('categories')}
>
Kategoriler
</button>
</div>
</div>
{activeTab === 'posts' ? (
<Table>
<THead>
<Tr>
<Th>Başlık</Th>
<Th>Kategori</Th>
<Th>Yazar</Th>
<Th>Durum</Th>
<Th>Gör. / Beğ. / Yorum</Th>
<Th>Yayın Tarihi</Th>
<Th>İşlemler</Th>
</Tr>
</THead>
<TBody>
{loading ? (
<Tr>
<Td colSpan={7}>Yükleniyor...</Td>
</Tr>
) : (
posts.map((post) => (
<Tr key={post.id}>
<Td>
<a
className="text-blue-600 hover:underline cursor-pointer"
onClick={() => navigate(`/blog/${post.slug || post.id}`)}
>
{post.title}
</a>
</Td>
<Td>{post.category?.name}</Td>
<Td>{post.author?.name}</Td>
<Td>
<Tag
className={
post.isPublished
? 'bg-green-100 text-green-800'
: 'bg-orange-100 text-orange-800'
}
>
{post.isPublished ? 'Yayında' : 'Taslak'}
</Tag>
</Td>
<Td>
{post.viewCount} / {post.likeCount} / {post.commentCount}
</Td>
<Td>
{post.publishedAt
? format(new Date(post.publishedAt), 'dd MMM yyyy', { locale: tr })
: '-'}
</Td>
<Td>
<div className="flex gap-2">
<Button
size="sm"
icon={<HiEye />}
onClick={() => navigate(`/blog/${post.slug || post.id}`)}
/>
<Button size="sm" icon={<HiPencil />} onClick={() => handleEdit(post)} />
<Switcher checked={post.isPublished} onChange={() => handlePublish(post)} />
<Button
size="sm"
variant="solid"
color="red-600"
icon={<HiTrash />}
onClick={() => handleDelete(post.id)}
/>
</div>
</Td>
</Tr>
))
)}
</TBody>
</Table>
) : (
<Table>
<THead>
<Tr>
<Th>İsim</Th>
<Th>Slug</Th>
<Th>ıklama</Th>
<Th>Yazı Sayısı</Th>
<Th>İşlemler</Th>
</Tr>
</THead>
<TBody>
{loading ? (
<Tr>
<Td colSpan={5}>Yükleniyor...</Td>
</Tr>
) : (
categories.map((category) => (
<Tr key={category.id}>
<Td className="font-medium">{category.name}</Td>
<Td>{category.slug}</Td>
<Td>{category.description}</Td>
<Td>{category.postCount}</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>
)}
</Card>
{/* Post Modal */}
<Dialog
isOpen={modalVisible}
onClose={() => setModalVisible(false)}
onRequestClose={() => setModalVisible(false)}
width={1000}
>
<h5 className="mb-4">{editingPost ? 'Blog Yazısını Düzenle' : 'Yeni Blog Yazısı'}</h5>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
enableReinitialize
>
{({ values, touched, errors, isSubmitting, setFieldValue }) => (
<Form>
<FormContainer>
<FormItem
label="Başlık"
invalid={errors.title && touched.title}
errorMessage={errors.title}
>
<Field type="text" name="title" placeholder="Blog başlığı" component={Input} />
</FormItem>
<FormItem
label="Özet"
invalid={errors.summary && touched.summary}
errorMessage={errors.summary}
>
<Field
name="summary"
placeholder="Kısa açıklama"
component={Input}
textArea={true}
rows={3}
/>
</FormItem>
<FormItem
label="Kategori"
invalid={errors.categoryId && touched.categoryId}
errorMessage={errors.categoryId}
>
<Field name="categoryId">
{({ field, form }: any) => (
<Select
field={field}
form={form}
options={categories.map((cat) => ({
value: cat.id,
label: cat.name,
}))}
placeholder="Kategori seçiniz"
/>
)}
</Field>
</FormItem>
<FormItem label="Etiketler" extra="Virgül ile ayırarak yazınız">
<Field
type="text"
name="tags"
placeholder="örn: react, javascript, web"
component={Input}
/>
</FormItem>
<FormItem label="Kapak Görseli">
<Field
type="text"
name="coverImage"
placeholder="Görsel URL'si"
component={Input}
/>
</FormItem>
<FormItem
label="İçerik"
invalid={errors.content && touched.content}
errorMessage={errors.content}
>
<ReactQuill
theme="snow"
value={values.content}
onChange={(val: string) => setFieldValue('content', val)}
style={{ height: '300px', marginBottom: '50px' }}
/>
</FormItem>
<FormItem>
<Field name="isPublished">
{({ field, form }: any) => (
<Switcher
{...field}
onChange={(checked) => form.setFieldValue(field.name, checked)}
checkedContent="Yayınla"
unCheckedContent="Taslak"
/>
)}
</Field>
</FormItem>
<FormItem>
<div className="flex gap-2">
<Button variant="solid" type="submit" loading={isSubmitting}>
{editingPost ? 'Güncelle' : 'Oluştur'}
</Button>
<Button variant="plain" onClick={() => setModalVisible(false)}>
İptal
</Button>
</div>
</FormItem>
</FormContainer>
</Form>
)}
</Formik>
</Dialog>
{/* Category Modal */}
<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>
<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 BlogManagement

View file

@ -0,0 +1,493 @@
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'
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 navigate = useNavigate()
const [activeTab, setActiveTab] = useState<'categories' | 'topics'>('categories')
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>,
)
} 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>,
)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
Silme işlemi başarısız
</Notification>,
)
}
}
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>,
)
} else {
await forumService.createCategory(data)
toast.push(
<Notification title="Başarılı" type="success">
Kategori oluşturuldu
</Notification>,
)
}
setCategoryModalVisible(false)
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
)
} 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>,
)
} else {
await forumService.lockTopic(topic.id)
toast.push(
<Notification title="Başarılı" type="success">
Konu kilitlendi
</Notification>,
)
}
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
)
}
}
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>,
)
} else {
await forumService.pinTopic(topic.id)
toast.push(
<Notification title="Başarılı" type="success">
Konu sabitlendi
</Notification>,
)
}
loadData()
} catch (error) {
toast.push(
<Notification title="Hata" type="danger">
İşlem başarısız
</Notification>,
)
}
}
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 (
<>
<div className="flex justify-between items-center mb-4">
<h3>Forum Yönetimi</h3>
{activeTab === 'categories' && (
<Button variant="solid" size='xs' icon={<HiPlus />} onClick={handleCreateCategory}>
Yeni Kategori
</Button>
)}
</div>
<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')}
>
Konular
</button>
<button
className={`pb-2 px-1 ${activeTab === 'categories' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('categories')}
>
Kategoriler
</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>İşlemler</Th>
</Tr>
</THead>
<TBody>
{loading ? (
<Tr>
<Td colSpan={8}>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}>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