forum management ve forum view

This commit is contained in:
Sedat Öztürk 2025-06-24 00:22:11 +03:00
parent 4d127032af
commit b0311425ce
38 changed files with 1408 additions and 1233 deletions

View file

@ -5,6 +5,7 @@ namespace Kurs.Platform.Blog
{
public class BlogCategoryDto : FullAuditedEntityDto<Guid>
{
public Guid? TenantId { get; set; }
public string Name { get; set; }
public string Slug { get; set; }
public string Description { get; set; }
@ -16,6 +17,7 @@ namespace Kurs.Platform.Blog
public class CreateUpdateBlogCategoryDto
{
public Guid? TenantId { get; set; }
public string Name { get; set; }
public string Slug { get; set; }
public string Description { get; set; }

View file

@ -6,6 +6,7 @@ namespace Kurs.Platform.Blog
{
public class BlogPostDto : FullAuditedEntityDto<Guid>
{
public Guid? TenantId { get; set; }
public string Title { get; set; }
public string Slug { get; set; }
public string ContentTr { get; set; }
@ -30,7 +31,7 @@ namespace Kurs.Platform.Blog
public BlogPostDto()
{
Tags = new List<string>();
Tags = [];
}
}
@ -43,6 +44,7 @@ namespace Kurs.Platform.Blog
public class CreateUpdateBlogPostDto
{
public Guid? TenantId { get; set; }
public string Title { get; set; }
public string Slug { get; set; }
public string ContentTr { get; set; }
@ -56,12 +58,13 @@ namespace Kurs.Platform.Blog
public CreateUpdateBlogPostDto()
{
Tags = new List<string>();
Tags = [];
}
}
public class BlogPostListDto : EntityDto<Guid>
{
public Guid? TenantId { get; set; }
public string Title { get; set; }
public string Slug { get; set; }
public string Summary { get; set; }
@ -85,7 +88,7 @@ namespace Kurs.Platform.Blog
public BlogPostListDto()
{
Tags = new List<string>();
Tags = [];
}
}
}

View file

@ -28,8 +28,9 @@ public class ForumSearchResultDto
}
// Category DTOs
public class ForumCategoryDto : EntityDto<Guid>
public class ForumCategoryDto : FullAuditedEntityDto<Guid>
{
public Guid? TenantId { get; set; }
public string Name { get; set; }
public string Slug { get; set; }
public string Description { get; set; }
@ -45,6 +46,8 @@ public class ForumCategoryDto : EntityDto<Guid>
public class CreateForumCategoryDto
{
public Guid? TenantId { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
@ -82,8 +85,9 @@ public class GetCategoriesInput : PagedAndSortedResultRequestDto
}
// Topic DTOs
public class ForumTopicDto : EntityDto<Guid>
public class ForumTopicDto : FullAuditedEntityDto<Guid>
{
public Guid? TenantId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Guid CategoryId { get; set; }
@ -103,6 +107,8 @@ public class ForumTopicDto : EntityDto<Guid>
public class CreateForumTopicDto
{
public Guid? TenantId { get; set; }
[Required]
[StringLength(200)]
public string Title { get; set; }
@ -119,6 +125,8 @@ public class CreateForumTopicDto
public class UpdateForumTopicDto
{
public Guid? TenantId { get; set; }
[Required]
[StringLength(200)]
public string Title { get; set; }
@ -140,8 +148,9 @@ public class GetTopicsInput : PagedAndSortedResultRequestDto
}
// Post DTOs
public class ForumPostDto : EntityDto<Guid>
public class ForumPostDto : FullAuditedEntityDto<Guid>
{
public Guid? TenantId { get; set; }
public Guid TopicId { get; set; }
public string Content { get; set; }
public Guid AuthorId { get; set; }
@ -154,6 +163,8 @@ public class ForumPostDto : EntityDto<Guid>
public class CreateForumPostDto
{
public Guid? TenantId { get; set; }
[Required]
public Guid TopicId { get; set; }
@ -165,6 +176,8 @@ public class CreateForumPostDto
public class UpdateForumPostDto
{
public Guid? TenantId { get; set; }
[Required]
public string Content { get; set; }

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kurs.Platform.Entities;
using Kurs.Platform.Localization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Localization;
@ -149,7 +150,7 @@ namespace Kurs.Platform.Blog
{
var post = await _postRepository.GetAsync(id);
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Update"))
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.BlogManagement.Update"))
{
throw new Volo.Abp.Authorization.AbpAuthorizationException();
}
@ -186,7 +187,7 @@ namespace Kurs.Platform.Blog
var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Delete"))
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.BlogManagement.Delete"))
{
throw new Volo.Abp.Authorization.AbpAuthorizationException();
}
@ -204,7 +205,7 @@ namespace Kurs.Platform.Blog
var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Publish"))
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.BlogManagement.Publish"))
{
throw new Volo.Abp.Authorization.AbpAuthorizationException();
}
@ -220,7 +221,7 @@ namespace Kurs.Platform.Blog
var post = await _postRepository.GetAsync(id);
// Check if user is author or has permission
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.Blog.Publish"))
if (post.AuthorId != _currentUser.Id && !await AuthorizationService.IsGrantedAsync("App.BlogManagement.Publish"))
{
throw new Volo.Abp.Authorization.AbpAuthorizationException();
}
@ -262,7 +263,7 @@ namespace Kurs.Platform.Blog
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
}
[Authorize("App.Blog.Create")]
[Authorize("App.BlogManagement.Create")]
public async Task<BlogCategoryDto> CreateCategoryAsync(CreateUpdateBlogCategoryDto input)
{
var category = new BlogCategory(
@ -282,7 +283,7 @@ namespace Kurs.Platform.Blog
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
}
[Authorize("App.Blog.Update")]
[Authorize("App.BlogManagement.Update")]
public async Task<BlogCategoryDto> UpdateCategoryAsync(Guid id, CreateUpdateBlogCategoryDto input)
{
var category = await _categoryRepository.GetAsync(id);
@ -299,7 +300,7 @@ namespace Kurs.Platform.Blog
return ObjectMapper.Map<BlogCategory, BlogCategoryDto>(category);
}
[Authorize("App.Blog.Delete")]
[Authorize("App.BlogManagement.Delete")]
public async Task DeleteCategoryAsync(Guid id)
{
// Check if category has posts

View file

@ -1,5 +1,6 @@
using AutoMapper;
using Kurs.Platform.Blog;
using Kurs.Platform.Entities;
namespace Kurs.Platform;

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Authorization;
using Volo.Abp.Domain.Entities;
@ -186,6 +187,24 @@ public class ForumAppService : PlatformAppService, IForumAppService
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
}
[Authorize("App.ForumManagement.Update")]
public async Task<ForumCategoryDto> UpdateCategoryLockAsync(Guid id)
{
var category = await _categoryRepository.GetAsync(id);
category.IsLocked = !category.IsLocked;
await _categoryRepository.UpdateAsync(category);
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
}
[Authorize("App.ForumManagement.Update")]
public async Task<ForumCategoryDto> UpdateCategoryActiveAsync(Guid id)
{
var category = await _categoryRepository.GetAsync(id);
category.IsActive = !category.IsActive;
await _categoryRepository.UpdateAsync(category);
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
}
[Authorize("App.ForumManagement.Delete")]
public async Task DeleteCategoryAsync(Guid id)
{

View file

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

View file

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

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

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

View file

@ -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 = [];
}
}

View 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 = [];
}
}

View 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 = [];
}
}

View 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 = [];
}
}

View file

@ -1,6 +1,5 @@
using Kurs.Languages.EntityFrameworkCore;
using Kurs.Platform.Entities;
using Kurs.Platform.Blog;
using Kurs.Settings.EntityFrameworkCore;
using Kurs.MailQueue.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20250623140407_AddForum")]
[Migration("20250623190205_AddForum")]
partial class AddForum
{
/// <inheritdoc />
@ -653,193 +653,6 @@ namespace Kurs.Platform.Migrations
b.ToTable("PNotificationRule", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<string>("Icon")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<int>("PostCount")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("Slug");
b.ToTable("PBlogCategories", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPost", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<int>("CommentCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.IsRequired()
.HasMaxLength(40)
.HasColumnType("nvarchar(40)")
.HasColumnName("ConcurrencyStamp");
b.Property<string>("ContentEn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ContentTr")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("CoverImage")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("ExtraProperties")
.IsRequired()
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int>("LikeCount")
.HasColumnType("int");
b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime2");
b.Property<string>("ReadTime")
.HasColumnType("nvarchar(max)");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<int>("ViewCount")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("IsPublished");
b.HasIndex("PublishedAt");
b.HasIndex("Slug");
b.ToTable("PBlogPosts", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b =>
{
b.Property<Guid>("Id")
@ -1071,6 +884,193 @@ namespace Kurs.Platform.Migrations
b.ToTable("BankAccounts");
});
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<string>("Icon")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<int>("PostCount")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("Slug");
b.ToTable("PBlogCategories", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.BlogPost", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<int>("CommentCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.IsRequired()
.HasMaxLength(40)
.HasColumnType("nvarchar(40)")
.HasColumnName("ConcurrencyStamp");
b.Property<string>("ContentEn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ContentTr")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("CoverImage")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("ExtraProperties")
.IsRequired()
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int>("LikeCount")
.HasColumnType("int");
b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime2");
b.Property<string>("ReadTime")
.HasColumnType("nvarchar(max)");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<int>("ViewCount")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("IsPublished");
b.HasIndex("PublishedAt");
b.HasIndex("Slug");
b.ToTable("PBlogPosts", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Branch", b =>
{
b.Property<Guid>("Id")
@ -2514,6 +2514,10 @@ namespace Kurs.Platform.Migrations
b.Property<string>("Slug")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<int>("TopicCount")
.HasColumnType("int");
@ -2578,6 +2582,10 @@ namespace Kurs.Platform.Migrations
b.Property<Guid?>("ParentPostId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<Guid>("TopicId")
.HasColumnType("uniqueidentifier");
@ -2665,6 +2673,10 @@ namespace Kurs.Platform.Migrations
b.Property<int>("ReplyCount")
.HasColumnType("int");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
@ -4757,17 +4769,6 @@ namespace Kurs.Platform.Migrations
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPost", b =>
{
b.HasOne("Kurs.Platform.Blog.BlogCategory", "Category")
.WithMany("Posts")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("Kurs.Platform.Entities.BankAccount", b =>
{
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
@ -4785,6 +4786,17 @@ namespace Kurs.Platform.Migrations
b.Navigation("Currency");
});
modelBuilder.Entity("Kurs.Platform.Entities.BlogPost", b =>
{
b.HasOne("Kurs.Platform.Entities.BlogCategory", "Category")
.WithMany("Posts")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
{
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
@ -5027,7 +5039,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Texts");
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
{
b.Navigation("Posts");
});

View file

@ -29,6 +29,7 @@ namespace Kurs.Platform.Migrations
LastPostDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastPostUserName = table.Column<string>(type: "nvarchar(max)", nullable: true),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
@ -62,6 +63,7 @@ namespace Kurs.Platform.Migrations
LastPostDate = table.Column<DateTime>(type: "datetime2", nullable: true),
LastPostUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastPostUserName = table.Column<string>(type: "nvarchar(max)", nullable: true),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
@ -93,6 +95,7 @@ namespace Kurs.Platform.Migrations
LikeCount = table.Column<int>(type: "int", nullable: false),
IsAcceptedAnswer = table.Column<bool>(type: "bit", nullable: false),
ParentPostId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),

View file

@ -650,193 +650,6 @@ namespace Kurs.Platform.Migrations
b.ToTable("PNotificationRule", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<string>("Icon")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<int>("PostCount")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("Slug");
b.ToTable("PBlogCategories", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPost", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<int>("CommentCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.IsRequired()
.HasMaxLength(40)
.HasColumnType("nvarchar(40)")
.HasColumnName("ConcurrencyStamp");
b.Property<string>("ContentEn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ContentTr")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("CoverImage")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("ExtraProperties")
.IsRequired()
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int>("LikeCount")
.HasColumnType("int");
b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime2");
b.Property<string>("ReadTime")
.HasColumnType("nvarchar(max)");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<int>("ViewCount")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("IsPublished");
b.HasIndex("PublishedAt");
b.HasIndex("Slug");
b.ToTable("PBlogPosts", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.AiBot", b =>
{
b.Property<Guid>("Id")
@ -1068,6 +881,193 @@ namespace Kurs.Platform.Migrations
b.ToTable("BankAccounts");
});
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<string>("Icon")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<int>("PostCount")
.HasColumnType("int");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("Slug");
b.ToTable("PBlogCategories", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.BlogPost", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("AuthorId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<int>("CommentCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.IsRequired()
.HasMaxLength(40)
.HasColumnType("nvarchar(40)")
.HasColumnName("ConcurrencyStamp");
b.Property<string>("ContentEn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ContentTr")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("CoverImage")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("ExtraProperties")
.IsRequired()
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsPublished")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int>("LikeCount")
.HasColumnType("int");
b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime2");
b.Property<string>("ReadTime")
.HasColumnType("nvarchar(max)");
b.Property<string>("Slug")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<int>("ViewCount")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("IsPublished");
b.HasIndex("PublishedAt");
b.HasIndex("Slug");
b.ToTable("PBlogPosts", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.Branch", b =>
{
b.Property<Guid>("Id")
@ -2511,6 +2511,10 @@ namespace Kurs.Platform.Migrations
b.Property<string>("Slug")
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<int>("TopicCount")
.HasColumnType("int");
@ -2575,6 +2579,10 @@ namespace Kurs.Platform.Migrations
b.Property<Guid?>("ParentPostId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<Guid>("TopicId")
.HasColumnType("uniqueidentifier");
@ -2662,6 +2670,10 @@ namespace Kurs.Platform.Migrations
b.Property<int>("ReplyCount")
.HasColumnType("int");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
@ -4754,17 +4766,6 @@ namespace Kurs.Platform.Migrations
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogPost", b =>
{
b.HasOne("Kurs.Platform.Blog.BlogCategory", "Category")
.WithMany("Posts")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("Kurs.Platform.Entities.BankAccount", b =>
{
b.HasOne("Kurs.Platform.Entities.Bank", "Bank")
@ -4782,6 +4783,17 @@ namespace Kurs.Platform.Migrations
b.Navigation("Currency");
});
modelBuilder.Entity("Kurs.Platform.Entities.BlogPost", b =>
{
b.HasOne("Kurs.Platform.Entities.BlogCategory", "Category")
.WithMany("Posts")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("Kurs.Platform.Entities.Country", b =>
{
b.HasOne("Kurs.Platform.Entities.CountryGroup", null)
@ -5024,7 +5036,7 @@ namespace Kurs.Platform.Migrations
b.Navigation("Texts");
});
modelBuilder.Entity("Kurs.Platform.Blog.BlogCategory", b =>
modelBuilder.Entity("Kurs.Platform.Entities.BlogCategory", b =>
{
b.Navigation("Posts");
});

View file

@ -39,23 +39,6 @@ export interface BlogCategory {
isActive: boolean;
}
export interface BlogComment {
id: string;
postId: string;
content: string;
author: {
id: string;
name: string;
avatar?: string;
};
parentId?: string;
replies?: BlogComment[];
likeCount: number;
isLiked?: boolean;
createdAt: string;
updatedAt: string;
}
export interface CreateBlogPostRequest {
title: string;
slug: string;

View file

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

81
ui/src/proxy/blog/blog.ts Normal file
View 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
}

View file

@ -10,9 +10,10 @@ export interface ForumCategory {
topicCount: number;
postCount: number;
lastPostId?: string;
lastPostDate?: Date;
lastPostDate?: string;
lastPostUserId?: string;
creationTime: Date;
creationTime: string;
tenantId?: string;
}
export interface ForumTopic {
@ -29,10 +30,11 @@ export interface ForumTopic {
isLocked: boolean;
isSolved: boolean;
lastPostId?: string;
lastPostDate?: Date;
lastPostDate?: string;
lastPostUserId?: string;
lastPostUserName?: string;
creationTime: Date;
creationTime: string;
tenantId?: string;
}
export interface ForumPost {
@ -44,7 +46,8 @@ export interface ForumPost {
likeCount: number;
isAcceptedAnswer: boolean;
parentPostId?: string;
creationTime: Date;
creationTime: string;
tenantId?: string;
}
export type ViewMode = 'forum' | 'admin';

View file

@ -1,106 +1,6 @@
import { BlogCategory, BlogListParams, BlogPost, CreateUpdateBlogCategoryDto, CreateUpdateBlogPostDto, PaginatedResponse } from '@/proxy/blog/blog';
import apiService from '@/services/api.service'
export interface BlogPost {
id: string
title: string
slug: string
contentTr?: string
contentEn?: string
summary: string
coverImage?: string
author: {
id: string
name: string
avatar?: string
}
category: {
id: string
name: string
slug: string
}
tags: string[]
viewCount: number
likeCount: number
commentCount: number
isPublished: boolean
publishedAt?: string
createdAt: string
updatedAt: string
}
export interface BlogCategory {
id: string
name: string
slug: string
description?: string
postCount: number
isActive: boolean
icon: string
displayOrder: number
}
export interface BlogComment {
id: string
postId: string
content: string
author: {
id: string
name: string
avatar?: string
}
parentId?: string
replies?: BlogComment[]
likeCount: number
isLiked?: boolean
createdAt: string
updatedAt: string
}
export interface CreateUpdateBlogPostDto {
title: string
slug: string
contentTr: string
contentEn: string
summary: string
categoryId: string
tags: string[]
coverImage?: string
isPublished: boolean
}
export interface CreateUpdateBlogCategoryDto {
name: string
slug: string
description: string
icon?: string
displayOrder: number
isActive: boolean
}
export interface CreateCommentDto {
postId: string
content: string
parentId?: string
}
export interface BlogListParams {
page?: number
pageSize?: number
categoryId?: string
tag?: string
search?: string
authorId?: string
sortBy?: 'latest' | 'popular' | 'trending'
}
export interface PaginatedResponse<T> {
items: T[]
totalCount: number
pageNumber: number
pageSize: number
totalPages: number
}
class BlogService {
async getPosts(params: BlogListParams = {}): Promise<PaginatedResponse<BlogPost>> {
const response = await apiService.fetchData<PaginatedResponse<BlogPost>>({
@ -176,23 +76,6 @@ class BlogService {
return response.data
}
async getComments(postId: string): Promise<BlogComment[]> {
const response = await apiService.fetchData<BlogComment[]>({
url: `/api/app/blog/posts/${postId}/comments`,
method: 'GET',
})
return response.data
}
async createComment(data: CreateCommentDto): Promise<BlogComment> {
const response = await apiService.fetchData<BlogComment>({
url: '/api/app/blog/comments',
method: 'POST',
data: data as any,
})
return response.data
}
async deleteComment(id: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/blog/comments/${id}`,

View file

@ -35,6 +35,7 @@ export interface CreateCategoryRequest {
displayOrder: number
isActive: boolean
isLocked: boolean
tenantId?: string
}
export interface CreateTopicRequest {
@ -43,12 +44,14 @@ export interface CreateTopicRequest {
categoryId: string
isPinned?: boolean
isLocked?: boolean
tenantId?: string
}
export interface CreatePostRequest {
topicId: string
content: string
parentPostId?: string
tenantId?: string
}
export interface CategoryListParams {
@ -115,7 +118,7 @@ class ForumService {
async createCategory(data: CreateCategoryRequest): Promise<ForumCategory> {
const response = await apiService.fetchData<ForumCategory>({
url: '/api/app/forum/categories',
url: '/api/app/forum/category',
method: 'POST',
data,
})
@ -124,27 +127,45 @@ class ForumService {
async updateCategory(id: string, data: Partial<CreateCategoryRequest>): Promise<ForumCategory> {
const response = await apiService.fetchData<ForumCategory>({
url: `/api/app/forum/categories/${id}`,
url: `/api/app/forum/${id}/category`,
method: 'PUT',
data,
})
return response.data
}
async updateCategoryLockState(id: string): Promise<ForumCategory> {
const response = await apiService.fetchData<ForumCategory>({
url: `/api/app/forum/${id}/category-lock`,
method: 'PUT',
})
return response.data
}
async updateCategoryActiveState(id: string): Promise<ForumCategory> {
const response = await apiService.fetchData<ForumCategory>({
url: `/api/app/forum/${id}/category-active`,
method: 'PUT',
})
return response.data
}
async deleteCategory(id: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/forum/categories/${id}`,
url: `/api/app/forum/${id}/category`,
method: 'DELETE',
})
}
async toggleCategoryStatus(id: string): Promise<ForumCategory> {
const response = await apiService.fetchData<ForumCategory>({
url: `/api/app/forum/categories/${id}/toggle-status`,
method: 'POST',
})
return response.data
}
// async toggleCategoryStatus(id: string): Promise<ForumCategory> {
// const response = await apiService.fetchData<ForumCategory>({
// url: `/api/app/forum/categories/${id}/toggle-status`,
// method: 'POST',
// })
// return response.data
// }
// Topics
async getTopics(params: TopicListParams = {}): Promise<PaginatedResponse<ForumTopic>> {
@ -175,7 +196,7 @@ class ForumService {
async updateTopic(id: string, data: Partial<CreateTopicRequest>): Promise<ForumTopic> {
const response = await apiService.fetchData<ForumTopic>({
url: `/api/app/forum/topics/${id}`,
url: `/api/app/forum/${id}/topic`,
method: 'PUT',
data,
})
@ -184,7 +205,7 @@ class ForumService {
async deleteTopic(id: string): Promise<void> {
await apiService.fetchData({
url: `/api/app/forum/topics/${id}`,
url: `/api/app/forum/${id}/topic`,
method: 'DELETE',
})
}
@ -336,10 +357,17 @@ class ForumService {
totalUsers: number
activeUsers: number
}> {
const response = await apiService.fetchData({
const response = await apiService.fetchData<{
totalCategories: number
totalTopics: number
totalPosts: number
totalUsers: number
activeUsers: number
}>({
url: '/api/app/forum/stats',
method: 'GET',
})
return response.data
}
}

View file

@ -12,10 +12,6 @@ import { HiPlus, HiPencil, HiTrash, HiEye } from 'react-icons/hi'
import { useNavigate } from 'react-router-dom'
import {
blogService,
BlogPost,
BlogCategory,
CreateUpdateBlogPostDto,
CreateUpdateBlogCategoryDto,
} from '@/services/blog.service'
import { format } from 'date-fns'
import { tr } from 'date-fns/locale'
@ -39,6 +35,7 @@ import { useStoreState } from '@/store/store'
import TabList from '@/components/ui/Tabs/TabList'
import TabNav from '@/components/ui/Tabs/TabNav'
import TabContent from '@/components/ui/Tabs/TabContent'
import { BlogCategory, BlogPost, CreateUpdateBlogCategoryDto, CreateUpdateBlogPostDto } from '@/proxy/blog/blog'
const validationSchema = Yup.object().shape({
title: Yup.string().required(),

View file

@ -14,6 +14,8 @@ export function Management() {
error,
createCategory,
updateCategory,
updateCategoryLockState,
updateCategoryActiveState,
deleteCategory,
createTopic,
updateTopic,
@ -79,6 +81,8 @@ export function Management() {
loading={loading}
onCreateCategory={(data) => createCategory(data).then(() => {})}
onUpdateCategory={(id, data) => updateCategory(id, data).then(() => {})}
onUpdateCategoryLockState={(id) => updateCategoryLockState(id).then(() => {})}
onUpdateCategoryActiveState={(id) => updateCategoryActiveState(id).then(() => {})}
onDeleteCategory={(id) => deleteCategory(id).then(() => {})}
onCreateTopic={(data) => createTopic(data).then(() => {})}
onUpdateTopic={(id, data) => updateTopic(id, data).then(() => {})}

View file

@ -21,6 +21,8 @@ interface AdminViewProps {
isLocked: boolean;
}) => Promise<void>;
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<void>;
onUpdateCategoryLockState: (id: string) => Promise<void>;
onUpdateCategoryActiveState: (id: string) => Promise<void>;
onDeleteCategory: (id: string) => Promise<void>;
onCreateTopic: (topic: {
title: string;
@ -58,6 +60,8 @@ export function AdminView({
onCreateCategory,
onUpdateCategory,
onDeleteCategory,
onUpdateCategoryLockState,
onUpdateCategoryActiveState,
onCreateTopic,
onUpdateTopic,
onDeleteTopic,
@ -121,6 +125,8 @@ export function AdminView({
onCreateCategory={onCreateCategory}
onUpdateCategory={onUpdateCategory}
onDeleteCategory={onDeleteCategory}
onUpdateCategoryLockState={onUpdateCategoryLockState}
onUpdateCategoryActiveState={onUpdateCategoryActiveState}
/>
)}

View file

@ -16,6 +16,8 @@ interface CategoryManagementProps {
}) => Promise<void>;
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<void>;
onDeleteCategory: (id: string) => Promise<void>;
onUpdateCategoryLockState: (id: string) => Promise<void>;
onUpdateCategoryActiveState: (id: string) => Promise<void>;
}
export function CategoryManagement({
@ -23,7 +25,9 @@ export function CategoryManagement({
loading,
onCreateCategory,
onUpdateCategory,
onDeleteCategory
onDeleteCategory,
onUpdateCategoryLockState,
onUpdateCategoryActiveState
}: CategoryManagementProps) {
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingCategory, setEditingCategory] = useState<ForumCategory | null>(null);
@ -87,7 +91,7 @@ export function CategoryManagement({
const handleToggleActive = async (category: ForumCategory) => {
try {
await onUpdateCategory(category.id, { isActive: !category.isActive });
await onUpdateCategoryActiveState(category.id);
} catch (error) {
console.error('Error toggling category status:', error);
}
@ -95,7 +99,7 @@ export function CategoryManagement({
const handleToggleLocked = async (category: ForumCategory) => {
try {
await onUpdateCategory(category.id, { isLocked: !category.isLocked });
await onUpdateCategoryLockState(category.id);
} catch (error) {
console.error('Error toggling category lock:', error);
}

View file

@ -1,116 +1,115 @@
import React, { useState } from 'react';
import { Plus, Edit2, Trash2, CheckCircle, Circle, Heart, Loader2 } from 'lucide-react';
import { ForumPost, ForumTopic } from '@/proxy/forum/forum';
import React, { useState } from 'react'
import { Plus, Edit2, Trash2, CheckCircle, Circle, Heart, Loader2 } from 'lucide-react'
import { ForumPost, ForumTopic } from '@/proxy/forum/forum'
interface PostManagementProps {
posts: ForumPost[];
topics: ForumTopic[];
loading: boolean;
onCreatePost: (post: {
topicId: string;
content: string;
parentPostId?: string;
}) => Promise<void>;
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<void>;
onDeletePost: (id: string) => Promise<void>;
onMarkPostAsAcceptedAnswer: (id: string) => Promise<void>;
onUnmarkPostAsAcceptedAnswer: (id: string) => Promise<void>;
posts: ForumPost[]
topics: ForumTopic[]
loading: boolean
onCreatePost: (post: { topicId: string; content: string; parentPostId?: string }) => Promise<void>
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<void>
onDeletePost: (id: string) => Promise<void>
onMarkPostAsAcceptedAnswer: (id: string) => Promise<void>
onUnmarkPostAsAcceptedAnswer: (id: string) => Promise<void>
}
export function PostManagement({
posts,
topics,
export function PostManagement({
posts,
topics,
loading,
onCreatePost,
onUpdatePost,
onCreatePost,
onUpdatePost,
onDeletePost,
onMarkPostAsAcceptedAnswer,
onUnmarkPostAsAcceptedAnswer
onUnmarkPostAsAcceptedAnswer,
}: PostManagementProps) {
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingPost, setEditingPost] = useState<ForumPost | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false)
const [editingPost, setEditingPost] = useState<ForumPost | null>(null)
const [formData, setFormData] = useState({
topicId: '',
content: '',
isAcceptedAnswer: false,
});
const [submitting, setSubmitting] = useState(false);
})
const [submitting, setSubmitting] = useState(false)
const resetForm = () => {
setFormData({
topicId: '',
content: '',
isAcceptedAnswer: false,
});
setShowCreateForm(false);
setEditingPost(null);
};
})
setShowCreateForm(false)
setEditingPost(null)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (submitting) return;
e.preventDefault()
if (submitting) return
try {
setSubmitting(true);
setSubmitting(true)
if (editingPost) {
await onUpdatePost(editingPost.id, formData);
await onUpdatePost(editingPost.id, formData)
} else {
await onCreatePost(formData);
await onCreatePost(formData)
}
resetForm();
resetForm()
} catch (error) {
console.error('Error submitting form:', error);
console.error('Error submitting form:', error)
} finally {
setSubmitting(false);
setSubmitting(false)
}
};
}
const handleEdit = (post: ForumPost) => {
setEditingPost(post);
setEditingPost(post)
setFormData({
topicId: post.topicId,
content: post.content,
isAcceptedAnswer: post.isAcceptedAnswer,
});
setShowCreateForm(true);
};
})
setShowCreateForm(true)
}
const handleToggleAcceptedAnswer = async (post: ForumPost) => {
try {
if (post.isAcceptedAnswer) {
await onUnmarkPostAsAcceptedAnswer(post.id);
await onUnmarkPostAsAcceptedAnswer(post.id)
} else {
await onMarkPostAsAcceptedAnswer(post.id);
await onMarkPostAsAcceptedAnswer(post.id)
}
} catch (error) {
console.error('Error toggling accepted answer:', error);
console.error('Error toggling accepted answer:', error)
}
};
}
const handleDelete = async (id: string) => {
if (confirm('Are you sure you want to delete this post?')) {
try {
await onDeletePost(id);
await onDeletePost(id)
} catch (error) {
console.error('Error deleting post:', error);
console.error('Error deleting post:', error)
}
}
};
}
const getTopicTitle = (topicId: string) => {
const topic = topics.find(t => t.id === topicId);
return topic ? topic.title : 'Unknown Topic';
};
const topic = topics.find((t) => t.id === topicId)
return topic ? topic.title : 'Unknown Topic'
}
const formatDate = (value: string | Date) => {
const date = value instanceof Date ? value : new Date(value)
if (isNaN(date.getTime())) return 'Invalid Date'
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
};
}).format(date)
}
return (
<div className="space-y-6">
@ -142,14 +141,14 @@ export function PostManagement({
required
>
<option value="">Select a topic</option>
{topics.map(topic => (
{topics.map((topic) => (
<option key={topic.id} value={topic.id}>
{topic.title}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea
@ -160,7 +159,7 @@ export function PostManagement({
required
/>
</div>
<div className="flex items-center">
<label className="flex items-center">
<input
@ -172,7 +171,7 @@ export function PostManagement({
Mark as Accepted Answer
</label>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
@ -200,7 +199,7 @@ export function PostManagement({
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Posts ({posts.length})</h3>
</div>
{loading ? (
<div className="p-8 text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
@ -209,7 +208,9 @@ export function PostManagement({
) : (
<div className="divide-y divide-gray-200">
{posts
.sort((a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime())
.sort(
(a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime(),
)
.map((post) => (
<div key={post.id} className="p-6 hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between">
@ -223,14 +224,15 @@ export function PostManagement({
</div>
)}
</div>
<div className="mb-3">
<p className="text-xs text-gray-500 mb-1">
Reply to: <span className="font-medium">{getTopicTitle(post.topicId)}</span>
Reply to:{' '}
<span className="font-medium">{getTopicTitle(post.topicId)}</span>
</p>
<p className="text-gray-700 line-clamp-3">{post.content}</p>
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center space-x-4">
<span>{formatDate(post.creationTime)}</span>
@ -241,7 +243,7 @@ export function PostManagement({
</div>
</div>
</div>
<div className="flex items-center space-x-2 ml-4">
<button
onClick={() => handleToggleAcceptedAnswer(post)}
@ -250,11 +252,19 @@ export function PostManagement({
? 'text-emerald-600 hover:bg-emerald-100'
: 'text-gray-400 hover:bg-gray-100'
}`}
title={post.isAcceptedAnswer ? 'Remove Accepted Answer' : 'Mark as Accepted Answer'}
title={
post.isAcceptedAnswer
? 'Remove Accepted Answer'
: 'Mark as Accepted Answer'
}
>
{post.isAcceptedAnswer ? <CheckCircle className="w-4 h-4" /> : <Circle className="w-4 h-4" />}
{post.isAcceptedAnswer ? (
<CheckCircle className="w-4 h-4" />
) : (
<Circle className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleEdit(post)}
className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
@ -262,7 +272,7 @@ export function PostManagement({
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(post.id)}
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
@ -278,5 +288,5 @@ export function PostManagement({
)}
</div>
</div>
);
}
)
}

View file

@ -1,44 +1,56 @@
import React, { useState } from 'react';
import { Plus, Edit2, Trash2, Lock, Unlock, Pin, PinOff, CheckCircle, Circle, Eye, Loader2 } from 'lucide-react';
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum';
import React, { useState } from 'react'
import {
Plus,
Edit2,
Trash2,
Lock,
Unlock,
Pin,
PinOff,
CheckCircle,
Circle,
Eye,
Loader2,
} from 'lucide-react'
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum'
interface TopicManagementProps {
topics: ForumTopic[];
categories: ForumCategory[];
loading: boolean;
topics: ForumTopic[]
categories: ForumCategory[]
loading: boolean
onCreateTopic: (topic: {
title: string;
content: string;
categoryId: string;
isPinned?: boolean;
isLocked?: boolean;
}) => Promise<void>;
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<void>;
onDeleteTopic: (id: string) => Promise<void>;
onPinTopic: (id: string) => Promise<void>;
onUnpinTopic: (id: string) => Promise<void>;
onLockTopic: (id: string) => Promise<void>;
onUnlockTopic: (id: string) => Promise<void>;
onMarkTopicAsSolved: (id: string) => Promise<void>;
onMarkTopicAsUnsolved: (id: string) => Promise<void>;
title: string
content: string
categoryId: string
isPinned?: boolean
isLocked?: boolean
}) => Promise<void>
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<void>
onDeleteTopic: (id: string) => Promise<void>
onPinTopic: (id: string) => Promise<void>
onUnpinTopic: (id: string) => Promise<void>
onLockTopic: (id: string) => Promise<void>
onUnlockTopic: (id: string) => Promise<void>
onMarkTopicAsSolved: (id: string) => Promise<void>
onMarkTopicAsUnsolved: (id: string) => Promise<void>
}
export function TopicManagement({
topics,
categories,
export function TopicManagement({
topics,
categories,
loading,
onCreateTopic,
onUpdateTopic,
onCreateTopic,
onUpdateTopic,
onDeleteTopic,
onPinTopic,
onUnpinTopic,
onLockTopic,
onUnlockTopic,
onMarkTopicAsSolved,
onMarkTopicAsUnsolved
onMarkTopicAsUnsolved,
}: TopicManagementProps) {
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false)
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null)
const [formData, setFormData] = useState({
title: '',
content: '',
@ -46,8 +58,8 @@ export function TopicManagement({
isPinned: false,
isLocked: false,
isSolved: false,
});
const [submitting, setSubmitting] = useState(false);
})
const [submitting, setSubmitting] = useState(false)
const resetForm = () => {
setFormData({
@ -57,32 +69,32 @@ export function TopicManagement({
isPinned: false,
isLocked: false,
isSolved: false,
});
setShowCreateForm(false);
setEditingTopic(null);
};
})
setShowCreateForm(false)
setEditingTopic(null)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (submitting) return;
e.preventDefault()
if (submitting) return
try {
setSubmitting(true);
setSubmitting(true)
if (editingTopic) {
await onUpdateTopic(editingTopic.id, formData);
await onUpdateTopic(editingTopic.id, formData)
} else {
await onCreateTopic(formData);
await onCreateTopic(formData)
}
resetForm();
resetForm()
} catch (error) {
console.error('Error submitting form:', error);
console.error('Error submitting form:', error)
} finally {
setSubmitting(false);
setSubmitting(false)
}
};
}
const handleEdit = (topic: ForumTopic) => {
setEditingTopic(topic);
setEditingTopic(topic)
setFormData({
title: topic.title,
content: topic.content,
@ -90,68 +102,77 @@ export function TopicManagement({
isPinned: topic.isPinned,
isLocked: topic.isLocked,
isSolved: topic.isSolved,
});
setShowCreateForm(true);
};
})
setShowCreateForm(true)
}
const handlePin = async (topic: ForumTopic) => {
try {
if (topic.isPinned) {
await onUnpinTopic(topic.id);
await onUnpinTopic(topic.id)
} else {
await onPinTopic(topic.id);
await onPinTopic(topic.id)
}
} catch (error) {
console.error('Error toggling pin:', error);
console.error('Error toggling pin:', error)
}
};
}
const handleLock = async (topic: ForumTopic) => {
try {
if (topic.isLocked) {
await onUnlockTopic(topic.id);
await onUnlockTopic(topic.id)
} else {
await onLockTopic(topic.id);
await onLockTopic(topic.id)
}
} catch (error) {
console.error('Error toggling lock:', error);
console.error('Error toggling lock:', error)
}
};
}
const handleSolved = async (topic: ForumTopic) => {
try {
if (topic.isSolved) {
await onMarkTopicAsUnsolved(topic.id);
await onMarkTopicAsUnsolved(topic.id)
} else {
await onMarkTopicAsSolved(topic.id);
await onMarkTopicAsSolved(topic.id)
}
} catch (error) {
console.error('Error toggling solved status:', error);
console.error('Error toggling solved status:', error)
}
};
}
const handleDelete = async (id: string) => {
if (confirm('Are you sure you want to delete this topic? This will also delete all posts in this topic.')) {
if (
confirm(
'Are you sure you want to delete this topic? This will also delete all posts in this topic.',
)
) {
try {
await onDeleteTopic(id);
await onDeleteTopic(id)
} catch (error) {
console.error('Error deleting topic:', error);
console.error('Error deleting topic:', error)
}
}
};
}
const getCategoryName = (categoryId: string) => {
const category = categories.find(c => c.id === categoryId);
return category ? category.name : 'Unknown Category';
};
const category = categories.find((c) => c.id === categoryId)
return category ? category.name : 'Unknown Category'
}
const formatDate = (value: string | Date) => {
const date = value instanceof Date ? value : new Date(value)
if (isNaN(date.getTime())) return 'Invalid Date'
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(date);
};
hour: '2-digit',
minute: '2-digit',
}).format(date)
}
return (
<div className="space-y-6">
@ -184,7 +205,7 @@ export function TopicManagement({
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
<select
@ -194,14 +215,14 @@ export function TopicManagement({
required
>
<option value="">Select a category</option>
{categories.map(category => (
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea
@ -212,7 +233,7 @@ export function TopicManagement({
required
/>
</div>
<div className="flex items-center space-x-6">
<label className="flex items-center">
<input
@ -242,7 +263,7 @@ export function TopicManagement({
Solved
</label>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
@ -270,7 +291,7 @@ export function TopicManagement({
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Topics ({topics.length})</h3>
</div>
{loading ? (
<div className="p-8 text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
@ -279,7 +300,9 @@ export function TopicManagement({
) : (
<div className="divide-y divide-gray-200">
{topics
.sort((a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime())
.sort(
(a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime(),
)
.map((topic) => (
<div key={topic.id} className="p-6 hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between">
@ -288,11 +311,13 @@ export function TopicManagement({
{topic.isPinned && <Pin className="w-4 h-4 text-orange-500" />}
{topic.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
{topic.isSolved && <CheckCircle className="w-4 h-4 text-emerald-500" />}
<h4 className="text-lg font-semibold text-gray-900 line-clamp-1">{topic.title}</h4>
<h4 className="text-lg font-semibold text-gray-900 line-clamp-1">
{topic.title}
</h4>
</div>
<p className="text-gray-600 mb-3 line-clamp-2">{topic.content}</p>
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center space-x-4">
<span className="font-medium">{getCategoryName(topic.categoryId)}</span>
@ -309,7 +334,7 @@ export function TopicManagement({
</div>
</div>
</div>
<div className="flex items-center space-x-2 ml-4">
<button
onClick={() => handlePin(topic)}
@ -320,9 +345,13 @@ export function TopicManagement({
}`}
title={topic.isPinned ? 'Unpin Topic' : 'Pin Topic'}
>
{topic.isPinned ? <PinOff className="w-4 h-4" /> : <Pin className="w-4 h-4" />}
{topic.isPinned ? (
<PinOff className="w-4 h-4" />
) : (
<Pin className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleLock(topic)}
className={`p-2 rounded-lg transition-colors ${
@ -332,9 +361,13 @@ export function TopicManagement({
}`}
title={topic.isLocked ? 'Unlock Topic' : 'Lock Topic'}
>
{topic.isLocked ? <Lock className="w-4 h-4" /> : <Unlock className="w-4 h-4" />}
{topic.isLocked ? (
<Lock className="w-4 h-4" />
) : (
<Unlock className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleSolved(topic)}
className={`p-2 rounded-lg transition-colors ${
@ -344,9 +377,13 @@ export function TopicManagement({
}`}
title={topic.isSolved ? 'Mark as Unsolved' : 'Mark as Solved'}
>
{topic.isSolved ? <CheckCircle className="w-4 h-4" /> : <Circle className="w-4 h-4" />}
{topic.isSolved ? (
<CheckCircle className="w-4 h-4" />
) : (
<Circle className="w-4 h-4" />
)}
</button>
<button
onClick={() => handleEdit(topic)}
className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
@ -354,7 +391,7 @@ export function TopicManagement({
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(topic.id)}
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
@ -370,5 +407,5 @@ export function TopicManagement({
)}
</div>
</div>
);
}
)
}

View file

@ -1,18 +1,22 @@
import React, { useState } from 'react';
import { X } from 'lucide-react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
interface CreatePostModalProps {
onClose: () => void;
onSubmit: (data: { content: string }) => void;
parentPostId?: string;
}
export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostModalProps) {
const [content, setContent] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (content.trim()) {
onSubmit({ content: content.trim() });
const plainText = content.replace(/<[^>]+>/g, '').trim(); // HTML etiketlerini temizle
if (plainText) {
onSubmit({ content });
}
};
@ -20,7 +24,9 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Reply to Topic</h3>
<h3 className="text-lg font-semibold text-gray-900">
{parentPostId ? 'Reply to Topic' : 'New Post'}
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
@ -28,24 +34,21 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-2">
Your Reply
<label className="block text-sm font-medium text-gray-700 mb-2">
{parentPostId ? 'Your Reply' : 'Message'}
</label>
<textarea
id="content"
<ReactQuill
theme="snow"
value={content}
onChange={(e) => setContent(e.target.value)}
rows={6}
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Write your reply..."
required
autoFocus
onChange={setContent}
style={{ height: '400px', marginBottom: '50px' }}
placeholder={parentPostId ? 'Write your reply...' : 'Write your message...'}
/>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
@ -57,7 +60,7 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
<button
type="submit"
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors disabled:opacity-50"
disabled={!content.trim()}
disabled={!content || content.replace(/<[^>]+>/g, '').trim() === ''}
>
Post Reply
</button>
@ -66,4 +69,4 @@ export function CreatePostModal({ onClose, onSubmit }: CreatePostModalProps) {
</div>
</div>
);
}
}

View file

@ -1,19 +1,24 @@
import React from 'react';
import { MessageSquare, Lock, TrendingUp } from 'lucide-react';
import { ForumCategory } from '@/proxy/forum/forum';
import React from 'react'
import { MessageSquare, Lock, TrendingUp } from 'lucide-react'
import { ForumCategory } from '@/proxy/forum/forum'
interface CategoryCardProps {
category: ForumCategory;
onClick: () => void;
category: ForumCategory
onClick: () => void
}
export function CategoryCard({ category, onClick }: CategoryCardProps) {
export function ForumCategoryCard({ category, onClick }: CategoryCardProps) {
const formatDate = (dateString?: string) => {
if (!dateString) return 'Never';
if (!dateString) return 'Never'
const date = new Date(dateString)
const diffDays = Math.floor((date.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
const now = new Date()
// Zaman farkını doğru hesapla
const diffTime = date.getTime() - now.getTime()
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24))
// Intl.RelativeTimeFormat doğru yönle çalışır
return new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(diffDays, 'day')
}
@ -30,13 +35,9 @@ export function CategoryCard({ category, onClick }: CategoryCardProps) {
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
{category.name}
</h3>
{category.isLocked && (
<Lock className="w-4 h-4 text-gray-400" />
)}
{category.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
</div>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
{category.description}
</p>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">{category.description}</p>
<div className="flex items-center space-x-4 text-sm text-gray-500">
<div className="flex items-center space-x-1">
<MessageSquare className="w-4 h-4" />
@ -51,11 +52,9 @@ export function CategoryCard({ category, onClick }: CategoryCardProps) {
</div>
<div className="text-right text-sm text-gray-500 ml-4">
<div>Last post</div>
<div className="font-medium text-gray-700">
{formatDate(category.lastPostDate)}
</div>
<div className="font-medium text-gray-700">{formatDate(category.lastPostDate)}</div>
</div>
</div>
</div>
);
}
)
}

View file

@ -1,40 +1,57 @@
import React from 'react';
import { Heart, User, CheckCircle, Reply } from 'lucide-react';
import { ForumPost } from '@/proxy/forum/forum';
import React from 'react'
import { Heart, User, CheckCircle, Reply } from 'lucide-react'
import { ForumPost } from '@/proxy/forum/forum'
import { AVATAR_URL } from '@/constants/app.constant'
interface PostCardProps {
post: ForumPost;
onLike: (postId: string, isFirst: boolean) => void;
onReply: (postId: string) => void;
isFirst?: boolean;
isLiked?: boolean;
post: ForumPost
onLike: (postId: string, isFirst: boolean) => void
onReply: (postId: string) => void
isFirst?: boolean
isLiked?: boolean
}
export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = false }: PostCardProps) {
export function ForumPostCard({
post,
onLike,
onReply,
isFirst = false,
isLiked = false,
}: PostCardProps) {
const handleLike = () => {
onLike(post.id, isFirst);
};
onLike(post.id, isFirst)
}
const formatDate = (value: string | Date) => {
const date = value instanceof Date ? value : new Date(value)
if (isNaN(date.getTime())) return 'Invalid Date'
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
};
minute: '2-digit',
}).format(date)
}
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${isFirst ? 'border-l-4 border-l-blue-500' : ''}`}>
<div
className={`bg-white rounded-xl shadow-sm border border-gray-200 p-6 ${isFirst ? 'border-l-4 border-l-blue-500' : ''}`}
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
<div className="w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-white" />
</div>
<img
src={AVATAR_URL(post.authorId, post.tenantId)}
onError={(e) => {
e.currentTarget.onerror = null
e.currentTarget.src = '/img/others/default-profile.png'
}}
alt="User"
className="w-10 h-10 rounded-full"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
@ -48,14 +65,14 @@ export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = fal
</div>
<span className="text-sm text-gray-500">{formatDate(post.creationTime)}</span>
</div>
<div className="prose prose-sm max-w-none mb-4">
<p className="text-gray-700 whitespace-pre-wrap">{post.content}</p>
<p className="text-gray-700 whitespace-pre-wrap" dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => onLike(post.id, isFirst)}
onClick={() => onLike(post.id, isFirst)}
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-sm transition-colors ${
isLiked
? 'bg-red-100 text-red-600 hover:bg-red-200'
@ -65,7 +82,7 @@ export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = fal
<Heart className={`w-4 h-4 ${isLiked ? 'fill-current' : ''}`} />
<span>{post.likeCount}</span>
</button>
<button
onClick={() => onReply(post.id)}
className="flex items-center space-x-1 px-3 py-1 rounded-full text-sm bg-gray-100 text-gray-600 hover:bg-gray-200 transition-colors"
@ -77,5 +94,5 @@ export function PostCard({ post, onLike, onReply, isFirst = false, isLiked = fal
</div>
</div>
</div>
);
}
)
}

View 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>
)
}

View file

@ -1,14 +1,14 @@
import React, { useState } from 'react'
import { ArrowLeft, Plus, Loader2, Search } from 'lucide-react'
import { CategoryCard } from './CategoryCard'
import { TopicCard } from './TopicCard'
import { PostCard } from './PostCard'
import { CreateTopicModal } from './CreateTopicModal'
import { CreatePostModal } from './CreatePostModal'
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
import { SearchModal } from './SearchModal'
import { forumService } from '@/services/forumService'
import { buildPostTree } from './utils'
import { ForumPostCard } from './ForumPostCard'
import { ForumCategoryCard } from './ForumCategoryCard'
import { ForumTopicCard } from './ForumTopicCard'
interface ForumViewProps {
categories: ForumCategory[]
@ -67,6 +67,7 @@ export function ForumView({
const [replyToPostId, setReplyToPostId] = useState<string | undefined>()
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false)
const [postLikeCounts, setPostLikeCounts] = useState<Record<string, number>>({})
const handleSearchCategorySelect = (category: ForumCategory) => {
if (onCategorySelect) onCategorySelect(category)
@ -182,25 +183,6 @@ export function ForumView({
}
}
// const handleBreadcrumbClick = (target: 'forum' | 'category') => {
// if (target === 'forum') {
// if (onViewStateChange) {
// onViewStateChange('categories')
// } else {
// setLocalViewState('categories')
// setLocalSelectedCategory(null)
// setLocalSelectedTopic(null)
// }
// } else if (target === 'category' && selectedCategory) {
// if (onViewStateChange) {
// onViewStateChange('topics')
// } else {
// setLocalViewState('topics')
// setLocalSelectedTopic(null)
// }
// }
// }
const filteredTopics = selectedCategory
? topics.filter((topic) => topic.categoryId === selectedCategory.id)
: []
@ -247,14 +229,14 @@ export function ForumView({
function renderPosts(posts: (ForumPost & { children: ForumPost[] })[]) {
return posts.map((post) => (
<div key={post.id}>
<PostCard
<ForumPostCard
post={post}
onLike={handleLike}
onReply={handleReply}
isLiked={likedPosts.has(post.id)}
/>
{post.children.length > 0 && (
<div className="pl-6 border-l border-gray-200 mt-4">{renderPosts(post.children)}</div>
<div className="pl-6 border-gray-200 mt-4">{renderPosts(post.children)}</div>
)}
</div>
))
@ -262,24 +244,40 @@ export function ForumView({
const handleLike = async (postId: string, isFirst: boolean = false) => {
try {
if (likedPosts.has(postId)) {
isFirst ? await forumService.unlikeTopic(postId) : await onUnlikePost(postId)
const isLiked = likedPosts.has(postId)
setLikedPosts((prev) => {
const newSet = new Set(prev)
newSet.delete(postId)
return newSet
})
if (isLiked) {
isFirst ? await forumService.unlikeTopic(postId) : await onUnlikePost(postId)
} else {
isFirst ? await forumService.likeTopic(postId) : await onLikePost(postId)
}
setLikedPosts((prev) => new Set(prev).add(postId))
// 🔁 toggle liked state
setLikedPosts((prev) => {
const updated = new Set(prev)
if (isLiked) updated.delete(postId)
else updated.add(postId)
return updated
})
if (selectedTopic?.id === postId) {
setPostLikeCounts((prev) => ({
...prev,
[postId]: (prev[postId] ?? selectedTopic.likeCount) + (isLiked ? -1 : 1),
}))
}
} catch (error) {
console.error('Error liking/unliking post or topic:', error)
}
}
// 🧠 Helper function to read initial like count
const getInitialLikeCount = (postId: string) => {
if (selectedTopic?.id === postId) return selectedTopic.likeCount
const post = posts.find((p) => p.id === postId)
return post?.likeCount ?? 0
}
const handleReply = (postId: string) => {
setReplyToPostId(postId)
setShowCreatePost(true)
@ -300,7 +298,7 @@ export function ForumView({
<>
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Breadcrumb + Actions + Search Row */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center justify-between mb-4">
{/* Left Side: Breadcrumb */}
<div className="flex items-center space-x-2">
{viewState !== 'categories' && (
@ -398,7 +396,7 @@ export function ForumView({
.filter((cat) => cat.isActive)
.sort((a, b) => a.displayOrder - b.displayOrder)
.map((category) => (
<CategoryCard
<ForumCategoryCard
key={category.id}
category={category}
onClick={() => handleCategoryClick(category)}
@ -422,7 +420,7 @@ export function ForumView({
return new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime()
})
.map((topic) => (
<TopicCard
<ForumTopicCard
key={topic.id}
topic={topic}
onClick={() => handleTopicClick(topic)}
@ -440,17 +438,17 @@ export function ForumView({
<h2 className="text-2xl font-bold text-gray-900 mb-6">{selectedTopic.title}</h2>
{/* Topic Ana İçeriği */}
<PostCard
<ForumPostCard
post={{
id: selectedTopic.id,
topicId: selectedTopic.id,
content: selectedTopic.content,
authorId: selectedTopic.authorId,
authorName: selectedTopic.authorName,
likeCount: selectedTopic.likeCount,
isAcceptedAnswer: false,
likeCount: postLikeCounts[selectedTopic.id] ?? selectedTopic.likeCount,
isAcceptedAnswer: false,
parentPostId: undefined,
creationTime: selectedTopic.creationTime,
parentPostId: undefined
}}
onLike={handleLike}
onReply={handleReply}
@ -474,7 +472,11 @@ export function ForumView({
{/* Create Post Modal */}
{showCreatePost && (
<CreatePostModal onClose={() => setShowCreatePost(false)} onSubmit={handleCreatePost} />
<CreatePostModal
onClose={() => setShowCreatePost(false)}
onSubmit={handleCreatePost}
parentPostId={replyToPostId}
/>
)}
</div>

View file

@ -1,17 +1,17 @@
import React, { useState, useEffect } from 'react';
import { X, Search, Folder, MessageSquare, FileText, User } from 'lucide-react';
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum';
import { useForumSearch } from '@/utils/hooks/useForumSearch';
import React, { useState, useEffect } from 'react'
import { X, Search, Folder, MessageSquare, FileText, User } from 'lucide-react'
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
import { useForumSearch } from '@/utils/hooks/useForumSearch'
interface SearchModalProps {
isOpen: boolean;
onClose: () => void;
categories: ForumCategory[];
topics: ForumTopic[];
posts: ForumPost[];
onCategorySelect: (category: ForumCategory) => void;
onTopicSelect: (topic: ForumTopic) => void;
onPostSelect: (post: ForumPost) => void;
isOpen: boolean
onClose: () => void
categories: ForumCategory[]
topics: ForumTopic[]
posts: ForumPost[]
onCategorySelect: (category: ForumCategory) => void
onTopicSelect: (topic: ForumTopic) => void
onPostSelect: (post: ForumPost) => void
}
export function SearchModal({
@ -22,80 +22,86 @@ export function SearchModal({
posts,
onCategorySelect,
onTopicSelect,
onPostSelect
onPostSelect,
}: SearchModalProps) {
const { searchQuery, setSearchQuery, searchResults, clearSearch, hasResults } = useForumSearch({
categories,
topics,
posts
});
posts,
})
const [selectedIndex, setSelectedIndex] = useState(0);
const [selectedIndex, setSelectedIndex] = useState(0)
useEffect(() => {
if (isOpen) {
setSelectedIndex(0);
setSelectedIndex(0)
}
}, [isOpen, searchResults]);
}, [isOpen, searchResults])
const handleKeyDown = (e: React.KeyboardEvent) => {
const totalResults = searchResults.categories.length + searchResults.topics.length + searchResults.posts.length;
const totalResults =
searchResults.categories.length + searchResults.topics.length + searchResults.posts.length
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex(prev => (prev + 1) % totalResults);
e.preventDefault()
setSelectedIndex((prev) => (prev + 1) % totalResults)
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex(prev => (prev - 1 + totalResults) % totalResults);
e.preventDefault()
setSelectedIndex((prev) => (prev - 1 + totalResults) % totalResults)
} else if (e.key === 'Enter') {
e.preventDefault();
handleSelectResult(selectedIndex);
e.preventDefault()
handleSelectResult(selectedIndex)
} else if (e.key === 'Escape') {
onClose();
onClose()
}
};
}
const handleSelectResult = (index: number) => {
let currentIndex = 0;
let currentIndex = 0
// Check categories
if (index < searchResults.categories.length) {
onCategorySelect(searchResults.categories[index]);
onClose();
return;
onCategorySelect(searchResults.categories[index])
onClose()
return
}
currentIndex += searchResults.categories.length;
currentIndex += searchResults.categories.length
// Check topics
if (index < currentIndex + searchResults.topics.length) {
onTopicSelect(searchResults.topics[index - currentIndex]);
onClose();
return;
onTopicSelect(searchResults.topics[index - currentIndex])
onClose()
return
}
currentIndex += searchResults.topics.length;
currentIndex += searchResults.topics.length
// Check posts
if (index < currentIndex + searchResults.posts.length) {
onPostSelect(searchResults.posts[index - currentIndex]);
onClose();
return;
onPostSelect(searchResults.posts[index - currentIndex])
onClose()
return
}
};
}
const formatDate = (value: string | Date) => {
const date = value instanceof Date ? value : new Date(value)
if (isNaN(date.getTime())) return 'Invalid Date'
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
year: 'numeric'
}).format(date);
};
hour: '2-digit',
minute: '2-digit',
}).format(date)
}
const getTopicTitle = (topicId: string) => {
const topic = topics.find(t => t.id === topicId);
return topic ? topic.title : 'Unknown Topic';
};
const topic = topics.find((t) => t.id === topicId)
return topic ? topic.title : 'Unknown Topic'
}
if (!isOpen) return null;
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-start justify-center pt-20 p-4 z-50">
@ -141,8 +147,8 @@ export function SearchModal({
<button
key={`category-${category.id}`}
onClick={() => {
onCategorySelect(category);
onClose();
onCategorySelect(category)
onClose()
}}
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
selectedIndex === index ? 'bg-blue-50 border-r-2 border-blue-500' : ''
@ -156,12 +162,12 @@ export function SearchModal({
</div>
<div className="text-left">
<div className="font-medium text-gray-900">{category.name}</div>
<div className="text-sm text-gray-500 line-clamp-1">{category.description}</div>
<div className="text-sm text-gray-500 line-clamp-1">
{category.description}
</div>
</div>
</div>
<div className="text-xs text-gray-400">
{category.topicCount} topics
</div>
<div className="text-xs text-gray-400">{category.topicCount} topics</div>
</button>
))}
</div>
@ -174,16 +180,18 @@ export function SearchModal({
Topics ({searchResults.topics.length})
</div>
{searchResults.topics.map((topic, index) => {
const globalIndex = searchResults.categories.length + index;
const globalIndex = searchResults.categories.length + index
return (
<button
key={`topic-${topic.id}`}
onClick={() => {
onTopicSelect(topic);
onClose();
onTopicSelect(topic)
onClose()
}}
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
selectedIndex === globalIndex ? 'bg-blue-50 border-r-2 border-blue-500' : ''
selectedIndex === globalIndex
? 'bg-blue-50 border-r-2 border-blue-500'
: ''
}`}
>
<div className="flex items-center space-x-3 flex-1">
@ -193,17 +201,17 @@ export function SearchModal({
</div>
</div>
<div className="text-left">
<div className="font-medium text-gray-900 line-clamp-1">{topic.title}</div>
<div className="font-medium text-gray-900 line-clamp-1">
{topic.title}
</div>
<div className="text-sm text-gray-500">
by {topic.authorName} {formatDate(topic.creationTime)}
</div>
</div>
</div>
<div className="text-xs text-gray-400">
{topic.replyCount} replies
</div>
<div className="text-xs text-gray-400">{topic.replyCount} replies</div>
</button>
);
)
})}
</div>
)}
@ -215,16 +223,19 @@ export function SearchModal({
Posts ({searchResults.posts.length})
</div>
{searchResults.posts.map((post, index) => {
const globalIndex = searchResults.categories.length + searchResults.topics.length + index;
const globalIndex =
searchResults.categories.length + searchResults.topics.length + index
return (
<button
key={`post-${post.id}`}
onClick={() => {
onPostSelect(post);
onClose();
onPostSelect(post)
onClose()
}}
className={`w-full flex items-center px-4 py-3 hover:bg-gray-50 transition-colors ${
selectedIndex === globalIndex ? 'bg-blue-50 border-r-2 border-blue-500' : ''
selectedIndex === globalIndex
? 'bg-blue-50 border-r-2 border-blue-500'
: ''
}`}
>
<div className="flex items-center space-x-3 flex-1">
@ -245,11 +256,9 @@ export function SearchModal({
</div>
</div>
</div>
<div className="text-xs text-gray-400">
{post.likeCount} likes
</div>
<div className="text-xs text-gray-400">{post.likeCount} likes</div>
</button>
);
)
})}
</div>
)}
@ -264,5 +273,5 @@ export function SearchModal({
)}
</div>
</div>
);
}
)
}

View file

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

View file

@ -94,6 +94,16 @@ export function useForumData() {
}
}
const updateCategoryLockState = async (id: string) => {
await forumService.updateCategoryLockState(id)
await loadCategories() // refresh after update
}
const updateCategoryActiveState = async (id: string) => {
await forumService.updateCategoryActiveState(id)
await loadCategories() // refresh after update
}
const deleteCategory = async (id: string) => {
try {
setLoading(true)
@ -410,6 +420,8 @@ export function useForumData() {
// Category operations
createCategory,
updateCategory,
updateCategoryLockState,
updateCategoryActiveState,
deleteCategory,
// Topic operations