forum management ve forum view
This commit is contained in:
parent
4d127032af
commit
b0311425ce
38 changed files with 1408 additions and 1233 deletions
|
|
@ -5,6 +5,7 @@ namespace Kurs.Platform.Blog
|
|||
{
|
||||
public class BlogCategoryDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
|
@ -16,6 +17,7 @@ namespace Kurs.Platform.Blog
|
|||
|
||||
public class CreateUpdateBlogCategoryDto
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace Kurs.Platform.Blog
|
|||
{
|
||||
public class BlogPostDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string ContentTr { get; set; }
|
||||
|
|
@ -30,7 +31,7 @@ namespace Kurs.Platform.Blog
|
|||
|
||||
public BlogPostDto()
|
||||
{
|
||||
Tags = new List<string>();
|
||||
Tags = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ namespace Kurs.Platform.Blog
|
|||
|
||||
public class CreateUpdateBlogPostDto
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string ContentTr { get; set; }
|
||||
|
|
@ -56,12 +58,13 @@ namespace Kurs.Platform.Blog
|
|||
|
||||
public CreateUpdateBlogPostDto()
|
||||
{
|
||||
Tags = new List<string>();
|
||||
Tags = [];
|
||||
}
|
||||
}
|
||||
|
||||
public class BlogPostListDto : EntityDto<Guid>
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string Summary { get; set; }
|
||||
|
|
@ -85,7 +88,7 @@ namespace Kurs.Platform.Blog
|
|||
|
||||
public BlogPostListDto()
|
||||
{
|
||||
Tags = new List<string>();
|
||||
Tags = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,9 @@ public class ForumSearchResultDto
|
|||
}
|
||||
|
||||
// Category DTOs
|
||||
public class ForumCategoryDto : EntityDto<Guid>
|
||||
public class ForumCategoryDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
|
@ -45,6 +46,8 @@ public class ForumCategoryDto : EntityDto<Guid>
|
|||
|
||||
public class CreateForumCategoryDto
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Name { get; set; }
|
||||
|
|
@ -82,8 +85,9 @@ public class GetCategoriesInput : PagedAndSortedResultRequestDto
|
|||
}
|
||||
|
||||
// Topic DTOs
|
||||
public class ForumTopicDto : EntityDto<Guid>
|
||||
public class ForumTopicDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public Guid CategoryId { get; set; }
|
||||
|
|
@ -103,6 +107,8 @@ public class ForumTopicDto : EntityDto<Guid>
|
|||
|
||||
public class CreateForumTopicDto
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string Title { get; set; }
|
||||
|
|
@ -119,6 +125,8 @@ public class CreateForumTopicDto
|
|||
|
||||
public class UpdateForumTopicDto
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string Title { get; set; }
|
||||
|
|
@ -140,8 +148,9 @@ public class GetTopicsInput : PagedAndSortedResultRequestDto
|
|||
}
|
||||
|
||||
// Post DTOs
|
||||
public class ForumPostDto : EntityDto<Guid>
|
||||
public class ForumPostDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public Guid TopicId { get; set; }
|
||||
public string Content { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
|
|
@ -154,6 +163,8 @@ public class ForumPostDto : EntityDto<Guid>
|
|||
|
||||
public class CreateForumPostDto
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
[Required]
|
||||
public Guid TopicId { get; set; }
|
||||
|
||||
|
|
@ -165,6 +176,8 @@ public class CreateForumPostDto
|
|||
|
||||
public class UpdateForumPostDto
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Content { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Kurs.Platform.Entities;
|
||||
using Kurs.Platform.Localization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
|
@ -149,7 +150,7 @@ namespace Kurs.Platform.Blog
|
|||
{
|
||||
var post = await _postRepository.GetAsync(id);
|
||||
|
||||
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Update"))
|
||||
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.BlogManagement.Update"))
|
||||
{
|
||||
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||
}
|
||||
|
|
@ -186,7 +187,7 @@ namespace Kurs.Platform.Blog
|
|||
var post = await _postRepository.GetAsync(id);
|
||||
|
||||
// Check if user is author or has permission
|
||||
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Delete"))
|
||||
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.BlogManagement.Delete"))
|
||||
{
|
||||
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||
}
|
||||
|
|
@ -204,7 +205,7 @@ namespace Kurs.Platform.Blog
|
|||
var post = await _postRepository.GetAsync(id);
|
||||
|
||||
// Check if user is author or has permission
|
||||
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Publish"))
|
||||
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.BlogManagement.Publish"))
|
||||
{
|
||||
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||
}
|
||||
|
|
@ -220,7 +221,7 @@ namespace Kurs.Platform.Blog
|
|||
var post = await _postRepository.GetAsync(id);
|
||||
|
||||
// Check if user is author or has permission
|
||||
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Publish"))
|
||||
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.BlogManagement.Publish"))
|
||||
{
|
||||
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||
}
|
||||
|
|
@ -262,7 +263,7 @@ namespace Kurs.Platform.Blog
|
|||
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
|
||||
}
|
||||
|
||||
[Authorize("App.Blog.Create")]
|
||||
[Authorize("App.BlogManagement.Create")]
|
||||
public async Task<BlogCategoryDto> CreateCategoryAsync(CreateUpdateBlogCategoryDto input)
|
||||
{
|
||||
var category = new BlogCategory(
|
||||
|
|
@ -282,7 +283,7 @@ namespace Kurs.Platform.Blog
|
|||
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
|
||||
}
|
||||
|
||||
[Authorize("App.Blog.Update")]
|
||||
[Authorize("App.BlogManagement.Update")]
|
||||
public async Task<BlogCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateBlogCategoryDto input)
|
||||
{
|
||||
var category = await _categoryRepository.GetAsync(id);
|
||||
|
|
@ -299,7 +300,7 @@ namespace Kurs.Platform.Blog
|
|||
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
|
||||
}
|
||||
|
||||
[Authorize("App.Blog.Delete")]
|
||||
[Authorize("App.BlogManagement.Delete")]
|
||||
public async Task DeleteCategoryAsync(Guid id)
|
||||
{
|
||||
// Check if category has posts
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AutoMapper;
|
||||
using Kurs.Platform.Blog;
|
||||
using Kurs.Platform.Entities;
|
||||
|
||||
namespace Kurs.Platform;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Authorization;
|
||||
using Volo.Abp.Domain.Entities;
|
||||
|
|
@ -186,6 +187,24 @@ public class ForumAppService : PlatformAppService, IForumAppService
|
|||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
||||
}
|
||||
|
||||
[Authorize("App.ForumManagement.Update")]
|
||||
public async Task<ForumCategoryDto> UpdateCategoryLockAsync(Guid id)
|
||||
{
|
||||
var category = await _categoryRepository.GetAsync(id);
|
||||
category.IsLocked = !category.IsLocked;
|
||||
await _categoryRepository.UpdateAsync(category);
|
||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
||||
}
|
||||
|
||||
[Authorize("App.ForumManagement.Update")]
|
||||
public async Task<ForumCategoryDto> UpdateCategoryActiveAsync(Guid id)
|
||||
{
|
||||
var category = await _categoryRepository.GetAsync(id);
|
||||
category.IsActive = !category.IsActive;
|
||||
await _categoryRepository.UpdateAsync(category);
|
||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
||||
}
|
||||
|
||||
[Authorize("App.ForumManagement.Delete")]
|
||||
public async Task DeleteCategoryAsync(Guid id)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
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--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
using System;
|
||||
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 ContentTr { get; set; }
|
||||
public string ContentEn { get; set; }
|
||||
public string Summary { get; set; }
|
||||
public string CoverImage { get; set; }
|
||||
public string ReadTime { 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; }
|
||||
|
||||
protected BlogPost()
|
||||
{
|
||||
}
|
||||
|
||||
public BlogPost(
|
||||
Guid id,
|
||||
string title,
|
||||
string slug,
|
||||
string contentTr,
|
||||
string contentEn,
|
||||
string summary,
|
||||
string readTime,
|
||||
string coverImage,
|
||||
Guid categoryId,
|
||||
Guid authorId,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
Title = title;
|
||||
Slug = slug;
|
||||
ContentTr = contentTr;
|
||||
ContentEn = contentEn;
|
||||
Summary = summary;
|
||||
ReadTime = readTime;
|
||||
CoverImage = coverImage;
|
||||
CategoryId = categoryId;
|
||||
AuthorId = authorId;
|
||||
TenantId = tenantId;
|
||||
|
||||
ViewCount = 0;
|
||||
LikeCount = 0;
|
||||
CommentCount = 0;
|
||||
IsPublished = false;
|
||||
}
|
||||
|
||||
public void Publish()
|
||||
{
|
||||
IsPublished = true;
|
||||
PublishedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void Unpublish()
|
||||
{
|
||||
IsPublished = false;
|
||||
PublishedAt = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
api/src/Kurs.Platform.Domain/Entities/BlogCategory.cs
Normal file
54
api/src/Kurs.Platform.Domain/Entities/BlogCategory.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Kurs.Platform.Entities;
|
||||
|
||||
public class BlogCategory : FullAuditedEntity<Guid>
|
||||
{
|
||||
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--;
|
||||
}
|
||||
}
|
||||
75
api/src/Kurs.Platform.Domain/Entities/BlogPost.cs
Normal file
75
api/src/Kurs.Platform.Domain/Entities/BlogPost.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Kurs.Platform.Entities;
|
||||
|
||||
public class BlogPost : FullAuditedAggregateRoot<Guid>
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Slug { get; set; }
|
||||
public string ContentTr { get; set; }
|
||||
public string ContentEn { get; set; }
|
||||
public string Summary { get; set; }
|
||||
public string CoverImage { get; set; }
|
||||
public string ReadTime { 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; }
|
||||
|
||||
protected BlogPost()
|
||||
{
|
||||
}
|
||||
|
||||
public BlogPost(
|
||||
Guid id,
|
||||
string title,
|
||||
string slug,
|
||||
string contentTr,
|
||||
string contentEn,
|
||||
string summary,
|
||||
string readTime,
|
||||
string coverImage,
|
||||
Guid categoryId,
|
||||
Guid authorId,
|
||||
Guid? tenantId = null) : base(id)
|
||||
{
|
||||
Title = title;
|
||||
Slug = slug;
|
||||
ContentTr = contentTr;
|
||||
ContentEn = contentEn;
|
||||
Summary = summary;
|
||||
ReadTime = readTime;
|
||||
CoverImage = coverImage;
|
||||
CategoryId = categoryId;
|
||||
AuthorId = authorId;
|
||||
TenantId = tenantId;
|
||||
|
||||
ViewCount = 0;
|
||||
LikeCount = 0;
|
||||
CommentCount = 0;
|
||||
IsPublished = false;
|
||||
}
|
||||
|
||||
public void Publish()
|
||||
{
|
||||
IsPublished = true;
|
||||
PublishedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void Unpublish()
|
||||
{
|
||||
IsPublished = false;
|
||||
PublishedAt = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Kurs.Platform.Forum;
|
||||
|
||||
public class ForumCategory : FullAuditedEntity<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 Guid? LastPostUserId { get; set; }
|
||||
public string LastPostUserName { get; set; }
|
||||
|
||||
public ICollection<ForumTopic> Topics { get; set; }
|
||||
|
||||
protected ForumCategory() { }
|
||||
|
||||
public ForumCategory(Guid id, string name, string slug, string description, string icon, int displayOrder) : base(id)
|
||||
{
|
||||
Name = name;
|
||||
Slug = slug;
|
||||
Description = description;
|
||||
Icon = icon;
|
||||
DisplayOrder = displayOrder;
|
||||
IsActive = true;
|
||||
IsLocked = false;
|
||||
TopicCount = 0;
|
||||
PostCount = 0;
|
||||
Topics = [];
|
||||
}
|
||||
}
|
||||
|
||||
public class ForumTopic : FullAuditedEntity<Guid>
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public Guid CategoryId { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
public string AuthorName { 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 string LastPostUserName { get; set; }
|
||||
|
||||
public ForumCategory Category { get; set; }
|
||||
public ICollection<ForumPost> Posts { get; set; }
|
||||
|
||||
protected ForumTopic() { }
|
||||
|
||||
public ForumTopic(Guid id, string title, string content, Guid categoryId, Guid authorId, string authorName) : base(id)
|
||||
{
|
||||
Title = title;
|
||||
Content = content;
|
||||
CategoryId = categoryId;
|
||||
AuthorId = authorId;
|
||||
AuthorName = authorName;
|
||||
ViewCount = 0;
|
||||
ReplyCount = 0;
|
||||
LikeCount = 0;
|
||||
IsPinned = false;
|
||||
IsLocked = false;
|
||||
IsSolved = false;
|
||||
Posts = [];
|
||||
}
|
||||
}
|
||||
|
||||
public class ForumPost : FullAuditedEntity<Guid>
|
||||
{
|
||||
public Guid TopicId { get; set; }
|
||||
public string Content { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
public string AuthorName { get; set; }
|
||||
public int LikeCount { get; set; }
|
||||
public bool IsAcceptedAnswer { get; set; }
|
||||
public Guid? ParentPostId { get; set; }
|
||||
|
||||
public ForumTopic Topic { get; set; }
|
||||
public ForumPost ParentPost { get; set; }
|
||||
public ICollection<ForumPost> Replies { get; set; }
|
||||
|
||||
protected ForumPost() { }
|
||||
|
||||
public ForumPost(Guid id, Guid topicId, string content, Guid authorId, string authorName, Guid? parentPostId = null) : base(id)
|
||||
{
|
||||
TopicId = topicId;
|
||||
Content = content;
|
||||
AuthorId = authorId;
|
||||
AuthorName = authorName;
|
||||
ParentPostId = parentPostId;
|
||||
LikeCount = 0;
|
||||
IsAcceptedAnswer = false;
|
||||
Replies = [];
|
||||
}
|
||||
}
|
||||
44
api/src/Kurs.Platform.Domain/Entities/ForumCategory.cs
Normal file
44
api/src/Kurs.Platform.Domain/Entities/ForumCategory.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
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>
|
||||
{
|
||||
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 string LastPostUserName { get; set; }
|
||||
|
||||
public ICollection<ForumTopic> Topics { get; set; }
|
||||
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
protected ForumCategory() { }
|
||||
|
||||
public ForumCategory(Guid id, string name, string slug, string description, string icon, int displayOrder, Guid? tenantId = null) : base(id)
|
||||
{
|
||||
Name = name;
|
||||
Slug = slug;
|
||||
Description = description;
|
||||
Icon = icon;
|
||||
DisplayOrder = displayOrder;
|
||||
IsActive = true;
|
||||
IsLocked = false;
|
||||
TopicCount = 0;
|
||||
PostCount = 0;
|
||||
TenantId = tenantId;
|
||||
Topics = [];
|
||||
}
|
||||
}
|
||||
37
api/src/Kurs.Platform.Domain/Entities/ForumPost.cs
Normal file
37
api/src/Kurs.Platform.Domain/Entities/ForumPost.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
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>
|
||||
{
|
||||
public Guid TopicId { get; set; }
|
||||
public string Content { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
public string AuthorName { get; set; }
|
||||
public int LikeCount { get; set; }
|
||||
public bool IsAcceptedAnswer { get; set; }
|
||||
public Guid? ParentPostId { get; set; }
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public ForumTopic Topic { get; set; }
|
||||
public ForumPost ParentPost { get; set; }
|
||||
public ICollection<ForumPost> Replies { get; set; }
|
||||
|
||||
protected ForumPost() { }
|
||||
|
||||
public ForumPost(Guid id, Guid topicId, string content, Guid authorId, string authorName, Guid? parentPostId = null, Guid? tenantId = null) : base(id)
|
||||
{
|
||||
TopicId = topicId;
|
||||
Content = content;
|
||||
AuthorId = authorId;
|
||||
AuthorName = authorName;
|
||||
ParentPostId = parentPostId;
|
||||
LikeCount = 0;
|
||||
IsAcceptedAnswer = false;
|
||||
TenantId = tenantId;
|
||||
Replies = [];
|
||||
}
|
||||
}
|
||||
48
api/src/Kurs.Platform.Domain/Entities/ForumTopic.cs
Normal file
48
api/src/Kurs.Platform.Domain/Entities/ForumTopic.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
|
||||
namespace Kurs.Platform.Forum;
|
||||
|
||||
public class ForumTopic : FullAuditedEntity<Guid>
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public Guid CategoryId { get; set; }
|
||||
public Guid AuthorId { get; set; }
|
||||
public string AuthorName { 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 string LastPostUserName { get; set; }
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
public ForumCategory Category { get; set; }
|
||||
public ICollection<ForumPost> Posts { get; set; }
|
||||
|
||||
protected ForumTopic() { }
|
||||
|
||||
public ForumTopic(Guid id, string title, string content, Guid categoryId, Guid authorId, string authorName, Guid? tenantId = null) : base(id)
|
||||
{
|
||||
Title = title;
|
||||
Content = content;
|
||||
CategoryId = categoryId;
|
||||
AuthorId = authorId;
|
||||
AuthorName = authorName;
|
||||
ViewCount = 0;
|
||||
ReplyCount = 0;
|
||||
LikeCount = 0;
|
||||
IsPinned = false;
|
||||
IsLocked = false;
|
||||
IsSolved = false;
|
||||
TenantId = tenantId;
|
||||
Posts = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using Kurs.Languages.EntityFrameworkCore;
|
||||
using Kurs.Platform.Entities;
|
||||
using Kurs.Platform.Blog;
|
||||
using Kurs.Settings.EntityFrameworkCore;
|
||||
using Kurs.MailQueue.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Kurs.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20250623140407_AddForum")]
|
||||
[Migration("20250623190205_AddForum")]
|
||||
partial class AddForum
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -653,193 +653,6 @@ namespace Kurs.Platform.Migrations
|
|||
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.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>("ContentEn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ContentTr")
|
||||
.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>("ReadTime")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
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.Entities.AiBot", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1071,6 +884,193 @@ namespace Kurs.Platform.Migrations
|
|||
b.ToTable("BankAccounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.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.Entities.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>("ContentEn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ContentTr")
|
||||
.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>("ReadTime")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
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.Entities.Branch", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -2514,6 +2514,10 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<string>("Slug")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<int>("TopicCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
|
|
@ -2578,6 +2582,10 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<Guid?>("ParentPostId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("TopicId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
@ -2665,6 +2673,10 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<int>("ReplyCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
|
|
@ -4757,17 +4769,6 @@ namespace Kurs.Platform.Migrations
|
|||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
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.Entities.BankAccount", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
|
||||
|
|
@ -4785,6 +4786,17 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("Currency");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.BlogPost", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.BlogCategory", "Category")
|
||||
.WithMany("Posts")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
|
||||
|
|
@ -5027,7 +5039,7 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("Texts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
|
||||
{
|
||||
b.Navigation("Posts");
|
||||
});
|
||||
|
|
@ -29,6 +29,7 @@ namespace Kurs.Platform.Migrations
|
|||
LastPostDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastPostUserName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
TenantId = 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),
|
||||
|
|
@ -62,6 +63,7 @@ namespace Kurs.Platform.Migrations
|
|||
LastPostDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastPostUserName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
TenantId = 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),
|
||||
|
|
@ -93,6 +95,7 @@ namespace Kurs.Platform.Migrations
|
|||
LikeCount = table.Column<int>(type: "int", nullable: false),
|
||||
IsAcceptedAnswer = table.Column<bool>(type: "bit", nullable: false),
|
||||
ParentPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
TenantId = 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),
|
||||
|
|
@ -650,193 +650,6 @@ namespace Kurs.Platform.Migrations
|
|||
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.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>("ContentEn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ContentTr")
|
||||
.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>("ReadTime")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
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.Entities.AiBot", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1068,6 +881,193 @@ namespace Kurs.Platform.Migrations
|
|||
b.ToTable("BankAccounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.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.Entities.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>("ContentEn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ContentTr")
|
||||
.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>("ReadTime")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
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.Entities.Branch", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -2511,6 +2511,10 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<string>("Slug")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<int>("TopicCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
|
|
@ -2575,6 +2579,10 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<Guid?>("ParentPostId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<Guid>("TopicId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
@ -2662,6 +2670,10 @@ namespace Kurs.Platform.Migrations
|
|||
b.Property<int>("ReplyCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("TenantId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("TenantId");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
|
|
@ -4754,17 +4766,6 @@ namespace Kurs.Platform.Migrations
|
|||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
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.Entities.BankAccount", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
|
||||
|
|
@ -4782,6 +4783,17 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("Currency");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.BlogPost", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.BlogCategory", "Category")
|
||||
.WithMany("Posts")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
|
||||
{
|
||||
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
|
||||
|
|
@ -5024,7 +5036,7 @@ namespace Kurs.Platform.Migrations
|
|||
b.Navigation("Texts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
|
||||
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
|
||||
{
|
||||
b.Navigation("Posts");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,23 +39,6 @@ export interface BlogCategory {
|
|||
isActive: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
slug: string;
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
|||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.ouca2h9ms1"
|
||||
"revision": "0.mvu82hb2mqg"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
|
|
|||
81
ui/src/proxy/blog/blog.ts
Normal file
81
ui/src/proxy/blog/blog.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
export interface BlogPost {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
contentTr?: string
|
||||
contentEn?: 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
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export interface BlogCategory {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
description?: string
|
||||
postCount: number
|
||||
isActive: boolean
|
||||
icon: string
|
||||
displayOrder: number
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export interface CreateUpdateBlogPostDto {
|
||||
title: string
|
||||
slug: string
|
||||
contentTr: string
|
||||
contentEn: string
|
||||
summary: string
|
||||
categoryId: string
|
||||
tags: string[]
|
||||
coverImage?: string
|
||||
isPublished: boolean
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export interface CreateUpdateBlogCategoryDto {
|
||||
tenantId?: string
|
||||
name: string
|
||||
slug: string
|
||||
description: string
|
||||
icon?: string
|
||||
displayOrder: number
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -10,9 +10,10 @@ export interface ForumCategory {
|
|||
topicCount: number;
|
||||
postCount: number;
|
||||
lastPostId?: string;
|
||||
lastPostDate?: Date;
|
||||
lastPostDate?: string;
|
||||
lastPostUserId?: string;
|
||||
creationTime: Date;
|
||||
creationTime: string;
|
||||
tenantId?: string;
|
||||
}
|
||||
|
||||
export interface ForumTopic {
|
||||
|
|
@ -29,10 +30,11 @@ export interface ForumTopic {
|
|||
isLocked: boolean;
|
||||
isSolved: boolean;
|
||||
lastPostId?: string;
|
||||
lastPostDate?: Date;
|
||||
lastPostDate?: string;
|
||||
lastPostUserId?: string;
|
||||
lastPostUserName?: string;
|
||||
creationTime: Date;
|
||||
creationTime: string;
|
||||
tenantId?: string;
|
||||
}
|
||||
|
||||
export interface ForumPost {
|
||||
|
|
@ -44,7 +46,8 @@ export interface ForumPost {
|
|||
likeCount: number;
|
||||
isAcceptedAnswer: boolean;
|
||||
parentPostId?: string;
|
||||
creationTime: Date;
|
||||
creationTime: string;
|
||||
tenantId?: string;
|
||||
}
|
||||
|
||||
export type ViewMode = 'forum' | 'admin';
|
||||
|
|
@ -1,106 +1,6 @@
|
|||
import { BlogCategory, BlogListParams, BlogPost, CreateUpdateBlogCategoryDto, CreateUpdateBlogPostDto, PaginatedResponse } from '@/proxy/blog/blog';
|
||||
import apiService from '@/services/api.service'
|
||||
|
||||
export interface BlogPost {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
contentTr?: string
|
||||
contentEn?: 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
|
||||
isActive: boolean
|
||||
icon: string
|
||||
displayOrder: 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
|
||||
slug: string
|
||||
contentTr: string
|
||||
contentEn: 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>>({
|
||||
|
|
@ -176,23 +76,6 @@ class BlogService {
|
|||
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}`,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export interface CreateCategoryRequest {
|
|||
displayOrder: number
|
||||
isActive: boolean
|
||||
isLocked: boolean
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export interface CreateTopicRequest {
|
||||
|
|
@ -43,12 +44,14 @@ export interface CreateTopicRequest {
|
|||
categoryId: string
|
||||
isPinned?: boolean
|
||||
isLocked?: boolean
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export interface CreatePostRequest {
|
||||
topicId: string
|
||||
content: string
|
||||
parentPostId?: string
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export interface CategoryListParams {
|
||||
|
|
@ -115,7 +118,7 @@ class ForumService {
|
|||
|
||||
async createCategory(data: CreateCategoryRequest): Promise<ForumCategory> {
|
||||
const response = await apiService.fetchData<ForumCategory>({
|
||||
url: '/api/app/forum/categories',
|
||||
url: '/api/app/forum/category',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
|
|
@ -124,27 +127,45 @@ class ForumService {
|
|||
|
||||
async updateCategory(id: string, data: Partial<CreateCategoryRequest>): Promise<ForumCategory> {
|
||||
const response = await apiService.fetchData<ForumCategory>({
|
||||
url: `/api/app/forum/categories/${id}`,
|
||||
url: `/api/app/forum/${id}/category`,
|
||||
method: 'PUT',
|
||||
data,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
async updateCategoryLockState(id: string): Promise<ForumCategory> {
|
||||
const response = await apiService.fetchData<ForumCategory>({
|
||||
url: `/api/app/forum/${id}/category-lock`,
|
||||
method: 'PUT',
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
async updateCategoryActiveState(id: string): Promise<ForumCategory> {
|
||||
const response = await apiService.fetchData<ForumCategory>({
|
||||
url: `/api/app/forum/${id}/category-active`,
|
||||
method: 'PUT',
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
async deleteCategory(id: string): Promise<void> {
|
||||
await apiService.fetchData({
|
||||
url: `/api/app/forum/categories/${id}`,
|
||||
url: `/api/app/forum/${id}/category`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
async toggleCategoryStatus(id: string): Promise<ForumCategory> {
|
||||
const response = await apiService.fetchData<ForumCategory>({
|
||||
url: `/api/app/forum/categories/${id}/toggle-status`,
|
||||
method: 'POST',
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
// async toggleCategoryStatus(id: string): Promise<ForumCategory> {
|
||||
// const response = await apiService.fetchData<ForumCategory>({
|
||||
// url: `/api/app/forum/categories/${id}/toggle-status`,
|
||||
// method: 'POST',
|
||||
// })
|
||||
// return response.data
|
||||
// }
|
||||
|
||||
// Topics
|
||||
async getTopics(params: TopicListParams = {}): Promise<PaginatedResponse<ForumTopic>> {
|
||||
|
|
@ -175,7 +196,7 @@ class ForumService {
|
|||
|
||||
async updateTopic(id: string, data: Partial<CreateTopicRequest>): Promise<ForumTopic> {
|
||||
const response = await apiService.fetchData<ForumTopic>({
|
||||
url: `/api/app/forum/topics/${id}`,
|
||||
url: `/api/app/forum/${id}/topic`,
|
||||
method: 'PUT',
|
||||
data,
|
||||
})
|
||||
|
|
@ -184,7 +205,7 @@ class ForumService {
|
|||
|
||||
async deleteTopic(id: string): Promise<void> {
|
||||
await apiService.fetchData({
|
||||
url: `/api/app/forum/topics/${id}`,
|
||||
url: `/api/app/forum/${id}/topic`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
|
@ -336,10 +357,17 @@ class ForumService {
|
|||
totalUsers: number
|
||||
activeUsers: number
|
||||
}> {
|
||||
const response = await apiService.fetchData({
|
||||
const response = await apiService.fetchData<{
|
||||
totalCategories: number
|
||||
totalTopics: number
|
||||
totalPosts: number
|
||||
totalUsers: number
|
||||
activeUsers: number
|
||||
}>({
|
||||
url: '/api/app/forum/stats',
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ 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'
|
||||
|
|
@ -39,6 +35,7 @@ import { useStoreState } from '@/store/store'
|
|||
import TabList from '@/components/ui/Tabs/TabList'
|
||||
import TabNav from '@/components/ui/Tabs/TabNav'
|
||||
import TabContent from '@/components/ui/Tabs/TabContent'
|
||||
import { BlogCategory, BlogPost, CreateUpdateBlogCategoryDto, CreateUpdateBlogPostDto } from '@/proxy/blog/blog'
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
title: Yup.string().required(),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export function Management() {
|
|||
error,
|
||||
createCategory,
|
||||
updateCategory,
|
||||
updateCategoryLockState,
|
||||
updateCategoryActiveState,
|
||||
deleteCategory,
|
||||
createTopic,
|
||||
updateTopic,
|
||||
|
|
@ -79,6 +81,8 @@ export function Management() {
|
|||
loading={loading}
|
||||
onCreateCategory={(data) => createCategory(data).then(() => {})}
|
||||
onUpdateCategory={(id, data) => updateCategory(id, data).then(() => {})}
|
||||
onUpdateCategoryLockState={(id) => updateCategoryLockState(id).then(() => {})}
|
||||
onUpdateCategoryActiveState={(id) => updateCategoryActiveState(id).then(() => {})}
|
||||
onDeleteCategory={(id) => deleteCategory(id).then(() => {})}
|
||||
onCreateTopic={(data) => createTopic(data).then(() => {})}
|
||||
onUpdateTopic={(id, data) => updateTopic(id, data).then(() => {})}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ interface AdminViewProps {
|
|||
isLocked: boolean;
|
||||
}) => Promise<void>;
|
||||
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<void>;
|
||||
onUpdateCategoryLockState: (id: string) => Promise<void>;
|
||||
onUpdateCategoryActiveState: (id: string) => Promise<void>;
|
||||
onDeleteCategory: (id: string) => Promise<void>;
|
||||
onCreateTopic: (topic: {
|
||||
title: string;
|
||||
|
|
@ -58,6 +60,8 @@ export function AdminView({
|
|||
onCreateCategory,
|
||||
onUpdateCategory,
|
||||
onDeleteCategory,
|
||||
onUpdateCategoryLockState,
|
||||
onUpdateCategoryActiveState,
|
||||
onCreateTopic,
|
||||
onUpdateTopic,
|
||||
onDeleteTopic,
|
||||
|
|
@ -121,6 +125,8 @@ export function AdminView({
|
|||
onCreateCategory={onCreateCategory}
|
||||
onUpdateCategory={onUpdateCategory}
|
||||
onDeleteCategory={onDeleteCategory}
|
||||
onUpdateCategoryLockState={onUpdateCategoryLockState}
|
||||
onUpdateCategoryActiveState={onUpdateCategoryActiveState}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ interface CategoryManagementProps {
|
|||
}) => Promise<void>;
|
||||
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<void>;
|
||||
onDeleteCategory: (id: string) => Promise<void>;
|
||||
onUpdateCategoryLockState: (id: string) => Promise<void>;
|
||||
onUpdateCategoryActiveState: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function CategoryManagement({
|
||||
|
|
@ -23,7 +25,9 @@ export function CategoryManagement({
|
|||
loading,
|
||||
onCreateCategory,
|
||||
onUpdateCategory,
|
||||
onDeleteCategory
|
||||
onDeleteCategory,
|
||||
onUpdateCategoryLockState,
|
||||
onUpdateCategoryActiveState
|
||||
}: CategoryManagementProps) {
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [editingCategory, setEditingCategory] = useState<ForumCategory | null>(null);
|
||||
|
|
@ -87,7 +91,7 @@ export function CategoryManagement({
|
|||
|
||||
const handleToggleActive = async (category: ForumCategory) => {
|
||||
try {
|
||||
await onUpdateCategory(category.id, { isActive: !category.isActive });
|
||||
await onUpdateCategoryActiveState(category.id);
|
||||
} catch (error) {
|
||||
console.error('Error toggling category status:', error);
|
||||
}
|
||||
|
|
@ -95,7 +99,7 @@ export function CategoryManagement({
|
|||
|
||||
const handleToggleLocked = async (category: ForumCategory) => {
|
||||
try {
|
||||
await onUpdateCategory(category.id, { isLocked: !category.isLocked });
|
||||
await onUpdateCategoryLockState(category.id);
|
||||
} catch (error) {
|
||||
console.error('Error toggling category lock:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,116 +1,115 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Plus, Edit2, Trash2, CheckCircle, Circle, Heart, Loader2 } from 'lucide-react';
|
||||
import { ForumPost, ForumTopic } from '@/proxy/forum/forum';
|
||||
import React, { useState } from 'react'
|
||||
import { Plus, Edit2, Trash2, CheckCircle, Circle, Heart, Loader2 } from 'lucide-react'
|
||||
import { ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||
|
||||
interface PostManagementProps {
|
||||
posts: ForumPost[];
|
||||
topics: ForumTopic[];
|
||||
loading: boolean;
|
||||
onCreatePost: (post: {
|
||||
topicId: string;
|
||||
content: string;
|
||||
parentPostId?: string;
|
||||
}) => Promise<void>;
|
||||
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<void>;
|
||||
onDeletePost: (id: string) => Promise<void>;
|
||||
onMarkPostAsAcceptedAnswer: (id: string) => Promise<void>;
|
||||
onUnmarkPostAsAcceptedAnswer: (id: string) => Promise<void>;
|
||||
posts: ForumPost[]
|
||||
topics: ForumTopic[]
|
||||
loading: boolean
|
||||
onCreatePost: (post: { topicId: string; content: string; parentPostId?: string }) => Promise<void>
|
||||
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<void>
|
||||
onDeletePost: (id: string) => Promise<void>
|
||||
onMarkPostAsAcceptedAnswer: (id: string) => Promise<void>
|
||||
onUnmarkPostAsAcceptedAnswer: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function PostManagement({
|
||||
posts,
|
||||
topics,
|
||||
export function PostManagement({
|
||||
posts,
|
||||
topics,
|
||||
loading,
|
||||
onCreatePost,
|
||||
onUpdatePost,
|
||||
onCreatePost,
|
||||
onUpdatePost,
|
||||
onDeletePost,
|
||||
onMarkPostAsAcceptedAnswer,
|
||||
onUnmarkPostAsAcceptedAnswer
|
||||
onUnmarkPostAsAcceptedAnswer,
|
||||
}: PostManagementProps) {
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [editingPost, setEditingPost] = useState<ForumPost | null>(null);
|
||||
const [showCreateForm, setShowCreateForm] = useState(false)
|
||||
const [editingPost, setEditingPost] = useState<ForumPost | null>(null)
|
||||
const [formData, setFormData] = useState({
|
||||
topicId: '',
|
||||
content: '',
|
||||
isAcceptedAnswer: false,
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
topicId: '',
|
||||
content: '',
|
||||
isAcceptedAnswer: false,
|
||||
});
|
||||
setShowCreateForm(false);
|
||||
setEditingPost(null);
|
||||
};
|
||||
})
|
||||
setShowCreateForm(false)
|
||||
setEditingPost(null)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (submitting) return;
|
||||
e.preventDefault()
|
||||
if (submitting) return
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
setSubmitting(true)
|
||||
if (editingPost) {
|
||||
await onUpdatePost(editingPost.id, formData);
|
||||
await onUpdatePost(editingPost.id, formData)
|
||||
} else {
|
||||
await onCreatePost(formData);
|
||||
await onCreatePost(formData)
|
||||
}
|
||||
resetForm();
|
||||
resetForm()
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
console.error('Error submitting form:', error)
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
setSubmitting(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleEdit = (post: ForumPost) => {
|
||||
setEditingPost(post);
|
||||
setEditingPost(post)
|
||||
setFormData({
|
||||
topicId: post.topicId,
|
||||
content: post.content,
|
||||
isAcceptedAnswer: post.isAcceptedAnswer,
|
||||
});
|
||||
setShowCreateForm(true);
|
||||
};
|
||||
})
|
||||
setShowCreateForm(true)
|
||||
}
|
||||
|
||||
const handleToggleAcceptedAnswer = async (post: ForumPost) => {
|
||||
try {
|
||||
if (post.isAcceptedAnswer) {
|
||||
await onUnmarkPostAsAcceptedAnswer(post.id);
|
||||
await onUnmarkPostAsAcceptedAnswer(post.id)
|
||||
} else {
|
||||
await onMarkPostAsAcceptedAnswer(post.id);
|
||||
await onMarkPostAsAcceptedAnswer(post.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling accepted answer:', error);
|
||||
console.error('Error toggling accepted answer:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this post?')) {
|
||||
try {
|
||||
await onDeletePost(id);
|
||||
await onDeletePost(id)
|
||||
} catch (error) {
|
||||
console.error('Error deleting post:', error);
|
||||
console.error('Error deleting post:', error)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getTopicTitle = (topicId: string) => {
|
||||
const topic = topics.find(t => t.id === topicId);
|
||||
return topic ? topic.title : 'Unknown Topic';
|
||||
};
|
||||
const topic = topics.find((t) => t.id === topicId)
|
||||
return topic ? topic.title : 'Unknown Topic'
|
||||
}
|
||||
|
||||
const formatDate = (value: string | Date) => {
|
||||
const date = value instanceof Date ? value : new Date(value)
|
||||
if (isNaN(date.getTime())) return 'Invalid Date'
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
};
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -142,14 +141,14 @@ export function PostManagement({
|
|||
required
|
||||
>
|
||||
<option value="">Select a topic</option>
|
||||
{topics.map(topic => (
|
||||
{topics.map((topic) => (
|
||||
<option key={topic.id} value={topic.id}>
|
||||
{topic.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
|
||||
<textarea
|
||||
|
|
@ -160,7 +159,7 @@ export function PostManagement({
|
|||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
|
|
@ -172,7 +171,7 @@ export function PostManagement({
|
|||
Mark as Accepted Answer
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -200,7 +199,7 @@ export function PostManagement({
|
|||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Posts ({posts.length})</h3>
|
||||
</div>
|
||||
|
||||
|
||||
{loading ? (
|
||||
<div className="p-8 text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
|
||||
|
|
@ -209,7 +208,9 @@ export function PostManagement({
|
|||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{posts
|
||||
.sort((a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime())
|
||||
.sort(
|
||||
(a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime(),
|
||||
)
|
||||
.map((post) => (
|
||||
<div key={post.id} className="p-6 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
|
|
@ -223,14 +224,15 @@ export function PostManagement({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-xs text-gray-500 mb-1">
|
||||
Reply to: <span className="font-medium">{getTopicTitle(post.topicId)}</span>
|
||||
Reply to:{' '}
|
||||
<span className="font-medium">{getTopicTitle(post.topicId)}</span>
|
||||
</p>
|
||||
<p className="text-gray-700 line-clamp-3">{post.content}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span>{formatDate(post.creationTime)}</span>
|
||||
|
|
@ -241,7 +243,7 @@ export function PostManagement({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
<button
|
||||
onClick={() => handleToggleAcceptedAnswer(post)}
|
||||
|
|
@ -250,11 +252,19 @@ export function PostManagement({
|
|||
? 'text-emerald-600 hover:bg-emerald-100'
|
||||
: 'text-gray-400 hover:bg-gray-100'
|
||||
}`}
|
||||
title={post.isAcceptedAnswer ? 'Remove Accepted Answer' : 'Mark as Accepted Answer'}
|
||||
title={
|
||||
post.isAcceptedAnswer
|
||||
? 'Remove Accepted Answer'
|
||||
: 'Mark as Accepted Answer'
|
||||
}
|
||||
>
|
||||
{post.isAcceptedAnswer ? <CheckCircle className="w-4 h-4" /> : <Circle className="w-4 h-4" />}
|
||||
{post.isAcceptedAnswer ? (
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
) : (
|
||||
<Circle className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => handleEdit(post)}
|
||||
className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
|
||||
|
|
@ -262,7 +272,7 @@ export function PostManagement({
|
|||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(post.id)}
|
||||
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
||||
|
|
@ -278,5 +288,5 @@ export function PostManagement({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,56 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Plus, Edit2, Trash2, Lock, Unlock, Pin, PinOff, CheckCircle, Circle, Eye, Loader2 } from 'lucide-react';
|
||||
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum';
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Plus,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Lock,
|
||||
Unlock,
|
||||
Pin,
|
||||
PinOff,
|
||||
CheckCircle,
|
||||
Circle,
|
||||
Eye,
|
||||
Loader2,
|
||||
} from 'lucide-react'
|
||||
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum'
|
||||
|
||||
interface TopicManagementProps {
|
||||
topics: ForumTopic[];
|
||||
categories: ForumCategory[];
|
||||
loading: boolean;
|
||||
topics: ForumTopic[]
|
||||
categories: ForumCategory[]
|
||||
loading: boolean
|
||||
onCreateTopic: (topic: {
|
||||
title: string;
|
||||
content: string;
|
||||
categoryId: string;
|
||||
isPinned?: boolean;
|
||||
isLocked?: boolean;
|
||||
}) => Promise<void>;
|
||||
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<void>;
|
||||
onDeleteTopic: (id: string) => Promise<void>;
|
||||
onPinTopic: (id: string) => Promise<void>;
|
||||
onUnpinTopic: (id: string) => Promise<void>;
|
||||
onLockTopic: (id: string) => Promise<void>;
|
||||
onUnlockTopic: (id: string) => Promise<void>;
|
||||
onMarkTopicAsSolved: (id: string) => Promise<void>;
|
||||
onMarkTopicAsUnsolved: (id: string) => Promise<void>;
|
||||
title: string
|
||||
content: string
|
||||
categoryId: string
|
||||
isPinned?: boolean
|
||||
isLocked?: boolean
|
||||
}) => Promise<void>
|
||||
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<void>
|
||||
onDeleteTopic: (id: string) => Promise<void>
|
||||
onPinTopic: (id: string) => Promise<void>
|
||||
onUnpinTopic: (id: string) => Promise<void>
|
||||
onLockTopic: (id: string) => Promise<void>
|
||||
onUnlockTopic: (id: string) => Promise<void>
|
||||
onMarkTopicAsSolved: (id: string) => Promise<void>
|
||||
onMarkTopicAsUnsolved: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function TopicManagement({
|
||||
topics,
|
||||
categories,
|
||||
export function TopicManagement({
|
||||
topics,
|
||||
categories,
|
||||
loading,
|
||||
onCreateTopic,
|
||||
onUpdateTopic,
|
||||
onCreateTopic,
|
||||
onUpdateTopic,
|
||||
onDeleteTopic,
|
||||
onPinTopic,
|
||||
onUnpinTopic,
|
||||
onLockTopic,
|
||||
onUnlockTopic,
|
||||
onMarkTopicAsSolved,
|
||||
onMarkTopicAsUnsolved
|
||||
onMarkTopicAsUnsolved,
|
||||
}: TopicManagementProps) {
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null);
|
||||
const [showCreateForm, setShowCreateForm] = useState(false)
|
||||
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null)
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
content: '',
|
||||
|
|
@ -46,8 +58,8 @@ export function TopicManagement({
|
|||
isPinned: false,
|
||||
isLocked: false,
|
||||
isSolved: false,
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
|
|
@ -57,32 +69,32 @@ export function TopicManagement({
|
|||
isPinned: false,
|
||||
isLocked: false,
|
||||
isSolved: false,
|
||||
});
|
||||
setShowCreateForm(false);
|
||||
setEditingTopic(null);
|
||||
};
|
||||
})
|
||||
setShowCreateForm(false)
|
||||
setEditingTopic(null)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (submitting) return;
|
||||
e.preventDefault()
|
||||
if (submitting) return
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
setSubmitting(true)
|
||||
if (editingTopic) {
|
||||
await onUpdateTopic(editingTopic.id, formData);
|
||||
await onUpdateTopic(editingTopic.id, formData)
|
||||
} else {
|
||||
await onCreateTopic(formData);
|
||||
await onCreateTopic(formData)
|
||||
}
|
||||
resetForm();
|
||||
resetForm()
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
console.error('Error submitting form:', error)
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
setSubmitting(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleEdit = (topic: ForumTopic) => {
|
||||
setEditingTopic(topic);
|
||||
setEditingTopic(topic)
|
||||
setFormData({
|
||||
title: topic.title,
|
||||
content: topic.content,
|
||||
|
|
@ -90,68 +102,77 @@ export function TopicManagement({
|
|||
isPinned: topic.isPinned,
|
||||
isLocked: topic.isLocked,
|
||||
isSolved: topic.isSolved,
|
||||
});
|
||||
setShowCreateForm(true);
|
||||
};
|
||||
})
|
||||
setShowCreateForm(true)
|
||||
}
|
||||
|
||||
const handlePin = async (topic: ForumTopic) => {
|
||||
try {
|
||||
if (topic.isPinned) {
|
||||
await onUnpinTopic(topic.id);
|
||||
await onUnpinTopic(topic.id)
|
||||
} else {
|
||||
await onPinTopic(topic.id);
|
||||
await onPinTopic(topic.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling pin:', error);
|
||||
console.error('Error toggling pin:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleLock = async (topic: ForumTopic) => {
|
||||
try {
|
||||
if (topic.isLocked) {
|
||||
await onUnlockTopic(topic.id);
|
||||
await onUnlockTopic(topic.id)
|
||||
} else {
|
||||
await onLockTopic(topic.id);
|
||||
await onLockTopic(topic.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling lock:', error);
|
||||
console.error('Error toggling lock:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleSolved = async (topic: ForumTopic) => {
|
||||
try {
|
||||
if (topic.isSolved) {
|
||||
await onMarkTopicAsUnsolved(topic.id);
|
||||
await onMarkTopicAsUnsolved(topic.id)
|
||||
} else {
|
||||
await onMarkTopicAsSolved(topic.id);
|
||||
await onMarkTopicAsSolved(topic.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling solved status:', error);
|
||||
console.error('Error toggling solved status:', error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this topic? This will also delete all posts in this topic.')) {
|
||||
if (
|
||||
confirm(
|
||||
'Are you sure you want to delete this topic? This will also delete all posts in this topic.',
|
||||
)
|
||||
) {
|
||||
try {
|
||||
await onDeleteTopic(id);
|
||||
await onDeleteTopic(id)
|
||||
} catch (error) {
|
||||
console.error('Error deleting topic:', error);
|
||||
console.error('Error deleting topic:', error)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getCategoryName = (categoryId: string) => {
|
||||
const category = categories.find(c => c.id === categoryId);
|
||||
return category ? category.name : 'Unknown Category';
|
||||
};
|
||||
const category = categories.find((c) => c.id === categoryId)
|
||||
return category ? category.name : 'Unknown Category'
|
||||
}
|
||||
|
||||
const formatDate = (value: string | Date) => {
|
||||
const date = value instanceof Date ? value : new Date(value)
|
||||
if (isNaN(date.getTime())) return 'Invalid Date'
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
};
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -184,7 +205,7 @@ export function TopicManagement({
|
|||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
||||
<select
|
||||
|
|
@ -194,14 +215,14 @@ export function TopicManagement({
|
|||
required
|
||||
>
|
||||
<option value="">Select a category</option>
|
||||
{categories.map(category => (
|
||||
{categories.map((category) => (
|
||||
<option key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
|
||||
<textarea
|
||||
|
|
@ -212,7 +233,7 @@ export function TopicManagement({
|
|||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-6">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
|
|
@ -242,7 +263,7 @@ export function TopicManagement({
|
|||
Solved
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -270,7 +291,7 @@ export function TopicManagement({
|
|||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Topics ({topics.length})</h3>
|
||||
</div>
|
||||
|
||||
|
||||
{loading ? (
|
||||
<div className="p-8 text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
|
||||
|
|
@ -279,7 +300,9 @@ export function TopicManagement({
|
|||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{topics
|
||||
.sort((a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime())
|
||||
.sort(
|
||||
(a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime(),
|
||||
)
|
||||
.map((topic) => (
|
||||
<div key={topic.id} className="p-6 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
|
|
@ -288,11 +311,13 @@ export function TopicManagement({
|
|||
{topic.isPinned && <Pin className="w-4 h-4 text-orange-500" />}
|
||||
{topic.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
|
||||
{topic.isSolved && <CheckCircle className="w-4 h-4 text-emerald-500" />}
|
||||
<h4 className="text-lg font-semibold text-gray-900 line-clamp-1">{topic.title}</h4>
|
||||
<h4 className="text-lg font-semibold text-gray-900 line-clamp-1">
|
||||
{topic.title}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
|
||||
<p className="text-gray-600 mb-3 line-clamp-2">{topic.content}</p>
|
||||
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="font-medium">{getCategoryName(topic.categoryId)}</span>
|
||||
|
|
@ -309,7 +334,7 @@ export function TopicManagement({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
<button
|
||||
onClick={() => handlePin(topic)}
|
||||
|
|
@ -320,9 +345,13 @@ export function TopicManagement({
|
|||
}`}
|
||||
title={topic.isPinned ? 'Unpin Topic' : 'Pin Topic'}
|
||||
>
|
||||
{topic.isPinned ? <PinOff className="w-4 h-4" /> : <Pin className="w-4 h-4" />}
|
||||
{topic.isPinned ? (
|
||||
<PinOff className="w-4 h-4" />
|
||||
) : (
|
||||
<Pin className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => handleLock(topic)}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
|
|
@ -332,9 +361,13 @@ export function TopicManagement({
|
|||
}`}
|
||||
title={topic.isLocked ? 'Unlock Topic' : 'Lock Topic'}
|
||||
>
|
||||
{topic.isLocked ? <Lock className="w-4 h-4" /> : <Unlock className="w-4 h-4" />}
|
||||
{topic.isLocked ? (
|
||||
<Lock className="w-4 h-4" />
|
||||
) : (
|
||||
<Unlock className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => handleSolved(topic)}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
|
|
@ -344,9 +377,13 @@ export function TopicManagement({
|
|||
}`}
|
||||
title={topic.isSolved ? 'Mark as Unsolved' : 'Mark as Solved'}
|
||||
>
|
||||
{topic.isSolved ? <CheckCircle className="w-4 h-4" /> : <Circle className="w-4 h-4" />}
|
||||
{topic.isSolved ? (
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
) : (
|
||||
<Circle className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => handleEdit(topic)}
|
||||
className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
|
||||
|
|
@ -354,7 +391,7 @@ export function TopicManagement({
|
|||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(topic.id)}
|
||||
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
||||
|
|
@ -370,5 +407,5 @@ export function TopicManagement({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import React, { useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import ReactQuill from 'react-quill';
|
||||
import 'react-quill/dist/quill.snow.css';
|
||||
|
||||
interface CreatePostModalProps {
|
||||
onClose: () => void;
|
||||
onSubmit: (data: { content: string }) => void;
|
||||
parentPostId?: string;
|
||||
}
|
||||
|
||||
export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
|
||||
export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostModalProps) {
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (content.trim()) {
|
||||
onSubmit({ content: content.trim() });
|
||||
const plainText = content.replace(/<[^>]+>/g, '').trim(); // HTML etiketlerini temizle
|
||||
if (plainText) {
|
||||
onSubmit({ content });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -20,7 +24,9 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
|
|||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Reply to Topic</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{parentPostId ? 'Reply to Topic' : 'New Post'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
|
|
@ -28,24 +34,21 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
|
|||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Your Reply
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{parentPostId ? 'Your Reply' : 'Message'}
|
||||
</label>
|
||||
<textarea
|
||||
id="content"
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
rows={6}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Write your reply..."
|
||||
required
|
||||
autoFocus
|
||||
onChange={setContent}
|
||||
style={{ height: '400px', marginBottom: '50px' }}
|
||||
placeholder={parentPostId ? 'Write your reply...' : 'Write your message...'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -57,7 +60,7 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
|
|||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors disabled:opacity-50"
|
||||
disabled={!content.trim()}
|
||||
disabled={!content || content.replace(/<[^>]+>/g, '').trim() === ''}
|
||||
>
|
||||
Post Reply
|
||||
</button>
|
||||
|
|
@ -66,4 +69,4 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,24 @@
|
|||
import React from 'react';
|
||||
import { MessageSquare, Lock, TrendingUp } from 'lucide-react';
|
||||
import { ForumCategory } from '@/proxy/forum/forum';
|
||||
import React from 'react'
|
||||
import { MessageSquare, Lock, TrendingUp } from 'lucide-react'
|
||||
import { ForumCategory } from '@/proxy/forum/forum'
|
||||
|
||||
interface CategoryCardProps {
|
||||
category: ForumCategory;
|
||||
onClick: () => void;
|
||||
category: ForumCategory
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function CategoryCard({ category, onClick }: CategoryCardProps) {
|
||||
export function ForumCategoryCard({ category, onClick }: CategoryCardProps) {
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return 'Never';
|
||||
if (!dateString) return 'Never'
|
||||
|
||||
const date = new Date(dateString)
|
||||
const diffDays = Math.floor((date.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
|
||||
const now = new Date()
|
||||
|
||||
// Zaman farkını doğru hesapla
|
||||
const diffTime = date.getTime() - now.getTime()
|
||||
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
// Intl.RelativeTimeFormat doğru yönle çalışır
|
||||
return new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(diffDays, 'day')
|
||||
}
|
||||
|
||||
|
|
@ -30,13 +35,9 @@ export function CategoryCard({ category, onClick }: CategoryCardProps) {
|
|||
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||
{category.name}
|
||||
</h3>
|
||||
{category.isLocked && (
|
||||
<Lock className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
{category.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
|
||||
{category.description}
|
||||
</p>
|
||||
<p className="text-gray-600 text-sm mb-3 line-clamp-2">{category.description}</p>
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
|
|
@ -51,11 +52,9 @@ export function CategoryCard({ category, onClick }: CategoryCardProps) {
|
|||
</div>
|
||||
<div className="text-right text-sm text-gray-500 ml-4">
|
||||
<div>Last post</div>
|
||||
<div className="font-medium text-gray-700">
|
||||
{formatDate(category.lastPostDate)}
|
||||
</div>
|
||||
<div className="font-medium text-gray-700">{formatDate(category.lastPostDate)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,40 +1,57 @@
|
|||
import React from 'react';
|
||||
import { Heart, User, CheckCircle, Reply } from 'lucide-react';
|
||||
import { ForumPost } from '@/proxy/forum/forum';
|
||||
import React from 'react'
|
||||
import { Heart, User, CheckCircle, Reply } from 'lucide-react'
|
||||
import { ForumPost } from '@/proxy/forum/forum'
|
||||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
|
||||
interface PostCardProps {
|
||||
post: ForumPost;
|
||||
onLike: (postId: string, isFirst: boolean) => void;
|
||||
onReply: (postId: string) => void;
|
||||
isFirst?: boolean;
|
||||
isLiked?: boolean;
|
||||
post: ForumPost
|
||||
onLike: (postId: string, isFirst: boolean) => void
|
||||
onReply: (postId: string) => void
|
||||
isFirst?: boolean
|
||||
isLiked?: boolean
|
||||
}
|
||||
|
||||
export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = false }: PostCardProps) {
|
||||
export function ForumPostCard({
|
||||
post,
|
||||
onLike,
|
||||
onReply,
|
||||
isFirst = false,
|
||||
isLiked = false,
|
||||
}: PostCardProps) {
|
||||
const handleLike = () => {
|
||||
onLike(post.id, isFirst);
|
||||
};
|
||||
onLike(post.id, isFirst)
|
||||
}
|
||||
|
||||
const formatDate = (value: string | Date) => {
|
||||
const date = value instanceof Date ? value : new Date(value)
|
||||
if (isNaN(date.getTime())) return 'Invalid Date'
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(date);
|
||||
};
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${isFirst ? 'border-l-4 border-l-blue-500' : ''}`}>
|
||||
<div
|
||||
className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${isFirst ? 'border-l-4 border-l-blue-500' : ''}`}
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
|
||||
<User className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<img
|
||||
src={AVATAR_URL(post.authorId, post.tenantId)}
|
||||
onError={(e) => {
|
||||
e.currentTarget.onerror = null
|
||||
e.currentTarget.src = '/img/others/default-profile.png'
|
||||
}}
|
||||
alt="User"
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
@ -48,14 +65,14 @@ export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = fal
|
|||
</div>
|
||||
<span className="text-sm text-gray-500">{formatDate(post.creationTime)}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="prose prose-sm max-w-none mb-4">
|
||||
<p className="text-gray-700 whitespace-pre-wrap">{post.content}</p>
|
||||
<p className="text-gray-700 whitespace-pre-wrap" dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => onLike(post.id, isFirst)}
|
||||
onClick={() => onLike(post.id, isFirst)}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-sm transition-colors ${
|
||||
isLiked
|
||||
? 'bg-red-100 text-red-600 hover:bg-red-200'
|
||||
|
|
@ -65,7 +82,7 @@ export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = fal
|
|||
<Heart className={`w-4 h-4 ${isLiked ? 'fill-current' : ''}`} />
|
||||
<span>{post.likeCount}</span>
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => onReply(post.id)}
|
||||
className="flex items-center space-x-1 px-3 py-1 rounded-full text-sm bg-gray-100 text-gray-600 hover:bg-gray-200 transition-colors"
|
||||
|
|
@ -77,5 +94,5 @@ export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = fal
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
91
ui/src/views/forum/forum/ForumTopicCard.tsx
Normal file
91
ui/src/views/forum/forum/ForumTopicCard.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react'
|
||||
import { MessageSquare, Heart, Eye, Pin, Lock, CheckCircle } from 'lucide-react'
|
||||
import { ForumTopic } from '@/proxy/forum/forum'
|
||||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
|
||||
interface TopicCardProps {
|
||||
topic: ForumTopic
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function ForumTopicCard({ topic, onClick }: TopicCardProps) {
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return 'Never'
|
||||
|
||||
const date = new Date(dateString)
|
||||
const now = new Date()
|
||||
|
||||
// Zaman farkını doğru hesapla
|
||||
const diffTime = date.getTime() - now.getTime()
|
||||
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
// Intl.RelativeTimeFormat doğru yönle çalışır
|
||||
return new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(diffDays, 'day')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md hover:border-blue-200 transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Sol taraf: Başlık, içerik, istatistik */}
|
||||
<div className="flex-1 min-w-0 pr-4">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
{topic.isPinned && <Pin className="w-4 h-4 text-orange-500" />}
|
||||
{topic.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
|
||||
{topic.isSolved && <CheckCircle className="w-4 h-4 text-emerald-500" />}
|
||||
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-1">
|
||||
{topic.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-4 line-clamp-2">{topic.content}</p>
|
||||
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1" title="Views">
|
||||
<Eye className="w-4 h-4" />
|
||||
<span>{topic.viewCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1" title="Replies">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>{topic.replyCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1" title="Likes">
|
||||
<Heart className="w-4 h-4" />
|
||||
<span>{topic.likeCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sağ taraf: Avatar + Yazar bilgisi */}
|
||||
<div className="flex flex-col items-center justify-start w-24 text-center space-y-1">
|
||||
<img
|
||||
src={AVATAR_URL(topic.authorId, topic.tenantId)}
|
||||
onError={(e) => {
|
||||
e.currentTarget.onerror = null
|
||||
e.currentTarget.src = '/img/others/default-profile.png'
|
||||
}}
|
||||
alt="User"
|
||||
className="w-10 h-10 rounded-full border"
|
||||
/>
|
||||
<div className="text-sm font-medium text-gray-700">{topic.authorName}</div>
|
||||
<div className="text-xs text-gray-500">{formatDate(topic.creationTime)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{topic.lastPostDate && topic.lastPostUserName && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<span>
|
||||
Last reply by{' '}
|
||||
<span className="font-medium text-gray-700">{topic.lastPostUserName}</span>
|
||||
{' '}
|
||||
<span>{formatDate(topic.lastPostDate)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import React, { useState } from 'react'
|
||||
import { ArrowLeft, Plus, Loader2, Search } from 'lucide-react'
|
||||
import { CategoryCard } from './CategoryCard'
|
||||
import { TopicCard } from './TopicCard'
|
||||
import { PostCard } from './PostCard'
|
||||
import { CreateTopicModal } from './CreateTopicModal'
|
||||
import { CreatePostModal } from './CreatePostModal'
|
||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||
import { SearchModal } from './SearchModal'
|
||||
import { forumService } from '@/services/forumService'
|
||||
import { buildPostTree } from './utils'
|
||||
import { ForumPostCard } from './ForumPostCard'
|
||||
import { ForumCategoryCard } from './ForumCategoryCard'
|
||||
import { ForumTopicCard } from './ForumTopicCard'
|
||||
|
||||
interface ForumViewProps {
|
||||
categories: ForumCategory[]
|
||||
|
|
@ -67,6 +67,7 @@ export function ForumView({
|
|||
const [replyToPostId, setReplyToPostId] = useState<string | undefined>()
|
||||
|
||||
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false)
|
||||
const [postLikeCounts, setPostLikeCounts] = useState<Record<string, number>>({})
|
||||
|
||||
const handleSearchCategorySelect = (category: ForumCategory) => {
|
||||
if (onCategorySelect) onCategorySelect(category)
|
||||
|
|
@ -182,25 +183,6 @@ export function ForumView({
|
|||
}
|
||||
}
|
||||
|
||||
// const handleBreadcrumbClick = (target: 'forum' | 'category') => {
|
||||
// if (target === 'forum') {
|
||||
// if (onViewStateChange) {
|
||||
// onViewStateChange('categories')
|
||||
// } else {
|
||||
// setLocalViewState('categories')
|
||||
// setLocalSelectedCategory(null)
|
||||
// setLocalSelectedTopic(null)
|
||||
// }
|
||||
// } else if (target === 'category' && selectedCategory) {
|
||||
// if (onViewStateChange) {
|
||||
// onViewStateChange('topics')
|
||||
// } else {
|
||||
// setLocalViewState('topics')
|
||||
// setLocalSelectedTopic(null)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const filteredTopics = selectedCategory
|
||||
? topics.filter((topic) => topic.categoryId === selectedCategory.id)
|
||||
: []
|
||||
|
|
@ -247,14 +229,14 @@ export function ForumView({
|
|||
function renderPosts(posts: (ForumPost & { children: ForumPost[] })[]) {
|
||||
return posts.map((post) => (
|
||||
<div key={post.id}>
|
||||
<PostCard
|
||||
<ForumPostCard
|
||||
post={post}
|
||||
onLike={handleLike}
|
||||
onReply={handleReply}
|
||||
isLiked={likedPosts.has(post.id)}
|
||||
/>
|
||||
{post.children.length > 0 && (
|
||||
<div className="pl-6 border-l border-gray-200 mt-4">{renderPosts(post.children)}</div>
|
||||
<div className="pl-6 border-gray-200 mt-4">{renderPosts(post.children)}</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
|
|
@ -262,24 +244,40 @@ export function ForumView({
|
|||
|
||||
const handleLike = async (postId: string, isFirst: boolean = false) => {
|
||||
try {
|
||||
if (likedPosts.has(postId)) {
|
||||
isFirst ? await forumService.unlikeTopic(postId) : await onUnlikePost(postId)
|
||||
const isLiked = likedPosts.has(postId)
|
||||
|
||||
setLikedPosts((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
newSet.delete(postId)
|
||||
return newSet
|
||||
})
|
||||
if (isLiked) {
|
||||
isFirst ? await forumService.unlikeTopic(postId) : await onUnlikePost(postId)
|
||||
} else {
|
||||
isFirst ? await forumService.likeTopic(postId) : await onLikePost(postId)
|
||||
}
|
||||
|
||||
setLikedPosts((prev) => new Set(prev).add(postId))
|
||||
// 🔁 toggle liked state
|
||||
setLikedPosts((prev) => {
|
||||
const updated = new Set(prev)
|
||||
if (isLiked) updated.delete(postId)
|
||||
else updated.add(postId)
|
||||
return updated
|
||||
})
|
||||
|
||||
if (selectedTopic?.id === postId) {
|
||||
setPostLikeCounts((prev) => ({
|
||||
...prev,
|
||||
[postId]: (prev[postId] ?? selectedTopic.likeCount) + (isLiked ? -1 : 1),
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error liking/unliking post or topic:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 🧠 Helper function to read initial like count
|
||||
const getInitialLikeCount = (postId: string) => {
|
||||
if (selectedTopic?.id === postId) return selectedTopic.likeCount
|
||||
const post = posts.find((p) => p.id === postId)
|
||||
return post?.likeCount ?? 0
|
||||
}
|
||||
|
||||
const handleReply = (postId: string) => {
|
||||
setReplyToPostId(postId)
|
||||
setShowCreatePost(true)
|
||||
|
|
@ -300,7 +298,7 @@ export function ForumView({
|
|||
<>
|
||||
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Breadcrumb + Actions + Search Row */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
{/* Left Side: Breadcrumb */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{viewState !== 'categories' && (
|
||||
|
|
@ -398,7 +396,7 @@ export function ForumView({
|
|||
.filter((cat) => cat.isActive)
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
.map((category) => (
|
||||
<CategoryCard
|
||||
<ForumCategoryCard
|
||||
key={category.id}
|
||||
category={category}
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
|
|
@ -422,7 +420,7 @@ export function ForumView({
|
|||
return new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime()
|
||||
})
|
||||
.map((topic) => (
|
||||
<TopicCard
|
||||
<ForumTopicCard
|
||||
key={topic.id}
|
||||
topic={topic}
|
||||
onClick={() => handleTopicClick(topic)}
|
||||
|
|
@ -440,17 +438,17 @@ export function ForumView({
|
|||
<h2 className="text-2xl font-bold text-gray-900 mb-6">{selectedTopic.title}</h2>
|
||||
|
||||
{/* Topic Ana İçeriği */}
|
||||
<PostCard
|
||||
<ForumPostCard
|
||||
post={{
|
||||
id: selectedTopic.id,
|
||||
topicId: selectedTopic.id,
|
||||
content: selectedTopic.content,
|
||||
authorId: selectedTopic.authorId,
|
||||
authorName: selectedTopic.authorName,
|
||||
likeCount: selectedTopic.likeCount,
|
||||
isAcceptedAnswer: false,
|
||||
likeCount: postLikeCounts[selectedTopic.id] ?? selectedTopic.likeCount,
|
||||
isAcceptedAnswer: false,
|
||||
parentPostId: undefined,
|
||||
creationTime: selectedTopic.creationTime,
|
||||
parentPostId: undefined
|
||||
}}
|
||||
onLike={handleLike}
|
||||
onReply={handleReply}
|
||||
|
|
@ -474,7 +472,11 @@ export function ForumView({
|
|||
|
||||
{/* Create Post Modal */}
|
||||
{showCreatePost && (
|
||||
<CreatePostModal onClose={() => setShowCreatePost(false)} onSubmit={handleCreatePost} />
|
||||
<CreatePostModal
|
||||
onClose={() => setShowCreatePost(false)}
|
||||
onSubmit={handleCreatePost}
|
||||
parentPostId={replyToPostId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Search, Folder, MessageSquare, FileText, User } from 'lucide-react';
|
||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum';
|
||||
import { useForumSearch } from '@/utils/hooks/useForumSearch';
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { X, Search, Folder, MessageSquare, FileText, User } from 'lucide-react'
|
||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||
import { useForumSearch } from '@/utils/hooks/useForumSearch'
|
||||
|
||||
interface SearchModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
categories: ForumCategory[];
|
||||
topics: ForumTopic[];
|
||||
posts: ForumPost[];
|
||||
onCategorySelect: (category: ForumCategory) => void;
|
||||
onTopicSelect: (topic: ForumTopic) => void;
|
||||
onPostSelect: (post: ForumPost) => void;
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
categories: ForumCategory[]
|
||||
topics: ForumTopic[]
|
||||
posts: ForumPost[]
|
||||
onCategorySelect: (category: ForumCategory) => void
|
||||
onTopicSelect: (topic: ForumTopic) => void
|
||||
onPostSelect: (post: ForumPost) => void
|
||||
}
|
||||
|
||||
export function SearchModal({
|
||||
|
|
@ -22,80 +22,86 @@ export function SearchModal({
|
|||
posts,
|
||||
onCategorySelect,
|
||||
onTopicSelect,
|
||||
onPostSelect
|
||||
onPostSelect,
|
||||
}: SearchModalProps) {
|
||||
const { searchQuery, setSearchQuery, searchResults, clearSearch, hasResults } = useForumSearch({
|
||||
categories,
|
||||
topics,
|
||||
posts
|
||||
});
|
||||
posts,
|
||||
})
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedIndex(0);
|
||||
setSelectedIndex(0)
|
||||
}
|
||||
}, [isOpen, searchResults]);
|
||||
}, [isOpen, searchResults])
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
const totalResults = searchResults.categories.length + searchResults.topics.length + searchResults.posts.length;
|
||||
|
||||
const totalResults =
|
||||
searchResults.categories.length + searchResults.topics.length + searchResults.posts.length
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setSelectedIndex(prev => (prev + 1) % totalResults);
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev + 1) % totalResults)
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setSelectedIndex(prev => (prev - 1 + totalResults) % totalResults);
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev - 1 + totalResults) % totalResults)
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSelectResult(selectedIndex);
|
||||
e.preventDefault()
|
||||
handleSelectResult(selectedIndex)
|
||||
} else if (e.key === 'Escape') {
|
||||
onClose();
|
||||
onClose()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleSelectResult = (index: number) => {
|
||||
let currentIndex = 0;
|
||||
|
||||
let currentIndex = 0
|
||||
|
||||
// Check categories
|
||||
if (index < searchResults.categories.length) {
|
||||
onCategorySelect(searchResults.categories[index]);
|
||||
onClose();
|
||||
return;
|
||||
onCategorySelect(searchResults.categories[index])
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
currentIndex += searchResults.categories.length;
|
||||
|
||||
currentIndex += searchResults.categories.length
|
||||
|
||||
// Check topics
|
||||
if (index < currentIndex + searchResults.topics.length) {
|
||||
onTopicSelect(searchResults.topics[index - currentIndex]);
|
||||
onClose();
|
||||
return;
|
||||
onTopicSelect(searchResults.topics[index - currentIndex])
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
currentIndex += searchResults.topics.length;
|
||||
|
||||
currentIndex += searchResults.topics.length
|
||||
|
||||
// Check posts
|
||||
if (index < currentIndex + searchResults.posts.length) {
|
||||
onPostSelect(searchResults.posts[index - currentIndex]);
|
||||
onClose();
|
||||
return;
|
||||
onPostSelect(searchResults.posts[index - currentIndex])
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const formatDate = (value: string | Date) => {
|
||||
const date = value instanceof Date ? value : new Date(value)
|
||||
if (isNaN(date.getTime())) return 'Invalid Date'
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
}).format(date);
|
||||
};
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
const getTopicTitle = (topicId: string) => {
|
||||
const topic = topics.find(t => t.id === topicId);
|
||||
return topic ? topic.title : 'Unknown Topic';
|
||||
};
|
||||
const topic = topics.find((t) => t.id === topicId)
|
||||
return topic ? topic.title : 'Unknown Topic'
|
||||
}
|
||||
|
||||
if (!isOpen) return null;
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-start justify-center pt-20 p-4 z-50">
|
||||
|
|
@ -141,8 +147,8 @@ export function SearchModal({
|
|||
<button
|
||||
key={`category-${category.id}`}
|
||||
onClick={() => {
|
||||
onCategorySelect(category);
|
||||
onClose();
|
||||
onCategorySelect(category)
|
||||
onClose()
|
||||
}}
|
||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
|
||||
selectedIndex === index ? 'bg-blue-50 border-r-2 border-blue-500' : ''
|
||||
|
|
@ -156,12 +162,12 @@ export function SearchModal({
|
|||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-gray-900">{category.name}</div>
|
||||
<div className="text-sm text-gray-500 line-clamp-1">{category.description}</div>
|
||||
<div className="text-sm text-gray-500 line-clamp-1">
|
||||
{category.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{category.topicCount} topics
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">{category.topicCount} topics</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -174,16 +180,18 @@ export function SearchModal({
|
|||
Topics ({searchResults.topics.length})
|
||||
</div>
|
||||
{searchResults.topics.map((topic, index) => {
|
||||
const globalIndex = searchResults.categories.length + index;
|
||||
const globalIndex = searchResults.categories.length + index
|
||||
return (
|
||||
<button
|
||||
key={`topic-${topic.id}`}
|
||||
onClick={() => {
|
||||
onTopicSelect(topic);
|
||||
onClose();
|
||||
onTopicSelect(topic)
|
||||
onClose()
|
||||
}}
|
||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
|
||||
selectedIndex === globalIndex ? 'bg-blue-50 border-r-2 border-blue-500' : ''
|
||||
selectedIndex === globalIndex
|
||||
? 'bg-blue-50 border-r-2 border-blue-500'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
|
|
@ -193,17 +201,17 @@ export function SearchModal({
|
|||
</div>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-gray-900 line-clamp-1">{topic.title}</div>
|
||||
<div className="font-medium text-gray-900 line-clamp-1">
|
||||
{topic.title}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
by {topic.authorName} • {formatDate(topic.creationTime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{topic.replyCount} replies
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">{topic.replyCount} replies</div>
|
||||
</button>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -215,16 +223,19 @@ export function SearchModal({
|
|||
Posts ({searchResults.posts.length})
|
||||
</div>
|
||||
{searchResults.posts.map((post, index) => {
|
||||
const globalIndex = searchResults.categories.length + searchResults.topics.length + index;
|
||||
const globalIndex =
|
||||
searchResults.categories.length + searchResults.topics.length + index
|
||||
return (
|
||||
<button
|
||||
key={`post-${post.id}`}
|
||||
onClick={() => {
|
||||
onPostSelect(post);
|
||||
onClose();
|
||||
onPostSelect(post)
|
||||
onClose()
|
||||
}}
|
||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
|
||||
selectedIndex === globalIndex ? 'bg-blue-50 border-r-2 border-blue-500' : ''
|
||||
selectedIndex === globalIndex
|
||||
? 'bg-blue-50 border-r-2 border-blue-500'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
|
|
@ -245,11 +256,9 @@ export function SearchModal({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{post.likeCount} likes
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">{post.likeCount} likes</div>
|
||||
</button>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -264,5 +273,5 @@ export function SearchModal({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import React from 'react'
|
||||
import { MessageSquare, Heart, Eye, Pin, Lock, CheckCircle } from 'lucide-react'
|
||||
import { ForumTopic } from '@/proxy/forum/forum'
|
||||
|
||||
interface TopicCardProps {
|
||||
topic: ForumTopic
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function TopicCard({ topic, onClick }: TopicCardProps) {
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return 'Never';
|
||||
|
||||
const date = new Date(dateString)
|
||||
const diffDays = Math.floor((date.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
return new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(diffDays, 'day')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md hover:border-blue-200 transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
{topic.isPinned && <Pin className="w-4 h-4 text-orange-500" />}
|
||||
{topic.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
|
||||
{topic.isSolved && <CheckCircle className="w-4 h-4 text-emerald-500" />}
|
||||
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-1">
|
||||
{topic.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-4 line-clamp-2">{topic.content}</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Eye className="w-4 h-4" />
|
||||
<span>{topic.viewCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>{topic.replyCount}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Heart className="w-4 h-4" />
|
||||
<span>{topic.likeCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right text-sm text-gray-500">
|
||||
<div className="font-medium text-gray-700">{topic.authorName}</div>
|
||||
<div>{formatDate(topic.creationTime)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{topic.lastPostDate && topic.lastPostUserName && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<span>
|
||||
Last reply by{' '}
|
||||
<span className="font-medium text-gray-700">{topic.lastPostUserName}</span>
|
||||
</span>
|
||||
<span>{formatDate(topic.lastPostDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -94,6 +94,16 @@ export function useForumData() {
|
|||
}
|
||||
}
|
||||
|
||||
const updateCategoryLockState = async (id: string) => {
|
||||
await forumService.updateCategoryLockState(id)
|
||||
await loadCategories() // refresh after update
|
||||
}
|
||||
|
||||
const updateCategoryActiveState = async (id: string) => {
|
||||
await forumService.updateCategoryActiveState(id)
|
||||
await loadCategories() // refresh after update
|
||||
}
|
||||
|
||||
const deleteCategory = async (id: string) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
|
@ -410,6 +420,8 @@ export function useForumData() {
|
|||
// Category operations
|
||||
createCategory,
|
||||
updateCategory,
|
||||
updateCategoryLockState,
|
||||
updateCategoryActiveState,
|
||||
deleteCategory,
|
||||
|
||||
// Topic operations
|
||||
|
|
|
|||
Loading…
Reference in a new issue