Forum ve Blog Componentleri
This commit is contained in:
parent
eb23174f7a
commit
2baff7538f
59 changed files with 15840 additions and 322 deletions
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
600
api/src/Kurs.Platform.Application/Blog/BlogAppService.cs
Normal file
600
api/src/Kurs.Platform.Application/Blog/BlogAppService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
648
api/src/Kurs.Platform.Application/Forum/ForumAppService.cs
Normal file
648
api/src/Kurs.Platform.Application/Forum/ForumAppService.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
56
api/src/Kurs.Platform.Domain/Blog/BlogCategory.cs
Normal file
56
api/src/Kurs.Platform.Domain/Blog/BlogCategory.cs
Normal 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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
api/src/Kurs.Platform.Domain/Blog/BlogComment.cs
Normal file
57
api/src/Kurs.Platform.Domain/Blog/BlogComment.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
api/src/Kurs.Platform.Domain/Blog/BlogCommentLike.cs
Normal file
31
api/src/Kurs.Platform.Domain/Blog/BlogCommentLike.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
api/src/Kurs.Platform.Domain/Blog/BlogPost.cs
Normal file
96
api/src/Kurs.Platform.Domain/Blog/BlogPost.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
api/src/Kurs.Platform.Domain/Blog/BlogPostLike.cs
Normal file
31
api/src/Kurs.Platform.Domain/Blog/BlogPostLike.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
api/src/Kurs.Platform.Domain/Blog/BlogPostTag.cs
Normal file
31
api/src/Kurs.Platform.Domain/Blog/BlogPostTag.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
86
api/src/Kurs.Platform.Domain/Forum/ForumCategory.cs
Normal file
86
api/src/Kurs.Platform.Domain/Forum/ForumCategory.cs
Normal 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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
api/src/Kurs.Platform.Domain/Forum/ForumPost.cs
Normal file
62
api/src/Kurs.Platform.Domain/Forum/ForumPost.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
api/src/Kurs.Platform.Domain/Forum/ForumPostLike.cs
Normal file
31
api/src/Kurs.Platform.Domain/Forum/ForumPostLike.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
api/src/Kurs.Platform.Domain/Forum/ForumTopic.cs
Normal file
117
api/src/Kurs.Platform.Domain/Forum/ForumTopic.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
api/src/Kurs.Platform.Domain/Forum/ForumTopicLike.cs
Normal file
31
api/src/Kurs.Platform.Domain/Forum/ForumTopicLike.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
api/src/Kurs.Platform.Domain/Forum/ForumTopicTag.cs
Normal file
31
api/src/Kurs.Platform.Domain/Forum/ForumTopicTag.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5449
api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250619131606_AddBlogForumEntities.Designer.cs
generated
Normal file
5449
api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20250619131606_AddBlogForumEntities.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
159
company/README_API_INTEGRATION.md
Normal file
159
company/README_API_INTEGRATION.md
Normal 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
1422
company/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
284
company/src/pages/Forum.tsx
Normal 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 Aç
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSearchModal(true)}
|
||||||
|
className="bg-white text-gray-700 px-4 py-2 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors flex items-center"
|
||||||
|
>
|
||||||
|
<Search className="w-4 h-4 mr-2" />
|
||||||
|
Forum'da Ara
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
navigate('/login');
|
||||||
|
} else {
|
||||||
|
navigate('/profile?tab=topics');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-white text-gray-700 px-4 py-2 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors flex items-center"
|
||||||
|
>
|
||||||
|
<User className="w-4 h-4 mr-2" />
|
||||||
|
Konularım
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Popular Tags */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Popüler Etiketler</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{['react', 'javascript', 'api', 'authentication', 'devexpress', 'abp-framework', 'ddd'].map((tag) => (
|
||||||
|
<Link
|
||||||
|
key={tag}
|
||||||
|
to={`/forum/tag/${tag}`}
|
||||||
|
className="bg-gray-200 text-gray-700 px-3 py-1 rounded-full text-sm hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Modal */}
|
||||||
|
{showSearchModal && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-white rounded-lg max-w-2xl w-full">
|
||||||
|
<div className="p-6 border-b">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">Forum'da Ara</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter' && searchQuery.trim()) {
|
||||||
|
navigate(`/forum/search?q=${encodeURIComponent(searchQuery)}`);
|
||||||
|
setShowSearchModal(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Arama yapmak için bir şeyler yazın..."
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
navigate(`/forum/search?q=${encodeURIComponent(searchQuery)}`);
|
||||||
|
setShowSearchModal(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<Search className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-sm text-gray-600">
|
||||||
|
<p>İpucu: Başlık, içerik veya etiketlerde arama yapabilirsiniz.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 border-t flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowSearchModal(false);
|
||||||
|
setSearchQuery('');
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Forum;
|
||||||
278
company/src/pages/ForumCategory.tsx
Normal file
278
company/src/pages/ForumCategory.tsx
Normal 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
142
company/src/pages/Login.tsx
Normal 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;
|
||||||
237
company/src/pages/LoginWithTenant.tsx
Normal file
237
company/src/pages/LoginWithTenant.tsx
Normal 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;
|
||||||
120
company/src/pages/Profile.tsx
Normal file
120
company/src/pages/Profile.tsx
Normal 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;
|
||||||
222
company/src/pages/Register.tsx
Normal file
222
company/src/pages/Register.tsx
Normal 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;
|
||||||
214
company/src/services/api/auth.service.ts
Normal file
214
company/src/services/api/auth.service.ts
Normal 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();
|
||||||
155
company/src/services/api/blog.service.ts
Normal file
155
company/src/services/api/blog.service.ts
Normal 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();
|
||||||
47
company/src/services/api/config.ts
Normal file
47
company/src/services/api/config.ts
Normal 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);
|
||||||
|
}
|
||||||
|
);
|
||||||
257
company/src/services/api/forum.service.ts
Normal file
257
company/src/services/api/forum.service.ts
Normal 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';
|
||||||
102
company/src/store/authStore.ts
Normal file
102
company/src/store/authStore.ts
Normal 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
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -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
369
ui/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
260
ui/src/services/blog.service.ts
Normal file
260
ui/src/services/blog.service.ts
Normal 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();
|
||||||
291
ui/src/services/forum.service.ts
Normal file
291
ui/src/services/forum.service.ts
Normal 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();
|
||||||
673
ui/src/views/blog/BlogManagement.tsx
Normal file
673
ui/src/views/blog/BlogManagement.tsx
Normal 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>Açı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
|
||||||
493
ui/src/views/forum/ForumManagement.tsx
Normal file
493
ui/src/views/forum/ForumManagement.tsx
Normal 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 açı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>Açı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
|
||||||
Loading…
Reference in a new issue