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 class BlogCategoryDto : FullAuditedEntityDto<Guid>
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Slug { get; set; }
|
public string Slug { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
@ -16,6 +17,7 @@ namespace Kurs.Platform.Blog
|
||||||
|
|
||||||
public class CreateUpdateBlogCategoryDto
|
public class CreateUpdateBlogCategoryDto
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Slug { get; set; }
|
public string Slug { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Kurs.Platform.Blog
|
||||||
{
|
{
|
||||||
public class BlogPostDto : FullAuditedEntityDto<Guid>
|
public class BlogPostDto : FullAuditedEntityDto<Guid>
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Slug { get; set; }
|
public string Slug { get; set; }
|
||||||
public string ContentTr { get; set; }
|
public string ContentTr { get; set; }
|
||||||
|
|
@ -30,7 +31,7 @@ namespace Kurs.Platform.Blog
|
||||||
|
|
||||||
public BlogPostDto()
|
public BlogPostDto()
|
||||||
{
|
{
|
||||||
Tags = new List<string>();
|
Tags = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +44,7 @@ namespace Kurs.Platform.Blog
|
||||||
|
|
||||||
public class CreateUpdateBlogPostDto
|
public class CreateUpdateBlogPostDto
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Slug { get; set; }
|
public string Slug { get; set; }
|
||||||
public string ContentTr { get; set; }
|
public string ContentTr { get; set; }
|
||||||
|
|
@ -56,12 +58,13 @@ namespace Kurs.Platform.Blog
|
||||||
|
|
||||||
public CreateUpdateBlogPostDto()
|
public CreateUpdateBlogPostDto()
|
||||||
{
|
{
|
||||||
Tags = new List<string>();
|
Tags = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BlogPostListDto : EntityDto<Guid>
|
public class BlogPostListDto : EntityDto<Guid>
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Slug { get; set; }
|
public string Slug { get; set; }
|
||||||
public string Summary { get; set; }
|
public string Summary { get; set; }
|
||||||
|
|
@ -85,7 +88,7 @@ namespace Kurs.Platform.Blog
|
||||||
|
|
||||||
public BlogPostListDto()
|
public BlogPostListDto()
|
||||||
{
|
{
|
||||||
Tags = new List<string>();
|
Tags = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,9 @@ public class ForumSearchResultDto
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category DTOs
|
// Category DTOs
|
||||||
public class ForumCategoryDto : EntityDto<Guid>
|
public class ForumCategoryDto : FullAuditedEntityDto<Guid>
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Slug { get; set; }
|
public string Slug { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
@ -45,6 +46,8 @@ public class ForumCategoryDto : EntityDto<Guid>
|
||||||
|
|
||||||
public class CreateForumCategoryDto
|
public class CreateForumCategoryDto
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(100)]
|
[StringLength(100)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
@ -82,8 +85,9 @@ public class GetCategoriesInput : PagedAndSortedResultRequestDto
|
||||||
}
|
}
|
||||||
|
|
||||||
// Topic DTOs
|
// Topic DTOs
|
||||||
public class ForumTopicDto : EntityDto<Guid>
|
public class ForumTopicDto : FullAuditedEntityDto<Guid>
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Content { get; set; }
|
public string Content { get; set; }
|
||||||
public Guid CategoryId { get; set; }
|
public Guid CategoryId { get; set; }
|
||||||
|
|
@ -103,6 +107,8 @@ public class ForumTopicDto : EntityDto<Guid>
|
||||||
|
|
||||||
public class CreateForumTopicDto
|
public class CreateForumTopicDto
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(200)]
|
[StringLength(200)]
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
@ -119,6 +125,8 @@ public class CreateForumTopicDto
|
||||||
|
|
||||||
public class UpdateForumTopicDto
|
public class UpdateForumTopicDto
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(200)]
|
[StringLength(200)]
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
@ -140,8 +148,9 @@ public class GetTopicsInput : PagedAndSortedResultRequestDto
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post DTOs
|
// Post DTOs
|
||||||
public class ForumPostDto : EntityDto<Guid>
|
public class ForumPostDto : FullAuditedEntityDto<Guid>
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
public Guid TopicId { get; set; }
|
public Guid TopicId { get; set; }
|
||||||
public string Content { get; set; }
|
public string Content { get; set; }
|
||||||
public Guid AuthorId { get; set; }
|
public Guid AuthorId { get; set; }
|
||||||
|
|
@ -154,6 +163,8 @@ public class ForumPostDto : EntityDto<Guid>
|
||||||
|
|
||||||
public class CreateForumPostDto
|
public class CreateForumPostDto
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public Guid TopicId { get; set; }
|
public Guid TopicId { get; set; }
|
||||||
|
|
||||||
|
|
@ -165,6 +176,8 @@ public class CreateForumPostDto
|
||||||
|
|
||||||
public class UpdateForumPostDto
|
public class UpdateForumPostDto
|
||||||
{
|
{
|
||||||
|
public Guid? TenantId { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public string Content { get; set; }
|
public string Content { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Kurs.Platform.Entities;
|
||||||
using Kurs.Platform.Localization;
|
using Kurs.Platform.Localization;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
|
|
@ -149,7 +150,7 @@ namespace Kurs.Platform.Blog
|
||||||
{
|
{
|
||||||
var post = await _postRepository.GetAsync(id);
|
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();
|
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +187,7 @@ namespace Kurs.Platform.Blog
|
||||||
var post = await _postRepository.GetAsync(id);
|
var post = await _postRepository.GetAsync(id);
|
||||||
|
|
||||||
// Check if user is author or has permission
|
// 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();
|
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +205,7 @@ namespace Kurs.Platform.Blog
|
||||||
var post = await _postRepository.GetAsync(id);
|
var post = await _postRepository.GetAsync(id);
|
||||||
|
|
||||||
// Check if user is author or has permission
|
// 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();
|
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +221,7 @@ namespace Kurs.Platform.Blog
|
||||||
var post = await _postRepository.GetAsync(id);
|
var post = await _postRepository.GetAsync(id);
|
||||||
|
|
||||||
// Check if user is author or has permission
|
// 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();
|
throw new Volo.Abp.Authorization.AbpAuthorizationException();
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +263,7 @@ namespace Kurs.Platform.Blog
|
||||||
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
|
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize("App.Blog.Create")]
|
[Authorize("App.BlogManagement.Create")]
|
||||||
public async Task<BlogCategoryDto> CreateCategoryAsync(CreateUpdateBlogCategoryDto input)
|
public async Task<BlogCategoryDto> CreateCategoryAsync(CreateUpdateBlogCategoryDto input)
|
||||||
{
|
{
|
||||||
var category = new BlogCategory(
|
var category = new BlogCategory(
|
||||||
|
|
@ -282,7 +283,7 @@ namespace Kurs.Platform.Blog
|
||||||
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
|
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize("App.Blog.Update")]
|
[Authorize("App.BlogManagement.Update")]
|
||||||
public async Task<BlogCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateBlogCategoryDto input)
|
public async Task<BlogCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateBlogCategoryDto input)
|
||||||
{
|
{
|
||||||
var category = await _categoryRepository.GetAsync(id);
|
var category = await _categoryRepository.GetAsync(id);
|
||||||
|
|
@ -299,7 +300,7 @@ namespace Kurs.Platform.Blog
|
||||||
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
|
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize("App.Blog.Delete")]
|
[Authorize("App.BlogManagement.Delete")]
|
||||||
public async Task DeleteCategoryAsync(Guid id)
|
public async Task DeleteCategoryAsync(Guid id)
|
||||||
{
|
{
|
||||||
// Check if category has posts
|
// Check if category has posts
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Kurs.Platform.Blog;
|
using Kurs.Platform.Blog;
|
||||||
|
using Kurs.Platform.Entities;
|
||||||
|
|
||||||
namespace Kurs.Platform;
|
namespace Kurs.Platform;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Authorization;
|
using Volo.Abp.Authorization;
|
||||||
using Volo.Abp.Domain.Entities;
|
using Volo.Abp.Domain.Entities;
|
||||||
|
|
@ -186,6 +187,24 @@ public class ForumAppService : PlatformAppService, IForumAppService
|
||||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
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")]
|
[Authorize("App.ForumManagement.Delete")]
|
||||||
public async Task DeleteCategoryAsync(Guid id)
|
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.Languages.EntityFrameworkCore;
|
||||||
using Kurs.Platform.Entities;
|
using Kurs.Platform.Entities;
|
||||||
using Kurs.Platform.Blog;
|
|
||||||
using Kurs.Settings.EntityFrameworkCore;
|
using Kurs.Settings.EntityFrameworkCore;
|
||||||
using Kurs.MailQueue.EntityFrameworkCore;
|
using Kurs.MailQueue.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
||||||
namespace Kurs.Platform.Migrations
|
namespace Kurs.Platform.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PlatformDbContext))]
|
[DbContext(typeof(PlatformDbContext))]
|
||||||
[Migration("20250623140407_AddForum")]
|
[Migration("20250623190205_AddForum")]
|
||||||
partial class AddForum
|
partial class AddForum
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -653,193 +653,6 @@ namespace Kurs.Platform.Migrations
|
||||||
b.ToTable("PNotificationRule", (string)null);
|
b.ToTable("PNotificationRule", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.HasColumnType("uniqueidentifier");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreationTime")
|
|
||||||
.HasColumnType("datetime2")
|
|
||||||
.HasColumnName("CreationTime");
|
|
||||||
|
|
||||||
b.Property<Guid?>("CreatorId")
|
|
||||||
.HasColumnType("uniqueidentifier")
|
|
||||||
.HasColumnName("CreatorId");
|
|
||||||
|
|
||||||
b.Property<Guid?>("DeleterId")
|
|
||||||
.HasColumnType("uniqueidentifier")
|
|
||||||
.HasColumnName("DeleterId");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletionTime")
|
|
||||||
.HasColumnType("datetime2")
|
|
||||||
.HasColumnName("DeletionTime");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("nvarchar(512)");
|
|
||||||
|
|
||||||
b.Property<int>("DisplayOrder")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("Icon")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("bit")
|
|
||||||
.HasDefaultValue(false)
|
|
||||||
.HasColumnName("IsDeleted");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("LastModificationTime")
|
|
||||||
.HasColumnType("datetime2")
|
|
||||||
.HasColumnName("LastModificationTime");
|
|
||||||
|
|
||||||
b.Property<Guid?>("LastModifierId")
|
|
||||||
.HasColumnType("uniqueidentifier")
|
|
||||||
.HasColumnName("LastModifierId");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("nvarchar(128)");
|
|
||||||
|
|
||||||
b.Property<int>("PostCount")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("nvarchar(128)");
|
|
||||||
|
|
||||||
b.Property<Guid?>("TenantId")
|
|
||||||
.HasColumnType("uniqueidentifier")
|
|
||||||
.HasColumnName("TenantId");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Slug");
|
|
||||||
|
|
||||||
b.ToTable("PBlogCategories", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Kurs.Platform.Blog.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 =>
|
modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|
@ -1071,6 +884,193 @@ namespace Kurs.Platform.Migrations
|
||||||
b.ToTable("BankAccounts");
|
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 =>
|
modelBuilder.Entity("Kurs.Platform.Entities.Branch", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|
@ -2514,6 +2514,10 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Property<string>("Slug")
|
b.Property<string>("Slug")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TenantId")
|
||||||
|
.HasColumnType("uniqueidentifier")
|
||||||
|
.HasColumnName("TenantId");
|
||||||
|
|
||||||
b.Property<int>("TopicCount")
|
b.Property<int>("TopicCount")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
|
@ -2578,6 +2582,10 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Property<Guid?>("ParentPostId")
|
b.Property<Guid?>("ParentPostId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TenantId")
|
||||||
|
.HasColumnType("uniqueidentifier")
|
||||||
|
.HasColumnName("TenantId");
|
||||||
|
|
||||||
b.Property<Guid>("TopicId")
|
b.Property<Guid>("TopicId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
|
@ -2665,6 +2673,10 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Property<int>("ReplyCount")
|
b.Property<int>("ReplyCount")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TenantId")
|
||||||
|
.HasColumnType("uniqueidentifier")
|
||||||
|
.HasColumnName("TenantId");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
|
|
@ -4757,17 +4769,6 @@ namespace Kurs.Platform.Migrations
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.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 =>
|
modelBuilder.Entity("Kurs.Platform.Entities.BankAccount", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
|
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
|
||||||
|
|
@ -4785,6 +4786,17 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Navigation("Currency");
|
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 =>
|
modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
|
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
|
||||||
|
|
@ -5027,7 +5039,7 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Navigation("Texts");
|
b.Navigation("Texts");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
|
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Posts");
|
b.Navigation("Posts");
|
||||||
});
|
});
|
||||||
|
|
@ -29,6 +29,7 @@ namespace Kurs.Platform.Migrations
|
||||||
LastPostDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
LastPostDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
LastPostUserName = table.Column<string>(type: "nvarchar(max)", 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),
|
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", 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),
|
LastPostDate = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
LastPostUserName = table.Column<string>(type: "nvarchar(max)", 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),
|
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", 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),
|
LikeCount = table.Column<int>(type: "int", nullable: false),
|
||||||
IsAcceptedAnswer = table.Column<bool>(type: "bit", nullable: false),
|
IsAcceptedAnswer = table.Column<bool>(type: "bit", nullable: false),
|
||||||
ParentPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
ParentPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
|
@ -650,193 +650,6 @@ namespace Kurs.Platform.Migrations
|
||||||
b.ToTable("PNotificationRule", (string)null);
|
b.ToTable("PNotificationRule", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.HasColumnType("uniqueidentifier");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreationTime")
|
|
||||||
.HasColumnType("datetime2")
|
|
||||||
.HasColumnName("CreationTime");
|
|
||||||
|
|
||||||
b.Property<Guid?>("CreatorId")
|
|
||||||
.HasColumnType("uniqueidentifier")
|
|
||||||
.HasColumnName("CreatorId");
|
|
||||||
|
|
||||||
b.Property<Guid?>("DeleterId")
|
|
||||||
.HasColumnType("uniqueidentifier")
|
|
||||||
.HasColumnName("DeleterId");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletionTime")
|
|
||||||
.HasColumnType("datetime2")
|
|
||||||
.HasColumnName("DeletionTime");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.HasMaxLength(512)
|
|
||||||
.HasColumnType("nvarchar(512)");
|
|
||||||
|
|
||||||
b.Property<int>("DisplayOrder")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("Icon")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("bit")
|
|
||||||
.HasDefaultValue(false)
|
|
||||||
.HasColumnName("IsDeleted");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("LastModificationTime")
|
|
||||||
.HasColumnType("datetime2")
|
|
||||||
.HasColumnName("LastModificationTime");
|
|
||||||
|
|
||||||
b.Property<Guid?>("LastModifierId")
|
|
||||||
.HasColumnType("uniqueidentifier")
|
|
||||||
.HasColumnName("LastModifierId");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("nvarchar(128)");
|
|
||||||
|
|
||||||
b.Property<int>("PostCount")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("nvarchar(128)");
|
|
||||||
|
|
||||||
b.Property<Guid?>("TenantId")
|
|
||||||
.HasColumnType("uniqueidentifier")
|
|
||||||
.HasColumnName("TenantId");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Slug");
|
|
||||||
|
|
||||||
b.ToTable("PBlogCategories", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Kurs.Platform.Blog.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 =>
|
modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|
@ -1068,6 +881,193 @@ namespace Kurs.Platform.Migrations
|
||||||
b.ToTable("BankAccounts");
|
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 =>
|
modelBuilder.Entity("Kurs.Platform.Entities.Branch", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
|
@ -2511,6 +2511,10 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Property<string>("Slug")
|
b.Property<string>("Slug")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TenantId")
|
||||||
|
.HasColumnType("uniqueidentifier")
|
||||||
|
.HasColumnName("TenantId");
|
||||||
|
|
||||||
b.Property<int>("TopicCount")
|
b.Property<int>("TopicCount")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
|
@ -2575,6 +2579,10 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Property<Guid?>("ParentPostId")
|
b.Property<Guid?>("ParentPostId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TenantId")
|
||||||
|
.HasColumnType("uniqueidentifier")
|
||||||
|
.HasColumnName("TenantId");
|
||||||
|
|
||||||
b.Property<Guid>("TopicId")
|
b.Property<Guid>("TopicId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
|
@ -2662,6 +2670,10 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Property<int>("ReplyCount")
|
b.Property<int>("ReplyCount")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TenantId")
|
||||||
|
.HasColumnType("uniqueidentifier")
|
||||||
|
.HasColumnName("TenantId");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
|
|
@ -4754,17 +4766,6 @@ namespace Kurs.Platform.Migrations
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.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 =>
|
modelBuilder.Entity("Kurs.Platform.Entities.BankAccount", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
|
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
|
||||||
|
|
@ -4782,6 +4783,17 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Navigation("Currency");
|
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 =>
|
modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
|
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
|
||||||
|
|
@ -5024,7 +5036,7 @@ namespace Kurs.Platform.Migrations
|
||||||
b.Navigation("Texts");
|
b.Navigation("Texts");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
|
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Posts");
|
b.Navigation("Posts");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -39,23 +39,6 @@ export interface BlogCategory {
|
||||||
isActive: boolean;
|
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 {
|
export interface CreateBlogPostRequest {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.ouca2h9ms1"
|
"revision": "0.mvu82hb2mqg"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
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;
|
topicCount: number;
|
||||||
postCount: number;
|
postCount: number;
|
||||||
lastPostId?: string;
|
lastPostId?: string;
|
||||||
lastPostDate?: Date;
|
lastPostDate?: string;
|
||||||
lastPostUserId?: string;
|
lastPostUserId?: string;
|
||||||
creationTime: Date;
|
creationTime: string;
|
||||||
|
tenantId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ForumTopic {
|
export interface ForumTopic {
|
||||||
|
|
@ -29,10 +30,11 @@ export interface ForumTopic {
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
isSolved: boolean;
|
isSolved: boolean;
|
||||||
lastPostId?: string;
|
lastPostId?: string;
|
||||||
lastPostDate?: Date;
|
lastPostDate?: string;
|
||||||
lastPostUserId?: string;
|
lastPostUserId?: string;
|
||||||
lastPostUserName?: string;
|
lastPostUserName?: string;
|
||||||
creationTime: Date;
|
creationTime: string;
|
||||||
|
tenantId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ForumPost {
|
export interface ForumPost {
|
||||||
|
|
@ -44,7 +46,8 @@ export interface ForumPost {
|
||||||
likeCount: number;
|
likeCount: number;
|
||||||
isAcceptedAnswer: boolean;
|
isAcceptedAnswer: boolean;
|
||||||
parentPostId?: string;
|
parentPostId?: string;
|
||||||
creationTime: Date;
|
creationTime: string;
|
||||||
|
tenantId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewMode = 'forum' | 'admin';
|
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'
|
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 {
|
class BlogService {
|
||||||
async getPosts(params: BlogListParams = {}): Promise<PaginatedResponse<BlogPost>> {
|
async getPosts(params: BlogListParams = {}): Promise<PaginatedResponse<BlogPost>> {
|
||||||
const response = await apiService.fetchData<PaginatedResponse<BlogPost>>({
|
const response = await apiService.fetchData<PaginatedResponse<BlogPost>>({
|
||||||
|
|
@ -176,23 +76,6 @@ class BlogService {
|
||||||
return response.data
|
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> {
|
async deleteComment(id: string): Promise<void> {
|
||||||
await apiService.fetchData({
|
await apiService.fetchData({
|
||||||
url: `/api/app/blog/comments/${id}`,
|
url: `/api/app/blog/comments/${id}`,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export interface CreateCategoryRequest {
|
||||||
displayOrder: number
|
displayOrder: number
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
isLocked: boolean
|
isLocked: boolean
|
||||||
|
tenantId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateTopicRequest {
|
export interface CreateTopicRequest {
|
||||||
|
|
@ -43,12 +44,14 @@ export interface CreateTopicRequest {
|
||||||
categoryId: string
|
categoryId: string
|
||||||
isPinned?: boolean
|
isPinned?: boolean
|
||||||
isLocked?: boolean
|
isLocked?: boolean
|
||||||
|
tenantId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreatePostRequest {
|
export interface CreatePostRequest {
|
||||||
topicId: string
|
topicId: string
|
||||||
content: string
|
content: string
|
||||||
parentPostId?: string
|
parentPostId?: string
|
||||||
|
tenantId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoryListParams {
|
export interface CategoryListParams {
|
||||||
|
|
@ -115,7 +118,7 @@ class ForumService {
|
||||||
|
|
||||||
async createCategory(data: CreateCategoryRequest): Promise<ForumCategory> {
|
async createCategory(data: CreateCategoryRequest): Promise<ForumCategory> {
|
||||||
const response = await apiService.fetchData<ForumCategory>({
|
const response = await apiService.fetchData<ForumCategory>({
|
||||||
url: '/api/app/forum/categories',
|
url: '/api/app/forum/category',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|
@ -124,27 +127,45 @@ class ForumService {
|
||||||
|
|
||||||
async updateCategory(id: string, data: Partial<CreateCategoryRequest>): Promise<ForumCategory> {
|
async updateCategory(id: string, data: Partial<CreateCategoryRequest>): Promise<ForumCategory> {
|
||||||
const response = await apiService.fetchData<ForumCategory>({
|
const response = await apiService.fetchData<ForumCategory>({
|
||||||
url: `/api/app/forum/categories/${id}`,
|
url: `/api/app/forum/${id}/category`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
return response.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> {
|
async deleteCategory(id: string): Promise<void> {
|
||||||
await apiService.fetchData({
|
await apiService.fetchData({
|
||||||
url: `/api/app/forum/categories/${id}`,
|
url: `/api/app/forum/${id}/category`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleCategoryStatus(id: string): Promise<ForumCategory> {
|
// async toggleCategoryStatus(id: string): Promise<ForumCategory> {
|
||||||
const response = await apiService.fetchData<ForumCategory>({
|
// const response = await apiService.fetchData<ForumCategory>({
|
||||||
url: `/api/app/forum/categories/${id}/toggle-status`,
|
// url: `/api/app/forum/categories/${id}/toggle-status`,
|
||||||
method: 'POST',
|
// method: 'POST',
|
||||||
})
|
// })
|
||||||
return response.data
|
// return response.data
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Topics
|
// Topics
|
||||||
async getTopics(params: TopicListParams = {}): Promise<PaginatedResponse<ForumTopic>> {
|
async getTopics(params: TopicListParams = {}): Promise<PaginatedResponse<ForumTopic>> {
|
||||||
|
|
@ -175,7 +196,7 @@ class ForumService {
|
||||||
|
|
||||||
async updateTopic(id: string, data: Partial<CreateTopicRequest>): Promise<ForumTopic> {
|
async updateTopic(id: string, data: Partial<CreateTopicRequest>): Promise<ForumTopic> {
|
||||||
const response = await apiService.fetchData<ForumTopic>({
|
const response = await apiService.fetchData<ForumTopic>({
|
||||||
url: `/api/app/forum/topics/${id}`,
|
url: `/api/app/forum/${id}/topic`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|
@ -184,7 +205,7 @@ class ForumService {
|
||||||
|
|
||||||
async deleteTopic(id: string): Promise<void> {
|
async deleteTopic(id: string): Promise<void> {
|
||||||
await apiService.fetchData({
|
await apiService.fetchData({
|
||||||
url: `/api/app/forum/topics/${id}`,
|
url: `/api/app/forum/${id}/topic`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -336,10 +357,17 @@ class ForumService {
|
||||||
totalUsers: number
|
totalUsers: number
|
||||||
activeUsers: 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',
|
url: '/api/app/forum/stats',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,6 @@ import { HiPlus, HiPencil, HiTrash, HiEye } from 'react-icons/hi'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
blogService,
|
blogService,
|
||||||
BlogPost,
|
|
||||||
BlogCategory,
|
|
||||||
CreateUpdateBlogPostDto,
|
|
||||||
CreateUpdateBlogCategoryDto,
|
|
||||||
} from '@/services/blog.service'
|
} from '@/services/blog.service'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { tr } from 'date-fns/locale'
|
import { tr } from 'date-fns/locale'
|
||||||
|
|
@ -39,6 +35,7 @@ import { useStoreState } from '@/store/store'
|
||||||
import TabList from '@/components/ui/Tabs/TabList'
|
import TabList from '@/components/ui/Tabs/TabList'
|
||||||
import TabNav from '@/components/ui/Tabs/TabNav'
|
import TabNav from '@/components/ui/Tabs/TabNav'
|
||||||
import TabContent from '@/components/ui/Tabs/TabContent'
|
import TabContent from '@/components/ui/Tabs/TabContent'
|
||||||
|
import { BlogCategory, BlogPost, CreateUpdateBlogCategoryDto, CreateUpdateBlogPostDto } from '@/proxy/blog/blog'
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
title: Yup.string().required(),
|
title: Yup.string().required(),
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ export function Management() {
|
||||||
error,
|
error,
|
||||||
createCategory,
|
createCategory,
|
||||||
updateCategory,
|
updateCategory,
|
||||||
|
updateCategoryLockState,
|
||||||
|
updateCategoryActiveState,
|
||||||
deleteCategory,
|
deleteCategory,
|
||||||
createTopic,
|
createTopic,
|
||||||
updateTopic,
|
updateTopic,
|
||||||
|
|
@ -79,6 +81,8 @@ export function Management() {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onCreateCategory={(data) => createCategory(data).then(() => {})}
|
onCreateCategory={(data) => createCategory(data).then(() => {})}
|
||||||
onUpdateCategory={(id, data) => updateCategory(id, data).then(() => {})}
|
onUpdateCategory={(id, data) => updateCategory(id, data).then(() => {})}
|
||||||
|
onUpdateCategoryLockState={(id) => updateCategoryLockState(id).then(() => {})}
|
||||||
|
onUpdateCategoryActiveState={(id) => updateCategoryActiveState(id).then(() => {})}
|
||||||
onDeleteCategory={(id) => deleteCategory(id).then(() => {})}
|
onDeleteCategory={(id) => deleteCategory(id).then(() => {})}
|
||||||
onCreateTopic={(data) => createTopic(data).then(() => {})}
|
onCreateTopic={(data) => createTopic(data).then(() => {})}
|
||||||
onUpdateTopic={(id, data) => updateTopic(id, data).then(() => {})}
|
onUpdateTopic={(id, data) => updateTopic(id, data).then(() => {})}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ interface AdminViewProps {
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => 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>;
|
onDeleteCategory: (id: string) => Promise<void>;
|
||||||
onCreateTopic: (topic: {
|
onCreateTopic: (topic: {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -58,6 +60,8 @@ export function AdminView({
|
||||||
onCreateCategory,
|
onCreateCategory,
|
||||||
onUpdateCategory,
|
onUpdateCategory,
|
||||||
onDeleteCategory,
|
onDeleteCategory,
|
||||||
|
onUpdateCategoryLockState,
|
||||||
|
onUpdateCategoryActiveState,
|
||||||
onCreateTopic,
|
onCreateTopic,
|
||||||
onUpdateTopic,
|
onUpdateTopic,
|
||||||
onDeleteTopic,
|
onDeleteTopic,
|
||||||
|
|
@ -121,6 +125,8 @@ export function AdminView({
|
||||||
onCreateCategory={onCreateCategory}
|
onCreateCategory={onCreateCategory}
|
||||||
onUpdateCategory={onUpdateCategory}
|
onUpdateCategory={onUpdateCategory}
|
||||||
onDeleteCategory={onDeleteCategory}
|
onDeleteCategory={onDeleteCategory}
|
||||||
|
onUpdateCategoryLockState={onUpdateCategoryLockState}
|
||||||
|
onUpdateCategoryActiveState={onUpdateCategoryActiveState}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ interface CategoryManagementProps {
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<void>;
|
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<void>;
|
||||||
onDeleteCategory: (id: string) => Promise<void>;
|
onDeleteCategory: (id: string) => Promise<void>;
|
||||||
|
onUpdateCategoryLockState: (id: string) => Promise<void>;
|
||||||
|
onUpdateCategoryActiveState: (id: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategoryManagement({
|
export function CategoryManagement({
|
||||||
|
|
@ -23,7 +25,9 @@ export function CategoryManagement({
|
||||||
loading,
|
loading,
|
||||||
onCreateCategory,
|
onCreateCategory,
|
||||||
onUpdateCategory,
|
onUpdateCategory,
|
||||||
onDeleteCategory
|
onDeleteCategory,
|
||||||
|
onUpdateCategoryLockState,
|
||||||
|
onUpdateCategoryActiveState
|
||||||
}: CategoryManagementProps) {
|
}: CategoryManagementProps) {
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||||
const [editingCategory, setEditingCategory] = useState<ForumCategory | null>(null);
|
const [editingCategory, setEditingCategory] = useState<ForumCategory | null>(null);
|
||||||
|
|
@ -87,7 +91,7 @@ export function CategoryManagement({
|
||||||
|
|
||||||
const handleToggleActive = async (category: ForumCategory) => {
|
const handleToggleActive = async (category: ForumCategory) => {
|
||||||
try {
|
try {
|
||||||
await onUpdateCategory(category.id, { isActive: !category.isActive });
|
await onUpdateCategoryActiveState(category.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling category status:', error);
|
console.error('Error toggling category status:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +99,7 @@ export function CategoryManagement({
|
||||||
|
|
||||||
const handleToggleLocked = async (category: ForumCategory) => {
|
const handleToggleLocked = async (category: ForumCategory) => {
|
||||||
try {
|
try {
|
||||||
await onUpdateCategory(category.id, { isLocked: !category.isLocked });
|
await onUpdateCategoryLockState(category.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling category lock:', error);
|
console.error('Error toggling category lock:', error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
import { Plus, Edit2, Trash2, CheckCircle, Circle, Heart, Loader2 } from 'lucide-react';
|
import { Plus, Edit2, Trash2, CheckCircle, Circle, Heart, Loader2 } from 'lucide-react'
|
||||||
import { ForumPost, ForumTopic } from '@/proxy/forum/forum';
|
import { ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||||
|
|
||||||
interface PostManagementProps {
|
interface PostManagementProps {
|
||||||
posts: ForumPost[];
|
posts: ForumPost[]
|
||||||
topics: ForumTopic[];
|
topics: ForumTopic[]
|
||||||
loading: boolean;
|
loading: boolean
|
||||||
onCreatePost: (post: {
|
onCreatePost: (post: { topicId: string; content: string; parentPostId?: string }) => Promise<void>
|
||||||
topicId: string;
|
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<void>
|
||||||
content: string;
|
onDeletePost: (id: string) => Promise<void>
|
||||||
parentPostId?: string;
|
onMarkPostAsAcceptedAnswer: (id: string) => Promise<void>
|
||||||
}) => Promise<void>;
|
onUnmarkPostAsAcceptedAnswer: (id: 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({
|
export function PostManagement({
|
||||||
|
|
@ -25,92 +21,95 @@ export function PostManagement({
|
||||||
onUpdatePost,
|
onUpdatePost,
|
||||||
onDeletePost,
|
onDeletePost,
|
||||||
onMarkPostAsAcceptedAnswer,
|
onMarkPostAsAcceptedAnswer,
|
||||||
onUnmarkPostAsAcceptedAnswer
|
onUnmarkPostAsAcceptedAnswer,
|
||||||
}: PostManagementProps) {
|
}: PostManagementProps) {
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
const [showCreateForm, setShowCreateForm] = useState(false)
|
||||||
const [editingPost, setEditingPost] = useState<ForumPost | null>(null);
|
const [editingPost, setEditingPost] = useState<ForumPost | null>(null)
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
topicId: '',
|
topicId: '',
|
||||||
content: '',
|
content: '',
|
||||||
isAcceptedAnswer: false,
|
isAcceptedAnswer: false,
|
||||||
});
|
})
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
topicId: '',
|
topicId: '',
|
||||||
content: '',
|
content: '',
|
||||||
isAcceptedAnswer: false,
|
isAcceptedAnswer: false,
|
||||||
});
|
})
|
||||||
setShowCreateForm(false);
|
setShowCreateForm(false)
|
||||||
setEditingPost(null);
|
setEditingPost(null)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
if (submitting) return;
|
if (submitting) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true)
|
||||||
if (editingPost) {
|
if (editingPost) {
|
||||||
await onUpdatePost(editingPost.id, formData);
|
await onUpdatePost(editingPost.id, formData)
|
||||||
} else {
|
} else {
|
||||||
await onCreatePost(formData);
|
await onCreatePost(formData)
|
||||||
}
|
}
|
||||||
resetForm();
|
resetForm()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error submitting form:', error);
|
console.error('Error submitting form:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (post: ForumPost) => {
|
const handleEdit = (post: ForumPost) => {
|
||||||
setEditingPost(post);
|
setEditingPost(post)
|
||||||
setFormData({
|
setFormData({
|
||||||
topicId: post.topicId,
|
topicId: post.topicId,
|
||||||
content: post.content,
|
content: post.content,
|
||||||
isAcceptedAnswer: post.isAcceptedAnswer,
|
isAcceptedAnswer: post.isAcceptedAnswer,
|
||||||
});
|
})
|
||||||
setShowCreateForm(true);
|
setShowCreateForm(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleToggleAcceptedAnswer = async (post: ForumPost) => {
|
const handleToggleAcceptedAnswer = async (post: ForumPost) => {
|
||||||
try {
|
try {
|
||||||
if (post.isAcceptedAnswer) {
|
if (post.isAcceptedAnswer) {
|
||||||
await onUnmarkPostAsAcceptedAnswer(post.id);
|
await onUnmarkPostAsAcceptedAnswer(post.id)
|
||||||
} else {
|
} else {
|
||||||
await onMarkPostAsAcceptedAnswer(post.id);
|
await onMarkPostAsAcceptedAnswer(post.id)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling accepted answer:', error);
|
console.error('Error toggling accepted answer:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
if (confirm('Are you sure you want to delete this post?')) {
|
if (confirm('Are you sure you want to delete this post?')) {
|
||||||
try {
|
try {
|
||||||
await onDeletePost(id);
|
await onDeletePost(id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting post:', error);
|
console.error('Error deleting post:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const getTopicTitle = (topicId: string) => {
|
const getTopicTitle = (topicId: string) => {
|
||||||
const topic = topics.find(t => t.id === topicId);
|
const topic = topics.find((t) => t.id === topicId)
|
||||||
return topic ? topic.title : 'Unknown Topic';
|
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', {
|
return new Intl.DateTimeFormat('en', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
}).format(date);
|
}).format(date)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -142,7 +141,7 @@ export function PostManagement({
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select a topic</option>
|
<option value="">Select a topic</option>
|
||||||
{topics.map(topic => (
|
{topics.map((topic) => (
|
||||||
<option key={topic.id} value={topic.id}>
|
<option key={topic.id} value={topic.id}>
|
||||||
{topic.title}
|
{topic.title}
|
||||||
</option>
|
</option>
|
||||||
|
|
@ -209,7 +208,9 @@ export function PostManagement({
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-200">
|
<div className="divide-y divide-gray-200">
|
||||||
{posts
|
{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) => (
|
.map((post) => (
|
||||||
<div key={post.id} className="p-6 hover:bg-gray-50 transition-colors">
|
<div key={post.id} className="p-6 hover:bg-gray-50 transition-colors">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
|
|
@ -226,7 +227,8 @@ export function PostManagement({
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<p className="text-xs text-gray-500 mb-1">
|
<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>
|
||||||
<p className="text-gray-700 line-clamp-3">{post.content}</p>
|
<p className="text-gray-700 line-clamp-3">{post.content}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -250,9 +252,17 @@ export function PostManagement({
|
||||||
? 'text-emerald-600 hover:bg-emerald-100'
|
? 'text-emerald-600 hover:bg-emerald-100'
|
||||||
: 'text-gray-400 hover:bg-gray-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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -278,5 +288,5 @@ export function PostManagement({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,26 +1,38 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
import { Plus, Edit2, Trash2, Lock, Unlock, Pin, PinOff, CheckCircle, Circle, Eye, Loader2 } from 'lucide-react';
|
import {
|
||||||
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum';
|
Plus,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
Lock,
|
||||||
|
Unlock,
|
||||||
|
Pin,
|
||||||
|
PinOff,
|
||||||
|
CheckCircle,
|
||||||
|
Circle,
|
||||||
|
Eye,
|
||||||
|
Loader2,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum'
|
||||||
|
|
||||||
interface TopicManagementProps {
|
interface TopicManagementProps {
|
||||||
topics: ForumTopic[];
|
topics: ForumTopic[]
|
||||||
categories: ForumCategory[];
|
categories: ForumCategory[]
|
||||||
loading: boolean;
|
loading: boolean
|
||||||
onCreateTopic: (topic: {
|
onCreateTopic: (topic: {
|
||||||
title: string;
|
title: string
|
||||||
content: string;
|
content: string
|
||||||
categoryId: string;
|
categoryId: string
|
||||||
isPinned?: boolean;
|
isPinned?: boolean
|
||||||
isLocked?: boolean;
|
isLocked?: boolean
|
||||||
}) => Promise<void>;
|
}) => Promise<void>
|
||||||
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<void>;
|
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<void>
|
||||||
onDeleteTopic: (id: string) => Promise<void>;
|
onDeleteTopic: (id: string) => Promise<void>
|
||||||
onPinTopic: (id: string) => Promise<void>;
|
onPinTopic: (id: string) => Promise<void>
|
||||||
onUnpinTopic: (id: string) => Promise<void>;
|
onUnpinTopic: (id: string) => Promise<void>
|
||||||
onLockTopic: (id: string) => Promise<void>;
|
onLockTopic: (id: string) => Promise<void>
|
||||||
onUnlockTopic: (id: string) => Promise<void>;
|
onUnlockTopic: (id: string) => Promise<void>
|
||||||
onMarkTopicAsSolved: (id: string) => Promise<void>;
|
onMarkTopicAsSolved: (id: string) => Promise<void>
|
||||||
onMarkTopicAsUnsolved: (id: string) => Promise<void>;
|
onMarkTopicAsUnsolved: (id: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopicManagement({
|
export function TopicManagement({
|
||||||
|
|
@ -35,10 +47,10 @@ export function TopicManagement({
|
||||||
onLockTopic,
|
onLockTopic,
|
||||||
onUnlockTopic,
|
onUnlockTopic,
|
||||||
onMarkTopicAsSolved,
|
onMarkTopicAsSolved,
|
||||||
onMarkTopicAsUnsolved
|
onMarkTopicAsUnsolved,
|
||||||
}: TopicManagementProps) {
|
}: TopicManagementProps) {
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
const [showCreateForm, setShowCreateForm] = useState(false)
|
||||||
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null);
|
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null)
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
|
|
@ -46,8 +58,8 @@ export function TopicManagement({
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
isSolved: false,
|
isSolved: false,
|
||||||
});
|
})
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|
@ -57,32 +69,32 @@ export function TopicManagement({
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
isSolved: false,
|
isSolved: false,
|
||||||
});
|
})
|
||||||
setShowCreateForm(false);
|
setShowCreateForm(false)
|
||||||
setEditingTopic(null);
|
setEditingTopic(null)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
if (submitting) return;
|
if (submitting) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true)
|
||||||
if (editingTopic) {
|
if (editingTopic) {
|
||||||
await onUpdateTopic(editingTopic.id, formData);
|
await onUpdateTopic(editingTopic.id, formData)
|
||||||
} else {
|
} else {
|
||||||
await onCreateTopic(formData);
|
await onCreateTopic(formData)
|
||||||
}
|
}
|
||||||
resetForm();
|
resetForm()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error submitting form:', error);
|
console.error('Error submitting form:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (topic: ForumTopic) => {
|
const handleEdit = (topic: ForumTopic) => {
|
||||||
setEditingTopic(topic);
|
setEditingTopic(topic)
|
||||||
setFormData({
|
setFormData({
|
||||||
title: topic.title,
|
title: topic.title,
|
||||||
content: topic.content,
|
content: topic.content,
|
||||||
|
|
@ -90,68 +102,77 @@ export function TopicManagement({
|
||||||
isPinned: topic.isPinned,
|
isPinned: topic.isPinned,
|
||||||
isLocked: topic.isLocked,
|
isLocked: topic.isLocked,
|
||||||
isSolved: topic.isSolved,
|
isSolved: topic.isSolved,
|
||||||
});
|
})
|
||||||
setShowCreateForm(true);
|
setShowCreateForm(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handlePin = async (topic: ForumTopic) => {
|
const handlePin = async (topic: ForumTopic) => {
|
||||||
try {
|
try {
|
||||||
if (topic.isPinned) {
|
if (topic.isPinned) {
|
||||||
await onUnpinTopic(topic.id);
|
await onUnpinTopic(topic.id)
|
||||||
} else {
|
} else {
|
||||||
await onPinTopic(topic.id);
|
await onPinTopic(topic.id)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling pin:', error);
|
console.error('Error toggling pin:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleLock = async (topic: ForumTopic) => {
|
const handleLock = async (topic: ForumTopic) => {
|
||||||
try {
|
try {
|
||||||
if (topic.isLocked) {
|
if (topic.isLocked) {
|
||||||
await onUnlockTopic(topic.id);
|
await onUnlockTopic(topic.id)
|
||||||
} else {
|
} else {
|
||||||
await onLockTopic(topic.id);
|
await onLockTopic(topic.id)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling lock:', error);
|
console.error('Error toggling lock:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleSolved = async (topic: ForumTopic) => {
|
const handleSolved = async (topic: ForumTopic) => {
|
||||||
try {
|
try {
|
||||||
if (topic.isSolved) {
|
if (topic.isSolved) {
|
||||||
await onMarkTopicAsUnsolved(topic.id);
|
await onMarkTopicAsUnsolved(topic.id)
|
||||||
} else {
|
} else {
|
||||||
await onMarkTopicAsSolved(topic.id);
|
await onMarkTopicAsSolved(topic.id)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling solved status:', error);
|
console.error('Error toggling solved status:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
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 {
|
try {
|
||||||
await onDeleteTopic(id);
|
await onDeleteTopic(id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting topic:', error);
|
console.error('Error deleting topic:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const getCategoryName = (categoryId: string) => {
|
const getCategoryName = (categoryId: string) => {
|
||||||
const category = categories.find(c => c.id === categoryId);
|
const category = categories.find((c) => c.id === categoryId)
|
||||||
return category ? category.name : 'Unknown Category';
|
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', {
|
return new Intl.DateTimeFormat('en', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
}).format(date);
|
hour: '2-digit',
|
||||||
};
|
minute: '2-digit',
|
||||||
|
}).format(date)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -194,7 +215,7 @@ export function TopicManagement({
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select a category</option>
|
<option value="">Select a category</option>
|
||||||
{categories.map(category => (
|
{categories.map((category) => (
|
||||||
<option key={category.id} value={category.id}>
|
<option key={category.id} value={category.id}>
|
||||||
{category.name}
|
{category.name}
|
||||||
</option>
|
</option>
|
||||||
|
|
@ -279,7 +300,9 @@ export function TopicManagement({
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-200">
|
<div className="divide-y divide-gray-200">
|
||||||
{topics
|
{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) => (
|
.map((topic) => (
|
||||||
<div key={topic.id} className="p-6 hover:bg-gray-50 transition-colors">
|
<div key={topic.id} className="p-6 hover:bg-gray-50 transition-colors">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
|
|
@ -288,7 +311,9 @@ export function TopicManagement({
|
||||||
{topic.isPinned && <Pin className="w-4 h-4 text-orange-500" />}
|
{topic.isPinned && <Pin className="w-4 h-4 text-orange-500" />}
|
||||||
{topic.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
|
{topic.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
|
||||||
{topic.isSolved && <CheckCircle className="w-4 h-4 text-emerald-500" />}
|
{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>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-600 mb-3 line-clamp-2">{topic.content}</p>
|
<p className="text-gray-600 mb-3 line-clamp-2">{topic.content}</p>
|
||||||
|
|
@ -320,7 +345,11 @@ export function TopicManagement({
|
||||||
}`}
|
}`}
|
||||||
title={topic.isPinned ? 'Unpin Topic' : 'Pin Topic'}
|
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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -332,7 +361,11 @@ export function TopicManagement({
|
||||||
}`}
|
}`}
|
||||||
title={topic.isLocked ? 'Unlock Topic' : 'Lock Topic'}
|
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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -344,7 +377,11 @@ export function TopicManagement({
|
||||||
}`}
|
}`}
|
||||||
title={topic.isSolved ? 'Mark as Unsolved' : 'Mark as Solved'}
|
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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -370,5 +407,5 @@ export function TopicManagement({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
import ReactQuill from 'react-quill';
|
||||||
|
import 'react-quill/dist/quill.snow.css';
|
||||||
|
|
||||||
interface CreatePostModalProps {
|
interface CreatePostModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (data: { content: string }) => 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 [content, setContent] = useState('');
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (content.trim()) {
|
const plainText = content.replace(/<[^>]+>/g, '').trim(); // HTML etiketlerini temizle
|
||||||
onSubmit({ content: content.trim() });
|
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="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="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">
|
<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
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
|
@ -31,18 +37,15 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Your Reply
|
{parentPostId ? 'Your Reply' : 'Message'}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<ReactQuill
|
||||||
id="content"
|
theme="snow"
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={setContent}
|
||||||
rows={6}
|
style={{ height: '400px', marginBottom: '50px' }}
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
placeholder={parentPostId ? 'Write your reply...' : 'Write your message...'}
|
||||||
placeholder="Write your reply..."
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -57,7 +60,7 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors disabled:opacity-50"
|
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
|
Post Reply
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,24 @@
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { MessageSquare, Lock, TrendingUp } from 'lucide-react';
|
import { MessageSquare, Lock, TrendingUp } from 'lucide-react'
|
||||||
import { ForumCategory } from '@/proxy/forum/forum';
|
import { ForumCategory } from '@/proxy/forum/forum'
|
||||||
|
|
||||||
interface CategoryCardProps {
|
interface CategoryCardProps {
|
||||||
category: ForumCategory;
|
category: ForumCategory
|
||||||
onClick: () => void;
|
onClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategoryCard({ category, onClick }: CategoryCardProps) {
|
export function ForumCategoryCard({ category, onClick }: CategoryCardProps) {
|
||||||
const formatDate = (dateString?: string) => {
|
const formatDate = (dateString?: string) => {
|
||||||
if (!dateString) return 'Never';
|
if (!dateString) return 'Never'
|
||||||
|
|
||||||
const date = new Date(dateString)
|
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')
|
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">
|
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||||
{category.name}
|
{category.name}
|
||||||
</h3>
|
</h3>
|
||||||
{category.isLocked && (
|
{category.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
|
||||||
<Lock className="w-4 h-4 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
|
<p className="text-gray-600 text-sm mb-3 line-clamp-2">{category.description}</p>
|
||||||
{category.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<MessageSquare className="w-4 h-4" />
|
<MessageSquare className="w-4 h-4" />
|
||||||
|
|
@ -51,11 +52,9 @@ export function CategoryCard({ category, onClick }: CategoryCardProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-sm text-gray-500 ml-4">
|
<div className="text-right text-sm text-gray-500 ml-4">
|
||||||
<div>Last post</div>
|
<div>Last post</div>
|
||||||
<div className="font-medium text-gray-700">
|
<div className="font-medium text-gray-700">{formatDate(category.lastPostDate)}</div>
|
||||||
{formatDate(category.lastPostDate)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,38 +1,55 @@
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { Heart, User, CheckCircle, Reply } from 'lucide-react';
|
import { Heart, User, CheckCircle, Reply } from 'lucide-react'
|
||||||
import { ForumPost } from '@/proxy/forum/forum';
|
import { ForumPost } from '@/proxy/forum/forum'
|
||||||
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
|
|
||||||
interface PostCardProps {
|
interface PostCardProps {
|
||||||
post: ForumPost;
|
post: ForumPost
|
||||||
onLike: (postId: string, isFirst: boolean) => void;
|
onLike: (postId: string, isFirst: boolean) => void
|
||||||
onReply: (postId: string) => void;
|
onReply: (postId: string) => void
|
||||||
isFirst?: boolean;
|
isFirst?: boolean
|
||||||
isLiked?: 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 = () => {
|
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', {
|
return new Intl.DateTimeFormat('en', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit',
|
||||||
}).format(date);
|
}).format(date)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
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 items-start space-x-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center">
|
<img
|
||||||
|
src={AVATAR_URL(post.authorId, post.tenantId)}
|
||||||
<User className="w-5 h-5 text-white" />
|
onError={(e) => {
|
||||||
</div>
|
e.currentTarget.onerror = null
|
||||||
|
e.currentTarget.src = '/img/others/default-profile.png'
|
||||||
|
}}
|
||||||
|
alt="User"
|
||||||
|
className="w-10 h-10 rounded-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|
@ -50,7 +67,7 @@ export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = fal
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="prose prose-sm max-w-none mb-4">
|
<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>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
|
@ -77,5 +94,5 @@ export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = fal
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 React, { useState } from 'react'
|
||||||
import { ArrowLeft, Plus, Loader2, Search } from 'lucide-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 { CreateTopicModal } from './CreateTopicModal'
|
||||||
import { CreatePostModal } from './CreatePostModal'
|
import { CreatePostModal } from './CreatePostModal'
|
||||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||||
import { SearchModal } from './SearchModal'
|
import { SearchModal } from './SearchModal'
|
||||||
import { forumService } from '@/services/forumService'
|
import { forumService } from '@/services/forumService'
|
||||||
import { buildPostTree } from './utils'
|
import { buildPostTree } from './utils'
|
||||||
|
import { ForumPostCard } from './ForumPostCard'
|
||||||
|
import { ForumCategoryCard } from './ForumCategoryCard'
|
||||||
|
import { ForumTopicCard } from './ForumTopicCard'
|
||||||
|
|
||||||
interface ForumViewProps {
|
interface ForumViewProps {
|
||||||
categories: ForumCategory[]
|
categories: ForumCategory[]
|
||||||
|
|
@ -67,6 +67,7 @@ export function ForumView({
|
||||||
const [replyToPostId, setReplyToPostId] = useState<string | undefined>()
|
const [replyToPostId, setReplyToPostId] = useState<string | undefined>()
|
||||||
|
|
||||||
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false)
|
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false)
|
||||||
|
const [postLikeCounts, setPostLikeCounts] = useState<Record<string, number>>({})
|
||||||
|
|
||||||
const handleSearchCategorySelect = (category: ForumCategory) => {
|
const handleSearchCategorySelect = (category: ForumCategory) => {
|
||||||
if (onCategorySelect) onCategorySelect(category)
|
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
|
const filteredTopics = selectedCategory
|
||||||
? topics.filter((topic) => topic.categoryId === selectedCategory.id)
|
? topics.filter((topic) => topic.categoryId === selectedCategory.id)
|
||||||
: []
|
: []
|
||||||
|
|
@ -247,14 +229,14 @@ export function ForumView({
|
||||||
function renderPosts(posts: (ForumPost & { children: ForumPost[] })[]) {
|
function renderPosts(posts: (ForumPost & { children: ForumPost[] })[]) {
|
||||||
return posts.map((post) => (
|
return posts.map((post) => (
|
||||||
<div key={post.id}>
|
<div key={post.id}>
|
||||||
<PostCard
|
<ForumPostCard
|
||||||
post={post}
|
post={post}
|
||||||
onLike={handleLike}
|
onLike={handleLike}
|
||||||
onReply={handleReply}
|
onReply={handleReply}
|
||||||
isLiked={likedPosts.has(post.id)}
|
isLiked={likedPosts.has(post.id)}
|
||||||
/>
|
/>
|
||||||
{post.children.length > 0 && (
|
{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>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
@ -262,24 +244,40 @@ export function ForumView({
|
||||||
|
|
||||||
const handleLike = async (postId: string, isFirst: boolean = false) => {
|
const handleLike = async (postId: string, isFirst: boolean = false) => {
|
||||||
try {
|
try {
|
||||||
if (likedPosts.has(postId)) {
|
const isLiked = likedPosts.has(postId)
|
||||||
isFirst ? await forumService.unlikeTopic(postId) : await onUnlikePost(postId)
|
|
||||||
|
|
||||||
setLikedPosts((prev) => {
|
if (isLiked) {
|
||||||
const newSet = new Set(prev)
|
isFirst ? await forumService.unlikeTopic(postId) : await onUnlikePost(postId)
|
||||||
newSet.delete(postId)
|
|
||||||
return newSet
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
isFirst ? await forumService.likeTopic(postId) : await onLikePost(postId)
|
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) {
|
} catch (error) {
|
||||||
console.error('Error liking/unliking post or topic:', 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) => {
|
const handleReply = (postId: string) => {
|
||||||
setReplyToPostId(postId)
|
setReplyToPostId(postId)
|
||||||
setShowCreatePost(true)
|
setShowCreatePost(true)
|
||||||
|
|
@ -300,7 +298,7 @@ export function ForumView({
|
||||||
<>
|
<>
|
||||||
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Breadcrumb + Actions + Search Row */}
|
{/* 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 */}
|
{/* Left Side: Breadcrumb */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{viewState !== 'categories' && (
|
{viewState !== 'categories' && (
|
||||||
|
|
@ -398,7 +396,7 @@ export function ForumView({
|
||||||
.filter((cat) => cat.isActive)
|
.filter((cat) => cat.isActive)
|
||||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||||
.map((category) => (
|
.map((category) => (
|
||||||
<CategoryCard
|
<ForumCategoryCard
|
||||||
key={category.id}
|
key={category.id}
|
||||||
category={category}
|
category={category}
|
||||||
onClick={() => handleCategoryClick(category)}
|
onClick={() => handleCategoryClick(category)}
|
||||||
|
|
@ -422,7 +420,7 @@ export function ForumView({
|
||||||
return new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime()
|
return new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime()
|
||||||
})
|
})
|
||||||
.map((topic) => (
|
.map((topic) => (
|
||||||
<TopicCard
|
<ForumTopicCard
|
||||||
key={topic.id}
|
key={topic.id}
|
||||||
topic={topic}
|
topic={topic}
|
||||||
onClick={() => handleTopicClick(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>
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">{selectedTopic.title}</h2>
|
||||||
|
|
||||||
{/* Topic Ana İçeriği */}
|
{/* Topic Ana İçeriği */}
|
||||||
<PostCard
|
<ForumPostCard
|
||||||
post={{
|
post={{
|
||||||
id: selectedTopic.id,
|
id: selectedTopic.id,
|
||||||
topicId: selectedTopic.id,
|
topicId: selectedTopic.id,
|
||||||
content: selectedTopic.content,
|
content: selectedTopic.content,
|
||||||
authorId: selectedTopic.authorId,
|
authorId: selectedTopic.authorId,
|
||||||
authorName: selectedTopic.authorName,
|
authorName: selectedTopic.authorName,
|
||||||
likeCount: selectedTopic.likeCount,
|
likeCount: postLikeCounts[selectedTopic.id] ?? selectedTopic.likeCount,
|
||||||
isAcceptedAnswer: false,
|
isAcceptedAnswer: false,
|
||||||
|
parentPostId: undefined,
|
||||||
creationTime: selectedTopic.creationTime,
|
creationTime: selectedTopic.creationTime,
|
||||||
parentPostId: undefined
|
|
||||||
}}
|
}}
|
||||||
onLike={handleLike}
|
onLike={handleLike}
|
||||||
onReply={handleReply}
|
onReply={handleReply}
|
||||||
|
|
@ -474,7 +472,11 @@ export function ForumView({
|
||||||
|
|
||||||
{/* Create Post Modal */}
|
{/* Create Post Modal */}
|
||||||
{showCreatePost && (
|
{showCreatePost && (
|
||||||
<CreatePostModal onClose={() => setShowCreatePost(false)} onSubmit={handleCreatePost} />
|
<CreatePostModal
|
||||||
|
onClose={() => setShowCreatePost(false)}
|
||||||
|
onSubmit={handleCreatePost}
|
||||||
|
parentPostId={replyToPostId}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react'
|
||||||
import { X, Search, Folder, MessageSquare, FileText, User } from 'lucide-react';
|
import { X, Search, Folder, MessageSquare, FileText, User } from 'lucide-react'
|
||||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum';
|
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||||
import { useForumSearch } from '@/utils/hooks/useForumSearch';
|
import { useForumSearch } from '@/utils/hooks/useForumSearch'
|
||||||
|
|
||||||
interface SearchModalProps {
|
interface SearchModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
categories: ForumCategory[];
|
categories: ForumCategory[]
|
||||||
topics: ForumTopic[];
|
topics: ForumTopic[]
|
||||||
posts: ForumPost[];
|
posts: ForumPost[]
|
||||||
onCategorySelect: (category: ForumCategory) => void;
|
onCategorySelect: (category: ForumCategory) => void
|
||||||
onTopicSelect: (topic: ForumTopic) => void;
|
onTopicSelect: (topic: ForumTopic) => void
|
||||||
onPostSelect: (post: ForumPost) => void;
|
onPostSelect: (post: ForumPost) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchModal({
|
export function SearchModal({
|
||||||
|
|
@ -22,80 +22,86 @@ export function SearchModal({
|
||||||
posts,
|
posts,
|
||||||
onCategorySelect,
|
onCategorySelect,
|
||||||
onTopicSelect,
|
onTopicSelect,
|
||||||
onPostSelect
|
onPostSelect,
|
||||||
}: SearchModalProps) {
|
}: SearchModalProps) {
|
||||||
const { searchQuery, setSearchQuery, searchResults, clearSearch, hasResults } = useForumSearch({
|
const { searchQuery, setSearchQuery, searchResults, clearSearch, hasResults } = useForumSearch({
|
||||||
categories,
|
categories,
|
||||||
topics,
|
topics,
|
||||||
posts
|
posts,
|
||||||
});
|
})
|
||||||
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
setSelectedIndex(0);
|
setSelectedIndex(0)
|
||||||
}
|
}
|
||||||
}, [isOpen, searchResults]);
|
}, [isOpen, searchResults])
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
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') {
|
if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setSelectedIndex(prev => (prev + 1) % totalResults);
|
setSelectedIndex((prev) => (prev + 1) % totalResults)
|
||||||
} else if (e.key === 'ArrowUp') {
|
} else if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setSelectedIndex(prev => (prev - 1 + totalResults) % totalResults);
|
setSelectedIndex((prev) => (prev - 1 + totalResults) % totalResults)
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
handleSelectResult(selectedIndex);
|
handleSelectResult(selectedIndex)
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
onClose();
|
onClose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectResult = (index: number) => {
|
const handleSelectResult = (index: number) => {
|
||||||
let currentIndex = 0;
|
let currentIndex = 0
|
||||||
|
|
||||||
// Check categories
|
// Check categories
|
||||||
if (index < searchResults.categories.length) {
|
if (index < searchResults.categories.length) {
|
||||||
onCategorySelect(searchResults.categories[index]);
|
onCategorySelect(searchResults.categories[index])
|
||||||
onClose();
|
onClose()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
currentIndex += searchResults.categories.length;
|
currentIndex += searchResults.categories.length
|
||||||
|
|
||||||
// Check topics
|
// Check topics
|
||||||
if (index < currentIndex + searchResults.topics.length) {
|
if (index < currentIndex + searchResults.topics.length) {
|
||||||
onTopicSelect(searchResults.topics[index - currentIndex]);
|
onTopicSelect(searchResults.topics[index - currentIndex])
|
||||||
onClose();
|
onClose()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
currentIndex += searchResults.topics.length;
|
currentIndex += searchResults.topics.length
|
||||||
|
|
||||||
// Check posts
|
// Check posts
|
||||||
if (index < currentIndex + searchResults.posts.length) {
|
if (index < currentIndex + searchResults.posts.length) {
|
||||||
onPostSelect(searchResults.posts[index - currentIndex]);
|
onPostSelect(searchResults.posts[index - currentIndex])
|
||||||
onClose();
|
onClose()
|
||||||
return;
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (date: Date) => {
|
const formatDate = (value: string | Date) => {
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (isNaN(date.getTime())) return 'Invalid Date'
|
||||||
|
|
||||||
return new Intl.DateTimeFormat('en', {
|
return new Intl.DateTimeFormat('en', {
|
||||||
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric'
|
hour: '2-digit',
|
||||||
}).format(date);
|
minute: '2-digit',
|
||||||
};
|
}).format(date)
|
||||||
|
}
|
||||||
|
|
||||||
const getTopicTitle = (topicId: string) => {
|
const getTopicTitle = (topicId: string) => {
|
||||||
const topic = topics.find(t => t.id === topicId);
|
const topic = topics.find((t) => t.id === topicId)
|
||||||
return topic ? topic.title : 'Unknown Topic';
|
return topic ? topic.title : 'Unknown Topic'
|
||||||
};
|
}
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-start justify-center pt-20 p-4 z-50">
|
<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
|
<button
|
||||||
key={`category-${category.id}`}
|
key={`category-${category.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onCategorySelect(category);
|
onCategorySelect(category)
|
||||||
onClose();
|
onClose()
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
|
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' : ''
|
selectedIndex === index ? 'bg-blue-50 border-r-2 border-blue-500' : ''
|
||||||
|
|
@ -156,12 +162,12 @@ export function SearchModal({
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="font-medium text-gray-900">{category.name}</div>
|
<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>
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
{category.topicCount} topics
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">{category.topicCount} topics</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,16 +180,18 @@ export function SearchModal({
|
||||||
Topics ({searchResults.topics.length})
|
Topics ({searchResults.topics.length})
|
||||||
</div>
|
</div>
|
||||||
{searchResults.topics.map((topic, index) => {
|
{searchResults.topics.map((topic, index) => {
|
||||||
const globalIndex = searchResults.categories.length + index;
|
const globalIndex = searchResults.categories.length + index
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={`topic-${topic.id}`}
|
key={`topic-${topic.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onTopicSelect(topic);
|
onTopicSelect(topic)
|
||||||
onClose();
|
onClose()
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
|
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">
|
<div className="flex items-center space-x-3 flex-1">
|
||||||
|
|
@ -193,17 +201,17 @@ export function SearchModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<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">
|
<div className="text-sm text-gray-500">
|
||||||
by {topic.authorName} • {formatDate(topic.creationTime)}
|
by {topic.authorName} • {formatDate(topic.creationTime)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">
|
<div className="text-xs text-gray-400">{topic.replyCount} replies</div>
|
||||||
{topic.replyCount} replies
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -215,16 +223,19 @@ export function SearchModal({
|
||||||
Posts ({searchResults.posts.length})
|
Posts ({searchResults.posts.length})
|
||||||
</div>
|
</div>
|
||||||
{searchResults.posts.map((post, index) => {
|
{searchResults.posts.map((post, index) => {
|
||||||
const globalIndex = searchResults.categories.length + searchResults.topics.length + index;
|
const globalIndex =
|
||||||
|
searchResults.categories.length + searchResults.topics.length + index
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={`post-${post.id}`}
|
key={`post-${post.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onPostSelect(post);
|
onPostSelect(post)
|
||||||
onClose();
|
onClose()
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
|
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">
|
<div className="flex items-center space-x-3 flex-1">
|
||||||
|
|
@ -245,11 +256,9 @@ export function SearchModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">
|
<div className="text-xs text-gray-400">{post.likeCount} likes</div>
|
||||||
{post.likeCount} likes
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -264,5 +273,5 @@ export function SearchModal({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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) => {
|
const deleteCategory = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
@ -410,6 +420,8 @@ export function useForumData() {
|
||||||
// Category operations
|
// Category operations
|
||||||
createCategory,
|
createCategory,
|
||||||
updateCategory,
|
updateCategory,
|
||||||
|
updateCategoryLockState,
|
||||||
|
updateCategoryActiveState,
|
||||||
deleteCategory,
|
deleteCategory,
|
||||||
|
|
||||||
// Topic operations
|
// Topic operations
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue