seed/data Seeder Forum

This commit is contained in:
Sedat ÖZTÜRK 2026-09-04 15:51:58 +03:00
parent ed143c01d0
commit 3ec9a94860
23 changed files with 11652 additions and 12745 deletions

View file

@ -1184,7 +1184,10 @@ soneklerini alır.
| **Developer Kit Components** | `App.DeveloperKit.Components` · `/admin/developerkit/components` | `Sas_H_CustomComponent` | host | Custom component listesi ve **Visual Designer** girişi (§10). |
| **Developer Kit ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. |
| **Developer Kit Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | — | host | Wizard seed dosyalarının listesi, düzenlemesi, silinmesi, export/import'u. Sihirbazın kendisi ayrı bir rotadadır: `/admin/listform/wizard`. |
| **Forum Management** | `App.ForumManagement` · `/admin/forumManagement` | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Forum kategori/konu yönetimi; yayın tarafı Administration'daki `/admin/forum`. |
| **Forum Management** | `App.ForumManagement` (grup menüsü) | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Altında Categories / Topics / Posts ListForm ekranları; yayın tarafı Administration'daki `/admin/forum`. |
| **Forum Categories** | `App.ForumManagement.Categories` · `/admin/list/App.ForumManagement.Categories` | `Sas_T_ForumCategory` | tenant | Forum kategorilerinin ListForm ekranı; veri `configs/seeds/host/data/App.ForumManagement.Categories.json` dosyasından seed edilir. |
| **Forum Topics** | `App.ForumManagement.Topics` · `/admin/list/App.ForumManagement.Topics` | `Sas_T_ForumTopic` | tenant | Forum konuları; kategori lookup'ı, sayaçlar ve sabitle/kilitle/çözüldü bayrakları. |
| **Forum Posts** | `App.ForumManagement.Posts` · `/admin/list/App.ForumManagement.Posts` | `Sas_T_ForumPost` | tenant | Forum gönderileri; konu lookup'ı, kabul edilen yanıt bayrağı. |
> Menü kodu ile yetki adı **her zaman aynı değildir**: Wizard Manager'da kod
> `App.Listforms.WizardManager`, yetki `App.Listforms.Wizard`'dır; Administration'daki Forum
@ -1274,7 +1277,7 @@ görünürlük `checkPermission` ile bileşen içinde kurulur (`lowcode` §7.5).
| `/admin/settings` | `settings/Settings` | `App.Setting` |
| `/admin/files` | `admin/files/FileManager` | `App.Files` |
| `/admin/ous` · `/admin/organization` | `organization-unit/OrganizationUnits` · `hr/OrgChart` | `Abp.Identity.OrganizationUnits` · `App.Definitions.Department` |
| `/admin/forum` · `/admin/forumManagement` | `forum/Forum` · `forum/Management` | `App.ForumManagement.Publish` · `App.ForumManagement` |
| `/admin/forum` | `forum/Forum` | `App.ForumManagement.Publish` |
| `/admin/videoroom/{dashboard,list,room/:id}` | `admin/videoroom/{Dashboard,RoomList,RoomDetail}` | `App.Videoroom{,.List,.Detail}` |
| `/admin/ai` | `ai/Assistant` | `App.Definitions.AiBot.Asistant` |
| `/admin/dashboard` · `/admin/activityLog` · `/admin/changeLog` · `/admin/profile/*` | `Dashboard` · `admin/activityLog/ActivityLog` · `version/ChangeLog` · `admin/profile/Profile` | `[]` |

View file

@ -800,7 +800,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer
| Dosya yöneticisi | `/admin/files` | Klasör/dosya işlemleri. |
| Aktivite ve denetim kaydı | `/admin/activityLog`, `/admin/changeLog` | Kullanıcı hareketleri ve ABP audit log detayı. |
| Bildirimler | `/admin/profile/notification-settings`, bildirim ekranları | Kanal tercihleri ve bildirim oluşturma. |
| Forum | `/admin/forum`, `/admin/forumManagement` | Forum kullanımı ve yönetimi. |
| Forum | `/admin/forum` | Forum kullanımı; yönetim `App.ForumManagement` altındaki ListForm ekranlarıyla yapılır. |
| Intranet | Intranet dashboard ve widget'ları | Duyuru, anket, sosyal duvar, etkinlik. Hangi widget'ın hangi kolonda, hangi sırada ve hangi yetkiyle yayınlandığı `Adm_T_IntranetWidget` kaydından gelir; kayıtlar `configs/seeds/{kapsam}/data/App.DeveloperKit.IntranetWidgets.json` ile senkron tutulur. |
| Video oda | `/admin/videoroom/*` | Oda listesi, detay, planlama, katılımcı ve ekran paylaşımı. |
| Raporlar | `/admin/reports/:report/view|design/...` | DevExpress rapor görüntüleyici ve tasarımcı. |

View file

@ -44,34 +44,6 @@ public class ForumCategoryDto : FullAuditedEntityDto<Guid>
public Guid? LastPostUserId { get; set; }
}
public class CreateForumCategoryDto
{
public Guid? TenantId { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
[Required]
[StringLength(100)]
public string Slug { get; set; }
[Required]
[StringLength(500)]
public string Description { get; set; }
[StringLength(10)]
public string Icon { get; set; }
public int DisplayOrder { get; set; }
public bool IsActive { get; set; } = true;
public bool IsLocked { get; set; } = false;
}
public class UpdateForumCategoryDto : CreateForumCategoryDto
{
}
public class GetCategoriesInput : PagedAndSortedResultRequestDto
{
public bool? IsActive { get; set; }

View file

@ -14,9 +14,6 @@ public interface IForumAppService : IApplicationService
Task<PagedResultDto<ForumCategoryDto>> GetCategoriesAsync(GetCategoriesInput input);
Task<ForumCategoryDto> GetCategoryAsync(Guid id);
Task<ForumCategoryDto> GetCategoryBySlugAsync(string slug);
Task<ForumCategoryDto> CreateCategoryAsync(CreateForumCategoryDto input);
Task<ForumCategoryDto> UpdateCategoryAsync(Guid id, UpdateForumCategoryDto input);
Task DeleteCategoryAsync(Guid id);
// Topics
Task<PagedResultDto<ForumTopicDto>> GetTopicsAsync(GetTopicsInput input);

View file

@ -179,84 +179,6 @@ public class ForumAppService : PlatformAppService, IForumAppService
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
}
[Authorize("App.ForumManagement.Create")]
public async Task<ForumCategoryDto> CreateCategoryAsync(CreateForumCategoryDto input)
{
var category = new ForumCategory
{
Name = input.Name,
Slug = input.Slug,
Description = input.Description,
Icon = input.Icon,
DisplayOrder = input.DisplayOrder,
IsActive = input.IsActive,
IsLocked = input.IsLocked
};
await _categoryRepository.InsertAsync(category, autoSave: true);
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
}
[Authorize("App.ForumManagement.Update")]
public async Task<ForumCategoryDto> UpdateCategoryAsync(Guid id, UpdateForumCategoryDto input)
{
var category = await _categoryRepository.GetAsync(id);
category.Name = input.Name;
category.Slug = input.Slug;
category.Description = input.Description;
category.Icon = input.Icon;
category.DisplayOrder = input.DisplayOrder;
category.IsActive = input.IsActive;
category.IsLocked = input.IsLocked;
await _categoryRepository.UpdateAsync(category, autoSave: true);
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, autoSave: true);
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, autoSave: true);
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
}
[Authorize("App.ForumManagement.Delete")]
[UnitOfWork]
public async Task DeleteCategoryAsync(Guid id)
{
// Tüm topic ID'lerini al
var topicQueryable = await _topicRepository.GetQueryableAsync();
var topicIds = await AsyncExecuter.ToListAsync(
topicQueryable
.Where(t => t.CategoryId == id)
.Select(t => t.Id)
);
if (topicIds.Any())
{
// Bulk delete - daha performanslı
await _postRepository.DeleteAsync(p => topicIds.Contains(p.TopicId));
await _topicRepository.DeleteAsync(t => t.CategoryId == id);
}
await _categoryRepository.DeleteAsync(id);
}
// Topic management
public async Task<PagedResultDto<ForumTopicDto>> GetTopicsAsync(GetTopicsInput input)
{

View file

@ -612,11 +612,101 @@
"en": "Forum Management",
"tr": "Forum Yönetimi"
},
{
"resourceName": "Platform",
"key": "App.ForumManagement.Categories",
"en": "Forum Categories",
"tr": "Forum Kategorileri"
},
{
"resourceName": "Platform",
"key": "App.ForumManagement.Categories.Description",
"en": "Forum categories",
"tr": "Forum kategorileri"
},
{
"resourceName": "Platform",
"key": "App.ForumManagement.Topics",
"en": "Forum Topics",
"tr": "Forum Konuları"
},
{
"resourceName": "Platform",
"key": "App.ForumManagement.Topics.Description",
"en": "Forum topics",
"tr": "Forum konuları"
},
{
"resourceName": "Platform",
"key": "App.ForumManagement.Posts",
"en": "Forum Posts",
"tr": "Forum Gönderileri"
},
{
"resourceName": "Platform",
"key": "App.ForumManagement.Posts.Description",
"en": "Forum posts",
"tr": "Forum gönderileri"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.ReplyCount",
"en": "Replies",
"tr": "Yanıt Sayısı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.IsSolved",
"en": "Solved",
"tr": "Çözüldü"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.AuthorName",
"en": "Author Name",
"tr": "Yazar Adı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.AuthorTitle",
"en": "Author Title",
"tr": "Yazar Ünvanı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.PhoneNumber",
"en": "Phone Number",
"tr": "Telefon"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Topic",
"en": "Topic",
"tr": "Konu"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.IsAcceptedAnswer",
"en": "Accepted Answer",
"tr": "Kabul Edilen Yanıt"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.ParentPost",
"en": "Parent Post",
"tr": "Üst Gönderi"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.IsLocked",
"en": "Locked",
"tr": "Kilitli"
},
{
"resourceName": "Platform",
"key": "App.ForumManagement.Publish",
"en": "Forum Publishing",
"tr": "Forum Yayınlama"
"en": "Forum",
"tr": "Forum"
},
{
"resourceName": "Platform",

View file

@ -192,14 +192,6 @@
"routeType": "protected",
"authority": []
},
{
"key": "admin.forumManagement",
"path": "/admin/forumManagement",
"componentType": "normal",
"componentPath": "@/views/forum/Management",
"routeType": "protected",
"authority": ["App.ForumManagement"]
},
{
"key": "admin.ai",
"path": "/admin/ai",
@ -1076,12 +1068,45 @@
"Code": "App.ForumManagement",
"DisplayName": "App.ForumManagement",
"Order": 16,
"Url": "/admin/forumManagement",
"Url": null,
"Icon": "FcReading",
"RequiredPermissionName": "App.ForumManagement",
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.ForumManagement",
"Code": "App.ForumManagement.Categories",
"DisplayName": "App.ForumManagement.Categories",
"Order": 1,
"Url": "/admin/list/App.ForumManagement.Categories",
"Icon": "FcFolder",
"RequiredPermissionName": "App.ForumManagement.Categories",
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.ForumManagement",
"Code": "App.ForumManagement.Topics",
"DisplayName": "App.ForumManagement.Topics",
"Order": 2,
"Url": "/admin/list/App.ForumManagement.Topics",
"Icon": "FcRules",
"RequiredPermissionName": "App.ForumManagement.Topics",
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": "App.ForumManagement",
"Code": "App.ForumManagement.Posts",
"DisplayName": "App.ForumManagement.Posts",
"Order": 3,
"Url": "/admin/list/App.ForumManagement.Posts",
"Icon": "FcFeedback",
"RequiredPermissionName": "App.ForumManagement.Posts",
"IsDisabled": false,
"ModuleId": "Sas"
},
{
"ParentCode": null,
"Code": "App.Administration",

View file

@ -2723,8 +2723,17 @@
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Create",
"Name": "App.ForumManagement.Categories",
"ParentName": "App.ForumManagement",
"DisplayName": "App.ForumManagement.Categories",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Categories.Create",
"ParentName": "App.ForumManagement.Categories",
"DisplayName": "App.Platform.Create",
"IsEnabled": true,
"MultiTenancySide": 2,
@ -2732,8 +2741,17 @@
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Delete",
"ParentName": "App.ForumManagement",
"Name": "App.ForumManagement.Categories.Update",
"ParentName": "App.ForumManagement.Categories",
"DisplayName": "App.Platform.Update",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Categories.Delete",
"ParentName": "App.ForumManagement.Categories",
"DisplayName": "App.Platform.Delete",
"IsEnabled": true,
"MultiTenancySide": 2,
@ -2741,8 +2759,8 @@
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Export",
"ParentName": "App.ForumManagement",
"Name": "App.ForumManagement.Categories.Export",
"ParentName": "App.ForumManagement.Categories",
"DisplayName": "Export",
"IsEnabled": true,
"MultiTenancySide": 2,
@ -2750,8 +2768,8 @@
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Import",
"ParentName": "App.ForumManagement",
"Name": "App.ForumManagement.Categories.Import",
"ParentName": "App.ForumManagement.Categories",
"DisplayName": "Import",
"IsEnabled": true,
"MultiTenancySide": 2,
@ -2759,13 +2777,112 @@
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Update",
"Name": "App.ForumManagement.Topics",
"ParentName": "App.ForumManagement",
"DisplayName": "App.ForumManagement.Topics",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Topics.Create",
"ParentName": "App.ForumManagement.Topics",
"DisplayName": "App.Platform.Create",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Topics.Update",
"ParentName": "App.ForumManagement.Topics",
"DisplayName": "App.Platform.Update",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Topics.Delete",
"ParentName": "App.ForumManagement.Topics",
"DisplayName": "App.Platform.Delete",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Topics.Export",
"ParentName": "App.ForumManagement.Topics",
"DisplayName": "Export",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Topics.Import",
"ParentName": "App.ForumManagement.Topics",
"DisplayName": "Import",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Posts",
"ParentName": "App.ForumManagement",
"DisplayName": "App.ForumManagement.Posts",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Posts.Create",
"ParentName": "App.ForumManagement.Posts",
"DisplayName": "App.Platform.Create",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Posts.Update",
"ParentName": "App.ForumManagement.Posts",
"DisplayName": "App.Platform.Update",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Posts.Delete",
"ParentName": "App.ForumManagement.Posts",
"DisplayName": "App.Platform.Delete",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Posts.Export",
"ParentName": "App.ForumManagement.Posts",
"DisplayName": "Export",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Saas",
"Name": "App.ForumManagement.Posts.Import",
"ParentName": "App.ForumManagement.Posts",
"DisplayName": "Import",
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{
"GroupName": "App.Administration",
"Name": "App.Administration.Definitions",

View file

@ -476,6 +476,15 @@ public static class SeedConsts
public const string Forum = Prefix.App + ".Forum";
public static class ForumManagement
{
public const string Default = Prefix.App + ".ForumManagement";
public const string Categories = Default + ".Categories";
public const string Topics = Default + ".Topics";
public const string Posts = Default + ".Posts";
}
//Administration
public const string Administration = Prefix.App + ".Administration";

View file

@ -1,38 +1,4 @@
{
"ForumCategories": [
{
"Name": "Genel Tartışma",
"Slug": "genel-tartisma",
"Description": "Her türlü konunun tartışılabileceği genel forum alanı",
"Icon": "💬",
"DisplayOrder": 1,
"IsActive": true
},
{
"Name": "Teknik Destek",
"Slug": "teknik-destek",
"Description": "Teknik sorunlar ve çözümler için destek forumu",
"Icon": "🔧",
"DisplayOrder": 2,
"IsActive": true
},
{
"Name": "Öneriler",
"Slug": "oneriler",
"Description": "Platform geliştirmeleri için öneri ve istekler",
"Icon": "💡",
"DisplayOrder": 3,
"IsActive": true
},
{
"Name": "Duyurular",
"Slug": "duyurular",
"Description": "Platform duyuruları ve güncellemeler",
"Icon": "📢",
"DisplayOrder": 4,
"IsActive": true
}
],
"Abouts": [
{
"stats": [

View file

@ -32,7 +32,6 @@ public class TenantSeederDto
//Forum
public List<ForumCategorySeedDto> ForumCategories { get; set; }
//Intranet
public List<SocialPostSeedDto> SocialPosts { get; set; }
@ -132,16 +131,6 @@ public class EventPhotoSeedDto
public string Url { get; set; }
}
public class ForumCategorySeedDto
{
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 class ServiceSeedDto
{
public string Icon { get; set; }
@ -228,7 +217,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
private readonly IRepository<About, Guid> _aboutRepository;
private readonly IRepository<Service, Guid> _servicesRepository;
private readonly IRepository<Contact, Guid> _contactRepository;
private readonly IRepository<ForumCategory, Guid> _forumCategoryRepository;
private readonly IIdentityUserRepository _repositoryUser;
private readonly IRepository<SocialPost, Guid> _socialPostRepository;
private readonly IRepository<SocialMedia, Guid> _socialMediaRepository;
@ -249,7 +237,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
IRepository<About, Guid> aboutRepository,
IRepository<Service, Guid> servicesRepository,
IRepository<Contact, Guid> contactRepository,
IRepository<ForumCategory, Guid> forumCategoryRepository,
IRepository<SocialPost, Guid> socialPostRepository,
IRepository<SocialMedia, Guid> socialMediaRepository,
@ -268,7 +255,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
_servicesRepository = servicesRepository;
_aboutRepository = aboutRepository;
_contactRepository = contactRepository;
_forumCategoryRepository = forumCategoryRepository;
_socialPostRepository = socialPostRepository;
_socialMediaRepository = socialMediaRepository;
_socialPollOptionRepository = socialPollOptionRepository;
@ -293,25 +279,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
foreach (var item in items.ForumCategories)
{
var exists = await _forumCategoryRepository.AnyAsync(x => x.Name == item.Name);
if (!exists)
{
var newCategory = new ForumCategory
{
Name = item.Name,
Slug = item.Slug,
Description = item.Description,
Icon = item.Icon,
DisplayOrder = item.DisplayOrder
};
await _forumCategoryRepository.InsertAsync(newCategory, autoSave: true);
}
}
foreach (var item in items.Services)
{
var exists = await _servicesRepository.AnyAsync(x => x.Title == item.Title);

View file

@ -0,0 +1,48 @@
{
"ListFormCode": "App.ForumManagement.Categories",
"KeyFieldName": "Id",
"GeneratedAt": "2026-09-04T00:00:00Z",
"Order": 0,
"Rows": [
{
"Id": "c37dd21d-cbcb-535c-a97d-be0e2f5570d1",
"Name": "Genel Tartışma",
"Slug": "genel-tartisma",
"Description": "Her türlü konunun tartışılabileceği genel forum alanı",
"Icon": "💬",
"DisplayOrder": 1,
"IsActive": true,
"IsLocked": false
},
{
"Id": "1f9b8e9a-6b39-512f-9edc-33a9b5948a84",
"Name": "Teknik Destek",
"Slug": "teknik-destek",
"Description": "Teknik sorunlar ve çözümler için destek forumu",
"Icon": "🔧",
"DisplayOrder": 2,
"IsActive": true,
"IsLocked": false
},
{
"Id": "657f4136-42b1-534f-afe1-fbb8045a023c",
"Name": "Öneriler",
"Slug": "oneriler",
"Description": "Platform geliştirmeleri için öneri ve istekler",
"Icon": "💡",
"DisplayOrder": 3,
"IsActive": true,
"IsLocked": false
},
{
"Id": "3a505a92-5302-5852-a95b-1764fd60ad77",
"Name": "Duyurular",
"Slug": "duyurular",
"Description": "Platform duyuruları ve güncellemeler",
"Icon": "📢",
"DisplayOrder": 4,
"IsActive": true,
"IsLocked": false
}
]
}

View file

@ -1,4 +1,4 @@
export const ROUTES_ENUM = {
export const ROUTES_ENUM = {
setup: '/setup',
public: {
home: '/home',
@ -34,7 +34,6 @@ export const ROUTES_ENUM = {
wizardManager: '/admin/listform/wizardManager',
edit: '/admin/listform/edit/:listFormCode',
},
forumManagement: '/admin/forumManagement',
developerKit: {
dashboard: '/admin/developerkit',
entities: '/admin/developerkit/entities',

View file

@ -1,4 +1,4 @@
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
import apiService from '@/services/api.service'
export interface ForumSearchResult {
@ -27,17 +27,6 @@ export interface PaginatedResponse<T> {
totalPages: number
}
export interface CreateCategoryRequest {
name: string
slug: string
description: string
icon: string
displayOrder: number
isActive: boolean
isLocked: boolean
tenantId?: string
}
export interface CreateTopicRequest {
title: string
content: string
@ -116,49 +105,6 @@ class ForumService {
return response.data
}
async createCategory(data: CreateCategoryRequest): Promise<ForumCategory> {
const response = await apiService.fetchData<ForumCategory>({
url: '/api/app/forum/category',
method: 'POST',
data: {...data},
})
return response.data
}
async updateCategory(id: string, data: Partial<CreateCategoryRequest>): Promise<ForumCategory> {
const response = await apiService.fetchData<ForumCategory>({
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/${id}/category`,
method: 'DELETE',
})
}
// Topics
async getTopics(params: TopicListParams = {}): Promise<PaginatedResponse<ForumTopic>> {
const response = await apiService.fetchData<PaginatedResponse<ForumTopic>>({

View file

@ -1,82 +0,0 @@
import { useEffect } from 'react'
import { useForumData } from './useForumData'
import { AdminView } from './admin/AdminView'
import { ForumErrorBanner } from './ForumErrorBanner'
import { Container } from '@/components/shared'
import PageTitle from '@/components/shared/PageTitle'
import { useLocalization } from '@/utils/hooks/useLocalization'
export function Management() {
const {
categories,
topics,
posts,
totalCounts,
loading,
error,
createCategory,
updateCategory,
updateCategoryLockState,
updateCategoryActiveState,
deleteCategory,
createTopic,
updateTopic,
deleteTopic,
pinTopic,
unpinTopic,
lockTopic,
unlockTopic,
solvedTopic,
unsolvedTopic,
createPost,
updatePost,
deletePost,
markPostAsAcceptedAnswer,
unmarkPostAsAcceptedAnswer,
clearError,
} = useForumData()
const { translate } = useLocalization()
useEffect(() => {
if (!error) return
const timer = setTimeout(clearError, 5000)
return () => clearTimeout(timer)
}, [error, clearError])
return (
<Container>
<PageTitle title={translate('::App.ForumManagement')} />
<ForumErrorBanner error={error} onDismiss={clearError} />
<AdminView
categories={categories}
topics={topics}
posts={posts}
totalCounts={totalCounts}
loading={loading}
onCreateCategory={createCategory}
onUpdateCategory={updateCategory}
onUpdateCategoryLockState={updateCategoryLockState}
onUpdateCategoryActiveState={updateCategoryActiveState}
onDeleteCategory={deleteCategory}
onCreateTopic={createTopic}
onUpdateTopic={updateTopic}
onDeleteTopic={deleteTopic}
onPinTopic={pinTopic}
onUnpinTopic={unpinTopic}
onLockTopic={lockTopic}
onUnlockTopic={unlockTopic}
onMarkTopicAsSolved={solvedTopic}
onMarkTopicAsUnsolved={unsolvedTopic}
onCreatePost={createPost}
onUpdatePost={updatePost}
onDeletePost={deletePost}
onMarkPostAsAcceptedAnswer={markPostAsAcceptedAnswer}
onUnmarkPostAsAcceptedAnswer={unmarkPostAsAcceptedAnswer}
/>
</Container>
)
}
export default Management

View file

@ -1,174 +0,0 @@
import { useState } from 'react'
import { FaFolder, FaCommentAlt, FaFileAlt, FaChartBar } from 'react-icons/fa'
import { CategoryManagement } from './CategoryManagement'
import { TopicManagement } from './TopicManagement'
import { PostManagement } from './PostManagement'
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
import {
CreateCategoryRequest,
CreatePostRequest,
CreateTopicRequest,
} from '@/services/forum.service'
import { AdminStats } from './Dashboard'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Button } from '@/components/ui'
interface AdminViewProps {
categories: ForumCategory[]
topics: ForumTopic[]
posts: ForumPost[]
totalCounts: { categories: number; topics: number; posts: number }
loading: boolean
onCreateCategory: (category: CreateCategoryRequest) => Promise<unknown>
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<unknown>
onUpdateCategoryLockState: (id: string) => Promise<unknown>
onUpdateCategoryActiveState: (id: string) => Promise<unknown>
onDeleteCategory: (id: string) => Promise<unknown>
onCreateTopic: (topic: CreateTopicRequest) => Promise<unknown>
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<unknown>
onDeleteTopic: (id: string) => Promise<unknown>
onPinTopic: (id: string) => Promise<unknown>
onUnpinTopic: (id: string) => Promise<unknown>
onLockTopic: (id: string) => Promise<unknown>
onUnlockTopic: (id: string) => Promise<unknown>
onMarkTopicAsSolved: (id: string) => Promise<unknown>
onMarkTopicAsUnsolved: (id: string) => Promise<unknown>
onCreatePost: (post: CreatePostRequest) => Promise<unknown>
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<unknown>
onDeletePost: (id: string) => Promise<unknown>
onMarkPostAsAcceptedAnswer: (id: string) => Promise<unknown>
onUnmarkPostAsAcceptedAnswer: (id: string) => Promise<unknown>
}
type AdminSection = 'stats' | 'categories' | 'topics' | 'posts'
export function AdminView({
categories,
topics,
posts,
totalCounts,
loading,
onCreateCategory,
onUpdateCategory,
onDeleteCategory,
onUpdateCategoryLockState,
onUpdateCategoryActiveState,
onCreateTopic,
onUpdateTopic,
onDeleteTopic,
onPinTopic,
onUnpinTopic,
onLockTopic,
onUnlockTopic,
onMarkTopicAsSolved,
onMarkTopicAsUnsolved,
onCreatePost,
onUpdatePost,
onDeletePost,
onMarkPostAsAcceptedAnswer,
onUnmarkPostAsAcceptedAnswer,
}: AdminViewProps) {
const [activeSection, setActiveSection] = useState<AdminSection>('stats')
const { translate } = useLocalization()
const navigationItems = [
{
id: 'stats' as AdminSection,
label: translate('::App.Forum.Dashboard'),
icon: FaChartBar,
},
{
id: 'categories' as AdminSection,
label: translate('::App.Platform.Category'),
icon: FaFolder,
},
{
id: 'topics' as AdminSection,
label: translate('::App.ForumDashboard.Topics'),
icon: FaCommentAlt,
},
{
id: 'posts' as AdminSection,
label: translate('::App.ForumDashboard.Posts'),
icon: FaFileAlt,
},
]
return (
<div className="flex flex-col lg:flex-row gap-4 mt-3">
{/* Sidebar Navigation */}
<div className="lg:w-64 flex-shrink-0 p-4 bg-gray-50 dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<nav className="space-y-2">
{navigationItems.map((item) => {
const Icon = item.icon
const isActive = activeSection === item.id
return (
<Button
key={item.id}
variant={isActive ? 'solid' : 'plain'}
active={isActive}
aria-current={isActive ? 'page' : undefined}
className="flex w-full items-center space-x-3 px-4 py-3 text-left transition-colors"
onClick={() => setActiveSection(item.id)}
>
<Icon className="h-5 w-5" />
<span className="font-medium">{item.label}</span>
</Button>
)
})}
</nav>
</div>
{/* Main Content */}
<div className="flex-1 bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
{activeSection === 'stats' && (
<AdminStats categories={categories} topics={topics} posts={posts} totalCounts={totalCounts} />
)}
{activeSection === 'categories' && (
<CategoryManagement
categories={categories}
loading={loading}
onCreateCategory={onCreateCategory}
onUpdateCategory={onUpdateCategory}
onDeleteCategory={onDeleteCategory}
onUpdateCategoryLockState={onUpdateCategoryLockState}
onUpdateCategoryActiveState={onUpdateCategoryActiveState}
/>
)}
{activeSection === 'topics' && (
<TopicManagement
topics={topics}
totalCount={totalCounts.topics}
categories={categories}
loading={loading}
onCreateTopic={onCreateTopic}
onUpdateTopic={onUpdateTopic}
onDeleteTopic={onDeleteTopic}
onPinTopic={onPinTopic}
onUnpinTopic={onUnpinTopic}
onLockTopic={onLockTopic}
onUnlockTopic={onUnlockTopic}
onMarkTopicAsSolved={onMarkTopicAsSolved}
onMarkTopicAsUnsolved={onMarkTopicAsUnsolved}
/>
)}
{activeSection === 'posts' && (
<PostManagement
posts={posts}
totalCount={totalCounts.posts}
topics={topics}
loading={loading}
onCreatePost={onCreatePost}
onUpdatePost={onUpdatePost}
onDeletePost={onDeletePost}
onMarkPostAsAcceptedAnswer={onMarkPostAsAcceptedAnswer}
onUnmarkPostAsAcceptedAnswer={onUnmarkPostAsAcceptedAnswer}
/>
)}
</div>
</div>
)
}

View file

@ -1,423 +0,0 @@
import { useState } from 'react'
import {
FaPlus,
FaEdit,
FaTrash,
FaLock,
FaUnlock,
FaEye,
FaEyeSlash,
FaSpinner,
} from 'react-icons/fa'
import { ForumCategory } from '@/proxy/forum/forum'
import { CreateCategoryRequest } from '@/services/forum.service'
import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { FormContainer, FormItem } from '@/components/ui/Form'
import { Input, Checkbox, Button, Select, Dialog } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
import { CategoryIconOption, resolveCategoryIconOptions } from './categoryIcons'
const FIELD_CLASS =
'w-full border border-gray-300 dark:border-gray-700 rounded-lg px-3 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500'
const CategorySchema = Yup.object().shape({
name: Yup.string().trim().required(),
slug: Yup.string().trim().required(),
description: Yup.string().trim().required(),
icon: Yup.string(),
displayOrder: Yup.number().required(),
isActive: Yup.boolean(),
isLocked: Yup.boolean(),
})
interface CategoryManagementProps {
categories: ForumCategory[]
loading: boolean
onCreateCategory: (category: CreateCategoryRequest) => Promise<unknown>
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<unknown>
onDeleteCategory: (id: string) => Promise<unknown>
onUpdateCategoryLockState: (id: string) => Promise<unknown>
onUpdateCategoryActiveState: (id: string) => Promise<unknown>
}
export function CategoryManagement({
categories,
loading,
onCreateCategory,
onUpdateCategory,
onDeleteCategory,
onUpdateCategoryLockState,
onUpdateCategoryActiveState,
}: CategoryManagementProps) {
const { translate } = useLocalization()
const { tenant } = useStoreState((state) => state.auth)
const [showCreateForm, setShowCreateForm] = useState(false)
const [editingCategory, setEditingCategory] = useState<ForumCategory | null>(null)
const [showConfirm, setShowConfirm] = useState(false)
const [categoryToDelete, setCategoryToDelete] = useState<ForumCategory | null>(null)
const confirmDeleteCategory = (category: ForumCategory) => {
setCategoryToDelete(category)
setShowConfirm(true)
}
const handleEdit = (category: ForumCategory) => {
setEditingCategory(category)
setShowCreateForm(true)
}
const handleCloseForm = () => {
setShowCreateForm(false)
setEditingCategory(null)
}
const handleToggleActive = async (category: ForumCategory) => {
try {
await onUpdateCategoryActiveState(category.id)
} catch (error) {
console.error('Error toggling category status:', error)
}
}
const handleToggleLocked = async (category: ForumCategory) => {
try {
await onUpdateCategoryLockState(category.id)
} catch (error) {
console.error('Error toggling category lock:', error)
}
}
return (
<div className="space-y-3">
{/* Create/Edit Modal */}
<Dialog
isOpen={showCreateForm}
width={720}
onClose={handleCloseForm}
onRequestClose={handleCloseForm}
>
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingCategory
? translate('::App.ForumCategoryManagement.EditCategory')
: translate('::App.ForumCategoryManagement.AddCategory')}
</h3>
</div>
<div className="pt-4 max-h-[70vh] overflow-y-auto">
<Formik
enableReinitialize
initialValues={{
name: editingCategory?.name ?? '',
slug: editingCategory?.slug ?? '',
description: editingCategory?.description ?? '',
icon: editingCategory?.icon ?? '',
displayOrder: editingCategory?.displayOrder ?? 0,
isActive: editingCategory?.isActive ?? true,
isLocked: editingCategory?.isLocked ?? false,
tenantId: tenant?.tenantId ?? '',
}}
validationSchema={CategorySchema}
onSubmit={async (values, { setSubmitting, resetForm }) => {
try {
setSubmitting(true)
if (editingCategory) {
await onUpdateCategory(editingCategory.id, values)
} else {
await onCreateCategory(values)
}
resetForm()
setShowCreateForm(false)
setEditingCategory(null)
} catch (error) {
console.error('Error submitting form:', error)
} finally {
setSubmitting(false)
}
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form>
<FormContainer className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormItem
asterisk
label={translate('::App.Listform.ListformField.Name')}
invalid={!!errors.name && !!touched.name}
errorMessage={errors.name}
>
<Field name="name" className={FIELD_CLASS} component={Input} />
</FormItem>
<FormItem
asterisk
label={translate('::App.Listform.ListformField.Slug')}
invalid={!!errors.slug && !!touched.slug}
errorMessage={errors.slug}
>
<Field name="slug" className={FIELD_CLASS} component={Input} />
</FormItem>
</div>
<FormItem
asterisk
label={translate('::App.Listform.ListformField.Description')}
invalid={!!errors.description && !!touched.description}
errorMessage={errors.description}
>
<Field
textArea
name="description"
className={FIELD_CLASS}
component={Input}
/>
</FormItem>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormItem
label={translate('::App.MenuField.Icon')}
invalid={!!errors.icon && !!touched.icon}
errorMessage={errors.icon}
>
<Field name="icon">
{({ field, form }: FieldProps<string>) => {
const iconOptions = resolveCategoryIconOptions(values.icon)
return (
<Select<CategoryIconOption>
isClearable
field={field}
form={form}
options={iconOptions}
placeholder="💬"
value={iconOptions.filter((option) => option.value === values.icon)}
formatOptionLabel={(option) => (
<span className="flex items-center gap-2">
<span className="text-lg">{option.value}</span>
<span>{option.label}</span>
</span>
)}
onChange={(option) =>
form.setFieldValue('icon', option?.value ?? '')
}
/>
)
}}
</Field>
</FormItem>
<FormItem
asterisk
label={translate('::App.Listform.ListformField.DisplayOrder')}
invalid={!!errors.displayOrder && !!touched.displayOrder}
errorMessage={errors.displayOrder}
>
<Field
name="displayOrder"
className={FIELD_CLASS}
placeholder="0"
type="number"
component={Input}
/>
</FormItem>
</div>
<div className="flex items-center space-x-4 pt-6">
<FormItem label={translate('::App.Listform.ListformField.Status')}>
<Field as={Checkbox} name="isActive" />
</FormItem>
<FormItem label={translate('::App.Platform.Locked')}>
<Field as={Checkbox} name="isLocked" />
</FormItem>
</div>
<div className="mt-4 flex justify-end space-x-3">
<Button variant="plain" type="button" onClick={handleCloseForm}>
{translate('::App.Platform.Cancel')}
</Button>
<Button variant="solid" type="submit" loading={isSubmitting}>
{editingCategory
? translate('::App.Platform.Update')
: translate('::App.Platform.Create')}
</Button>
</div>
</FormContainer>
</Form>
)}
</Formik>
</div>
</Dialog>
{/* Categories List */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="flex items-center justify-between px-3 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{translate('::App.Platform.Category')} ({categories.length})
</h3>
<Button
size="sm"
variant="solid"
disabled={loading}
className="flex items-center space-x-2"
onClick={() => setShowCreateForm(true)}
>
<FaPlus className="w-4 h-4" />
<span>{translate('::App.ForumCategoryManagement.AddCategory')}</span>
</Button>
</div>
{loading ? (
<div className="p-8 text-center">
<FaSpinner className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600 dark:text-blue-400" />
<p className="text-gray-500 dark:text-gray-400">
{translate('::App.ForumCategoryManagement.Loadingcategories')}
</p>
</div>
) : (
<div className="divide-y divide-gray-200 dark:divide-gray-800">
{categories.length === 0 && (
<p className="p-8 text-center text-gray-500 dark:text-gray-400">
{translate('::App.Platform.NoDataFound')}
</p>
)}
{[...categories]
.sort((a, b) => a.displayOrder - b.displayOrder)
.map((category) => (
<div
key={category.id}
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-start space-x-4">
<div className="text-2xl">{category.icon}</div>
<div>
<div className="flex items-center space-x-2 mb-1">
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{category.name}
</h4>
{!category.isActive && (
<span className="px-2 py-1 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 text-xs rounded-full">
{translate('::App.Platform.Passive')}
</span>
)}
{category.isLocked && (
<span className="px-2 py-1 bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300 text-xs rounded-full">
{translate('::App.Platform.Locked')}
</span>
)}
</div>
<p className="text-gray-600 dark:text-gray-300 mb-2">
{category.description}
</p>
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
<span>
{category.topicCount} {translate('::App.ForumDashboard.Topics')}
</span>
<span>
{category.postCount} {translate('::App.ForumDashboard.Posts')}
</span>
<span>
{translate('::App.MenuField.Order')}: {category.displayOrder}
</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
size="sm"
className={`p-1 rounded-lg transition-colors ${
category.isActive
? 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900'
: 'text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900'
}`}
title={
category.isActive
? translate('::App.ForumCategoryManagement.HideCategory')
: translate('::App.ForumCategoryManagement.ShowCategory')
}
onClick={() => handleToggleActive(category)}
>
{category.isActive ? (
<FaEye className="w-4 h-4" />
) : (
<FaEyeSlash className="w-4 h-4" />
)}
</Button>
<Button
size="sm"
className={`p-1 rounded-lg transition-colors ${
category.isLocked
? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900'
: 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900'
}`}
title={
category.isLocked
? translate('::App.ForumCategoryManagement.UnlockCategory')
: translate('::App.ForumCategoryManagement.LockCategory')
}
onClick={() => handleToggleLocked(category)}
>
{category.isLocked ? (
<FaLock className="w-4 h-4" />
) : (
<FaUnlock className="w-4 h-4" />
)}
</Button>
<Button
size="sm"
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
title={translate('::App.ForumCategoryManagement.EditCategory')}
onClick={() => handleEdit(category)}
>
<FaEdit className="w-4 h-4" />
</Button>
<Button
size="sm"
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
title={translate('::App.ForumCategoryManagement.DeleteCategory')}
onClick={() => confirmDeleteCategory(category)}
>
<FaTrash className="w-4 h-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{categoryToDelete && (
<ConfirmDialog
isOpen={showConfirm}
type="danger"
title={translate('::App.Platform.DeleteConfirmation')}
confirmText={translate('::App.Platform.Delete')}
cancelText={translate('::App.Platform.Cancel')}
onCancel={() => {
setShowConfirm(false)
setCategoryToDelete(null)
}}
onConfirm={async () => {
try {
if (categoryToDelete) {
await onDeleteCategory(categoryToDelete.id)
}
} catch (error) {
console.error('Error deleting category:', error)
} finally {
setShowConfirm(false)
setCategoryToDelete(null)
}
}}
></ConfirmDialog>
)}
</div>
)
}

View file

@ -1,161 +0,0 @@
import { useMemo } from 'react'
import Widget from '@/components/ui/Widget/Widget'
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
import { useLocalization } from '@/utils/hooks/useLocalization'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/tr'
// Plugin kaydı modül seviyesinde yapılır; her render'da tekrarlanması gereksiz iştir.
dayjs.extend(relativeTime)
const MAX_ACTIVITIES = 5
interface AdminStatsProps {
categories: ForumCategory[]
topics: ForumTopic[]
posts: ForumPost[]
totalCounts: { categories: number; topics: number; posts: number }
}
interface Activity {
id: string
message: string
color: string
date: Date
}
export function AdminStats({ categories, topics, posts, totalCounts }: AdminStatsProps) {
const { translate, cultureName } = useLocalization()
const activeCategories = categories.filter((c) => c.isActive).length
const solvedTopics = topics.filter((t) => t.isSolved).length
const acceptedAnswers = posts.filter((p) => p.isAcceptedAnswer).length
const stats = [
{
key: 'categories',
title: translate('::App.ForumDashboard.TotalCategories'),
value: totalCounts.categories,
subTitle: translate('::App.ForumDashboard.ActiveCategories', { 0: activeCategories }),
icon: 'FaFolder' as const,
color: 'blue' as const,
},
{
key: 'topics',
title: translate('::App.ForumDashboard.TotalTopics'),
value: totalCounts.topics,
subTitle: translate('::App.ForumDashboard.SolvedTopics', { 0: solvedTopics }),
icon: 'FaCommentDots' as const,
color: 'green' as const,
},
{
key: 'posts',
title: translate('::App.ForumDashboard.TotalPosts'),
value: totalCounts.posts,
subTitle: translate('::App.ForumDashboard.AcceptedAnswers', { 0: acceptedAnswers }),
icon: 'FaFileAlt' as const,
color: 'orange' as const,
},
{
key: 'engagement',
title: translate('::App.ForumDashboard.EngagementRate'),
value:
totalCounts.topics > 0
? Math.round((totalCounts.posts / totalCounts.topics) * 100) / 100
: 0,
subTitle: translate('::App.ForumDashboard.PostsPerTopic'),
icon: 'FaChartLine' as const,
color: 'purple' as const,
},
]
const latestActivities = useMemo<Activity[]>(() => {
const categoryNameById = new Map(categories.map((c) => [c.id, c.name]))
const activities: Activity[] = []
topics.forEach((topic) => {
if (!topic.creationTime) return
activities.push({
id: `topic-${topic.id}`,
message: `${translate('::App.ForumTopicManagement.NewTopic')}${
categoryNameById.get(topic.categoryId) ?? translate('::App.Platform.Unknown')
}`,
color: 'bg-blue-500',
date: new Date(topic.creationTime),
})
})
posts.forEach((post) => {
if (!post.isAcceptedAnswer || !post.creationTime) return
activities.push({
id: `post-${post.id}`,
message: translate('::App.ForumDashboard.Postmarked'),
color: 'bg-emerald-500',
date: new Date(post.creationTime),
})
})
categories.forEach((category) => {
if (!category.creationTime) return
activities.push({
id: `category-${category.id}`,
message: `${translate('::App.ForumDashboard.NewCategoryCreated')}${category.name}`,
color: 'bg-orange-500',
date: new Date(category.creationTime),
})
})
return activities
.sort((a, b) => b.date.getTime() - a.date.getTime())
.slice(0, MAX_ACTIVITIES)
}, [categories, posts, topics, translate])
return (
<div className="space-y-4">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{stats.map((stat) => (
<Widget
key={stat.key}
title={stat.title}
value={stat.value}
subTitle={stat.subTitle}
icon={stat.icon}
color={stat.color}
/>
))}
</div>
{/* Recent Activity */}
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-900">
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
{translate('::App.ForumDashboard.RecentActivity')}
</h3>
<div className="space-y-4">
{latestActivities.length === 0 ? (
<p className="text-sm text-gray-500 dark:text-gray-400">
{translate('::App.Platform.NoDataFound')}
</p>
) : (
latestActivities.map((activity) => (
<div
key={activity.id}
className="flex items-start space-x-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-800"
>
<div className={`mt-2 h-2 w-2 ${activity.color} rounded-full`} />
<div>
<p className="text-sm text-gray-900 dark:text-white">{activity.message}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{dayjs(activity.date)
.locale(cultureName?.startsWith('tr') ? 'tr' : 'en')
.fromNow()}
</p>
</div>
</div>
))
)}
</div>
</div>
</div>
)
}

View file

@ -1,416 +0,0 @@
import { useState } from 'react'
import {
FaPlus,
FaEdit,
FaTrashAlt,
FaCheckCircle,
FaCircle,
FaHeart,
FaSpinner,
} from 'react-icons/fa'
import { ForumPost, ForumTopic } from '@/proxy/forum/forum'
import { CreatePostRequest } from '@/services/forum.service'
import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor'
import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { FormContainer, FormItem, Button, Dialog } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
import { formatForumDate, sanitizeForumHtml, stripHtml } from '../forum/utils'
import {
fontFamilyOptions,
fontSizeOptions,
fontValues,
headerOptions,
headerValues,
sizeValues,
} from '@/proxy/reports/data'
const postValidationSchema = Yup.object().shape({
topicId: Yup.string().required(),
content: Yup.string().test('not-empty', 'Content is required', (value) => !!stripHtml(value)),
})
interface PostManagementProps {
posts: ForumPost[]
totalCount: number
topics: ForumTopic[]
loading: boolean
onCreatePost: (post: CreatePostRequest) => Promise<unknown>
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<unknown>
onDeletePost: (id: string) => Promise<unknown>
onMarkPostAsAcceptedAnswer: (id: string) => Promise<unknown>
onUnmarkPostAsAcceptedAnswer: (id: string) => Promise<unknown>
}
export function PostManagement({
posts,
totalCount,
topics,
loading,
onCreatePost,
onUpdatePost,
onDeletePost,
onMarkPostAsAcceptedAnswer,
onUnmarkPostAsAcceptedAnswer,
}: PostManagementProps) {
const { translate } = useLocalization()
const { tenant } = useStoreState((state) => state.auth)
const [showCreateForm, setShowCreateForm] = useState(false)
const [editingPost, setEditingPost] = useState<ForumPost | null>(null)
const [showConfirm, setShowConfirm] = useState(false)
const [postToDelete, setPostToDelete] = useState<ForumPost | null>(null)
const confirmDeletePost = (post: ForumPost) => {
setPostToDelete(post)
setShowConfirm(true)
}
const handleEdit = (post: ForumPost) => {
setEditingPost(post)
setShowCreateForm(true)
}
const handleCloseForm = () => {
setShowCreateForm(false)
setEditingPost(null)
}
const handleToggleAcceptedAnswer = async (post: ForumPost) => {
try {
if (post.isAcceptedAnswer) {
await onUnmarkPostAsAcceptedAnswer(post.id)
} else {
await onMarkPostAsAcceptedAnswer(post.id)
}
} catch (error) {
console.error('Error toggling accepted answer:', error)
}
}
const getTopicTitle = (topicId: string) =>
topics.find((t) => t.id === topicId)?.title ?? translate('::App.Platform.Unknown')
return (
<div className="space-y-3">
{/* Create/Edit Modal */}
<Dialog
isOpen={showCreateForm}
width={900}
onClose={handleCloseForm}
onRequestClose={handleCloseForm}
>
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingPost
? translate('::App.ForumPostManagement.EditPost')
: translate('::App.ForumPostManagement.AddPost')}
</h3>
</div>
<div className="pt-4 max-h-[70vh] overflow-y-auto">
<Formik
enableReinitialize
initialValues={{
topicId: editingPost?.topicId ?? '',
content: editingPost?.content ?? '',
isAcceptedAnswer: editingPost?.isAcceptedAnswer ?? false,
tenantId: tenant?.tenantId ?? '',
}}
validationSchema={postValidationSchema}
onSubmit={async (values, { setSubmitting, resetForm }) => {
try {
if (editingPost) {
await onUpdatePost(editingPost.id, values)
} else {
// parentPostId gönderilmez: yönetim ekranından açılan post her zaman kök gönderidir.
await onCreatePost({
topicId: values.topicId,
content: values.content,
tenantId: values.tenantId || undefined,
})
}
resetForm()
setShowCreateForm(false)
setEditingPost(null)
} catch (error) {
console.error('Error submitting post:', error)
} finally {
setSubmitting(false)
}
}}
>
{({ values, setFieldValue, errors, touched, isSubmitting }) => (
<Form>
<FormContainer className="space-y-4">
<FormItem
asterisk
label={translate('::App.ForumTopicManagement.Baslik')}
invalid={!!errors.topicId && touched.topicId}
errorMessage={errors.topicId}
>
<Field
as="select"
name="topicId"
className="w-full border border-gray-300 dark:border-gray-700 rounded-lg px-3 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
>
<option value="">{translate('::App.ForumPostManagement.SelectTopic')}</option>
{topics.map((topic) => (
<option key={topic.id} value={topic.id}>
{topic.title}
</option>
))}
</Field>
</FormItem>
<FormItem
asterisk
label={translate('::App.Listform.ListformField.Content')}
invalid={!!errors.content && touched.content}
errorMessage={errors.content}
>
<Field name="content">
{({ field }: FieldProps) => (
<HtmlEditor
value={field.value}
height={400}
placeholder={translate('::App.ForumPostManagement.MessageNew')}
onValueChanged={(e) => setFieldValue('content', e.value)}
>
<MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" />
<Toolbar multiline={true}>
<Item name="undo" />
<Item name="redo" />
<Item name="separator" />
<Item
name="size"
acceptedValues={sizeValues}
options={fontSizeOptions}
/>
<Item
name="font"
acceptedValues={fontValues}
options={fontFamilyOptions}
/>
<Item name="separator" />
<Item name="bold" />
<Item name="italic" />
<Item name="strike" />
<Item name="underline" />
<Item name="separator" />
<Item name="alignLeft" />
<Item name="alignCenter" />
<Item name="alignRight" />
<Item name="alignJustify" />
<Item name="separator" />
<Item name="orderedList" />
<Item name="bulletList" />
<Item name="separator" />
<Item
name="header"
acceptedValues={headerValues}
options={headerOptions}
/>
<Item name="separator" />
<Item name="color" />
<Item name="background" />
<Item name="separator" />
<Item name="link" />
<Item name="image" />
<Item name="separator" />
<Item name="clear" />
<Item name="codeBlock" />
<Item name="blockquote" />
<Item name="separator" />
<Item name="insertTable" />
<Item name="deleteTable" />
<Item name="insertRowAbove" />
<Item name="insertRowBelow" />
<Item name="deleteRow" />
<Item name="insertColumnLeft" />
<Item name="insertColumnRight" />
<Item name="deleteColumn" />
</Toolbar>
</HtmlEditor>
)}
</Field>
</FormItem>
<FormItem label={translate('::App.ForumPostManagement.AcceptedAnswer')}>
<label className="flex items-center">
<Field type="checkbox" name="isAcceptedAnswer" className="mr-2" />
{translate('::App.ForumPostManagement.MarkAsAcceptedAnswer')}
</label>
</FormItem>
<div className="flex justify-end space-x-3 pt-2">
<Button variant="plain" type="button" onClick={handleCloseForm}>
{translate('::App.Platform.Cancel')}
</Button>
<Button
variant="solid"
type="submit"
loading={isSubmitting}
disabled={!values.topicId || !stripHtml(values.content)}
>
{editingPost
? translate('::App.Platform.Update')
: translate('::App.Platform.Create')}
</Button>
</div>
</FormContainer>
</Form>
)}
</Formik>
</div>
</Dialog>
{/* Posts List */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="flex items-center justify-between px-3 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{translate('::App.ForumDashboard.Posts')} ({totalCount})
</h3>
<Button
size="sm"
variant="solid"
disabled={loading}
className="flex items-center space-x-2"
onClick={() => setShowCreateForm(true)}
>
<FaPlus className="w-4 h-4" />
<span>{translate('::App.ForumPostManagement.AddPost')}</span>
</Button>
</div>
{loading ? (
<div className="p-8 text-center">
<FaSpinner className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600 dark:text-blue-400" />
<p className="text-gray-500 dark:text-gray-400">{translate('::App.ForumPostManagement.Loadingtopics')}</p>
</div>
) : (
<div className="divide-y divide-gray-200 dark:divide-gray-800">
{posts.length === 0 && (
<p className="p-8 text-center text-gray-500 dark:text-gray-400">
{translate('::App.Platform.NoDataFound')}
</p>
)}
{[...posts]
.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 dark:hover:bg-gray-800 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{post.authorName}</h4>
{post.isAcceptedAnswer && (
<div className="flex items-center space-x-1 bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300 px-2 py-1 rounded-full text-xs">
<FaCheckCircle className="w-4 h-4" />
<span>{translate('::App.ForumPostManagement.AcceptedAnswer')}</span>
</div>
)}
</div>
<div className="mb-3">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
{translate('::App.ForumPostManagement.ReplyTo')}:{' '}
<span className="font-medium">{getTopicTitle(post.topicId)}</span>
</p>
<div
className="line-clamp-3 break-words text-gray-700 dark:text-gray-200"
dangerouslySetInnerHTML={{ __html: sanitizeForumHtml(post.content) }}
/>
</div>
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center space-x-4">
<span>{formatForumDate(post.creationTime)}</span>
<div className="flex items-center space-x-1">
<FaHeart className="w-4 h-4" />
<span>
{post.likeCount} {translate('::App.Listform.ListformField.Likes')}
</span>
</div>
</div>
</div>
</div>
<div className="flex items-center space-x-2 ml-4">
<Button
size="sm"
className={`p-1 rounded-lg transition-colors ${
post.isAcceptedAnswer
? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900'
: 'text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
title={
post.isAcceptedAnswer
? translate('::App.ForumPostManagement.RemoveAcceptedAnswer')
: translate('::App.ForumPostManagement.MarkAsAcceptedAnswer')
}
onClick={() => handleToggleAcceptedAnswer(post)}
>
{post.isAcceptedAnswer ? (
<FaCheckCircle className="w-4 h-4" />
) : (
<FaCircle className="w-4 h-4" />
)}
</Button>
<Button
size="sm"
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
title={translate('::App.ForumPostManagement.EditPost')}
onClick={() => handleEdit(post)}
>
<FaEdit className="w-4 h-4" />
</Button>
<Button
size="sm"
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
title={translate('::App.ForumPostManagement.DeletePost')}
onClick={() => confirmDeletePost(post)}
>
<FaTrashAlt className="w-4 h-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{postToDelete && (
<ConfirmDialog
isOpen={showConfirm}
type="danger"
title={translate('::App.Platform.DeleteConfirmation')}
confirmText={translate('::App.Platform.Delete')}
cancelText={translate('::App.Platform.Cancel')}
onCancel={() => {
setShowConfirm(false)
setPostToDelete(null)
}}
onConfirm={async () => {
try {
if (postToDelete) {
await onDeletePost(postToDelete.id)
}
} catch (error) {
console.error('Error deleting post:', error)
} finally {
setShowConfirm(false)
setPostToDelete(null)
}
}}
></ConfirmDialog>
)}
</div>
)
}

View file

@ -1,484 +0,0 @@
import { useState } from 'react'
import {
FaPlus,
FaEdit,
FaTrashAlt,
FaLock,
FaUnlock,
FaThumbtack,
FaCheckCircle,
FaCircle,
FaEye,
FaSpinner,
} from 'react-icons/fa'
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum'
import { CreateTopicRequest } from '@/services/forum.service'
import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { FormContainer, FormItem, Button, Select, Dialog } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
import { formatForumDate } from '../forum/utils'
const FIELD_CLASS =
'w-full border border-gray-300 dark:border-gray-700 rounded-lg px-3 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500'
interface CategoryOption {
value: string
label: string
icon?: string
}
const topicValidationSchema = Yup.object().shape({
title: Yup.string().trim().required(),
content: Yup.string().trim().required(),
categoryId: Yup.string().required(),
})
interface TopicManagementProps {
topics: ForumTopic[]
totalCount: number
categories: ForumCategory[]
loading: boolean
onCreateTopic: (topic: CreateTopicRequest) => Promise<unknown>
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<unknown>
onDeleteTopic: (id: string) => Promise<unknown>
onPinTopic: (id: string) => Promise<unknown>
onUnpinTopic: (id: string) => Promise<unknown>
onLockTopic: (id: string) => Promise<unknown>
onUnlockTopic: (id: string) => Promise<unknown>
onMarkTopicAsSolved: (id: string) => Promise<unknown>
onMarkTopicAsUnsolved: (id: string) => Promise<unknown>
}
export function TopicManagement({
topics,
totalCount,
categories,
loading,
onCreateTopic,
onUpdateTopic,
onDeleteTopic,
onPinTopic,
onUnpinTopic,
onLockTopic,
onUnlockTopic,
onMarkTopicAsSolved,
onMarkTopicAsUnsolved,
}: TopicManagementProps) {
const { translate } = useLocalization()
const { tenant } = useStoreState((state) => state.auth)
const [showCreateForm, setShowCreateForm] = useState(false)
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null)
const [showConfirm, setShowConfirm] = useState(false)
const [topicToDelete, setTopicToDelete] = useState<ForumTopic | null>(null)
const confirmDeleteTopic = (topic: ForumTopic) => {
setTopicToDelete(topic)
setShowConfirm(true)
}
const handleEdit = (topic: ForumTopic) => {
setEditingTopic(topic)
setShowCreateForm(true)
}
const handleCloseForm = () => {
setShowCreateForm(false)
setEditingTopic(null)
}
const handlePin = async (topic: ForumTopic) => {
try {
if (topic.isPinned) {
await onUnpinTopic(topic.id)
} else {
await onPinTopic(topic.id)
}
} catch (error) {
console.error('Error toggling pin:', error)
}
}
const handleLock = async (topic: ForumTopic) => {
try {
if (topic.isLocked) {
await onUnlockTopic(topic.id)
} else {
await onLockTopic(topic.id)
}
} catch (error) {
console.error('Error toggling lock:', error)
}
}
const handleSolved = async (topic: ForumTopic) => {
try {
if (topic.isSolved) {
await onMarkTopicAsUnsolved(topic.id)
} else {
await onMarkTopicAsSolved(topic.id)
}
} catch (error) {
console.error('Error toggling solved status:', error)
}
}
const getCategoryName = (categoryId: string) =>
categories.find((c) => c.id === categoryId)?.name ?? translate('::App.Platform.Unknown')
const categoryOptions: CategoryOption[] = categories.map((category) => ({
value: category.id,
label: category.name,
icon: category.icon,
}))
const topicInitialValues = {
title: '',
content: '',
categoryId: '',
isPinned: false,
isLocked: false,
isSolved: false,
tenantId: tenant?.tenantId ?? '',
}
return (
<div className="space-y-3">
{/* Create/Edit Modal */}
<Dialog
isOpen={showCreateForm}
width={720}
onClose={handleCloseForm}
onRequestClose={handleCloseForm}
>
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{editingTopic
? translate('::App.ForumTopicManagement.EditTopic')
: translate('::App.ForumTopicManagement.AddTopic')}
</h3>
</div>
<div className="pt-4 max-h-[70vh] overflow-y-auto">
<Formik
enableReinitialize
initialValues={
editingTopic
? {
title: editingTopic.title,
content: editingTopic.content,
categoryId: editingTopic.categoryId,
isPinned: editingTopic.isPinned,
isLocked: editingTopic.isLocked,
isSolved: editingTopic.isSolved,
tenantId: tenant.tenantId || '',
}
: topicInitialValues
}
validationSchema={topicValidationSchema}
onSubmit={async (values, { setSubmitting, resetForm }) => {
try {
if (editingTopic) {
await onUpdateTopic(editingTopic.id, values)
} else {
await onCreateTopic(values)
}
resetForm()
setShowCreateForm(false)
setEditingTopic(null)
} catch (error) {
console.error('Error submitting topic:', error)
} finally {
setSubmitting(false)
}
}}
>
{({ values, isSubmitting, errors, touched }) => (
<Form>
<FormContainer className="space-y-4">
<FormItem
asterisk
label={translate('::App.Listform.ListformField.Title')}
invalid={!!errors.title && touched.title}
errorMessage={errors.title}
>
<Field
name="title"
className={FIELD_CLASS}
/>
</FormItem>
<FormItem
asterisk
label={translate('::App.Platform.Category')}
invalid={!!errors.categoryId && !!touched.categoryId}
errorMessage={errors.categoryId}
>
<Field name="categoryId">
{({ field, form }: FieldProps<string>) => (
<Select<CategoryOption>
field={field}
form={form}
options={categoryOptions}
placeholder={translate('::App.ForumTopicManagement.SelectCategory')}
value={categoryOptions.filter(
(option) => option.value === values.categoryId,
)}
formatOptionLabel={(option) => (
<span className="flex items-center gap-2">
<span className="text-lg">{option.icon}</span>
<span>{option.label}</span>
</span>
)}
onChange={(option) =>
form.setFieldValue('categoryId', option?.value ?? '')
}
/>
)}
</Field>
</FormItem>
<FormItem
asterisk
label={translate('::App.Listform.ListformField.Content')}
invalid={!!errors.content && touched.content}
errorMessage={errors.content}
>
<Field
as="textarea"
name="content"
rows={6}
className={FIELD_CLASS}
/>
</FormItem>
<FormItem label={translate('::App.Listform.ListformField.Options')}>
<div className="flex items-center space-x-6 pt-1">
<label className="flex items-center">
<Field type="checkbox" name="isPinned" className="mr-2" />
{translate('::App.ForumTopicManagement.Pinned')}
</label>
<label className="flex items-center">
<Field type="checkbox" name="isLocked" className="mr-2" />
{translate('::App.Platform.Locked')}
</label>
<label className="flex items-center">
<Field type="checkbox" name="isSolved" className="mr-2" />
{translate('::App.ForumTopicManagement.Solved')}
</label>
</div>
</FormItem>
<div className="flex justify-end space-x-3 pt-2">
<Button variant="plain" type="button" onClick={handleCloseForm}>
{translate('::App.Platform.Cancel')}
</Button>
<Button variant="solid" type="submit" loading={isSubmitting}>
{editingTopic ? translate('::App.Platform.Update') : translate('::App.Platform.Create')}
</Button>
</div>
</FormContainer>
</Form>
)}
</Formik>
</div>
</Dialog>
{/* Topics List */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="flex items-center justify-between px-3 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{translate('::App.ForumDashboard.Topics')} ({totalCount})
</h3>
<Button
size="sm"
variant="solid"
disabled={loading}
className="flex items-center space-x-2"
onClick={() => setShowCreateForm(true)}
>
<FaPlus className="w-4 h-4" />
<span>{translate('::App.ForumTopicManagement.AddTopic')}</span>
</Button>
</div>
{loading ? (
<div className="p-8 text-center">
<FaSpinner className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600 dark:text-blue-400" />
<p className="text-gray-500 dark:text-gray-400">
{translate('::App.ForumTopicManagement.Loadingtopics')}
</p>
</div>
) : (
<div className="divide-y divide-gray-200 dark:divide-gray-800">
{topics.length === 0 && (
<p className="p-8 text-center text-gray-500 dark:text-gray-400">
{translate('::App.Platform.NoDataFound')}
</p>
)}
{[...topics]
.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 dark:hover:bg-gray-800 transition-colors"
>
<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 && (
<FaThumbtack className="w-4 h-4 text-orange-500 dark:text-orange-400" />
)}
{topic.isLocked && (
<FaLock className="w-4 h-4 text-gray-400 dark:text-gray-500" />
)}
{topic.isSolved && (
<FaCheckCircle className="w-4 h-4 text-emerald-500 dark:text-emerald-400" />
)}
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100 line-clamp-1">
{topic.title}
</h4>
</div>
<p className="text-gray-600 dark:text-gray-300 mb-3 line-clamp-2">
{topic.content}
</p>
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center space-x-4">
<span className="font-medium">{getCategoryName(topic.categoryId)}</span>
<span>{topic.authorName}</span>
<span>{formatForumDate(topic.creationTime)}</span>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1">
<FaEye className="w-4 h-4" />
<span>{topic.viewCount}</span>
</div>
<span>
{topic.replyCount} {translate('::App.Platform.Replies')}
</span>
<span>
{topic.likeCount} {translate('::App.Listform.ListformField.Likes')}
</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2 ml-4">
<Button
size="sm"
className={`p-1 rounded-lg transition-colors ${
topic.isPinned
? 'text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-900'
: 'text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
title={
topic.isPinned
? translate('::App.ForumTopicManagement.UnpinTopic')
: translate('::App.ForumTopicManagement.PinTopic')
}
onClick={() => handlePin(topic)}
>
<FaThumbtack className="h-4 w-4" />
</Button>
<Button
size="sm"
className={`p-1 rounded-lg transition-colors ${
topic.isLocked
? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900'
: 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900'
}`}
title={
topic.isLocked
? translate('::App.ForumTopicManagement.UnlockTopic')
: translate('::App.ForumTopicManagement.LockTopic')
}
onClick={() => handleLock(topic)}
>
{topic.isLocked ? (
<FaLock className="w-4 h-4" />
) : (
<FaUnlock className="w-4 h-4" />
)}
</Button>
<Button
size="sm"
className={`p-1 rounded-lg transition-colors ${
topic.isSolved
? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900'
: 'text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
title={
topic.isSolved
? translate('::App.ForumTopicManagement.MarkAsUnsolved')
: translate('::App.ForumTopicManagement.MarkAsSolved')
}
onClick={() => handleSolved(topic)}
>
{topic.isSolved ? (
<FaCheckCircle className="w-4 h-4" />
) : (
<FaCircle className="w-4 h-4" />
)}
</Button>
<Button
size="sm"
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
title={translate('::App.ForumTopicManagement.EditTopic')}
onClick={() => handleEdit(topic)}
>
<FaEdit className="w-4 h-4" />
</Button>
<Button
size="sm"
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
title={translate('::App.ForumTopicManagement.DeleteTopic')}
onClick={() => confirmDeleteTopic(topic)}
>
<FaTrashAlt className="w-4 h-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{topicToDelete && (
<ConfirmDialog
isOpen={showConfirm}
type="danger"
title={translate('::App.Platform.DeleteConfirmation')}
confirmText={translate('::App.Platform.Delete')}
cancelText={translate('::App.Platform.Cancel')}
onCancel={() => {
setShowConfirm(false)
setTopicToDelete(null)
}}
onConfirm={async () => {
try {
if (topicToDelete) {
await onDeleteTopic(topicToDelete.id)
}
} catch (error) {
console.error('Error deleting topic:', error)
} finally {
setShowConfirm(false)
setTopicToDelete(null)
}
}}
></ConfirmDialog>
)}
</div>
)
}

View file

@ -1,52 +0,0 @@
export interface CategoryIconOption {
/** Kategoride saklanan emoji değeri. */
value: string
/** Aramada eşleşen etiket. */
label: string
}
/** Forum kategorileri için hazır emoji seti. */
export const CATEGORY_ICON_OPTIONS: CategoryIconOption[] = [
{ value: '💬', label: 'Discussion' },
{ value: '📢', label: 'Announcement' },
{ value: '❓', label: 'Question' },
{ value: '💡', label: 'Idea' },
{ value: '🛠️', label: 'Support' },
{ value: '🐞', label: 'Bug' },
{ value: '🚀', label: 'Release' },
{ value: '📚', label: 'Documentation' },
{ value: '📝', label: 'Notes' },
{ value: '📌', label: 'Pinned' },
{ value: '⭐', label: 'Featured' },
{ value: '🔥', label: 'Popular' },
{ value: '🎯', label: 'Goals' },
{ value: '📊', label: 'Reports' },
{ value: '📈', label: 'Analytics' },
{ value: '⚙️', label: 'Settings' },
{ value: '🔒', label: 'Private' },
{ value: '🧩', label: 'Integration' },
{ value: '🖥️', label: 'Development' },
{ value: '📱', label: 'Mobile' },
{ value: '🗄️', label: 'Database' },
{ value: '🔐', label: 'Security' },
{ value: '👥', label: 'Community' },
{ value: '🏢', label: 'Company' },
{ value: '🧾', label: 'Finance' },
{ value: '🛒', label: 'Sales' },
{ value: '🚚', label: 'Logistics' },
{ value: '🏭', label: 'Production' },
{ value: '🎓', label: 'Training' },
{ value: '🎉', label: 'Events' },
{ value: '☕', label: 'Off topic' },
{ value: '🗳️', label: 'Poll' },
]
/**
* Kayıtlı ikon hazır sette yoksa kaybolmasın diye listenin başına eklenir.
*/
export function resolveCategoryIconOptions(current?: string): CategoryIconOption[] {
if (!current || CATEGORY_ICON_OPTIONS.some((option) => option.value === current)) {
return CATEGORY_ICON_OPTIONS
}
return [{ value: current, label: current }, ...CATEGORY_ICON_OPTIONS]
}

View file

@ -1,23 +1,11 @@
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
import {
CreateCategoryRequest,
CreatePostRequest,
CreateTopicRequest,
forumService,
} from '@/services/forum.service'
import { CreatePostRequest, CreateTopicRequest, forumService } from '@/services/forum.service'
import { useCallback, useEffect, useRef, useState } from 'react'
type TotalCounts = { categories: number; topics: number; posts: number }
export function useForumData() {
const [categories, setCategories] = useState<ForumCategory[]>([])
const [topics, setTopics] = useState<ForumTopic[]>([])
const [posts, setPosts] = useState<ForumPost[]>([])
const [totalCounts, setTotalCounts] = useState<TotalCounts>({
categories: 0,
topics: 0,
posts: 0,
})
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@ -56,7 +44,6 @@ export function useForumData() {
const response = await forumService.getCategories(params)
if (!mounted.current) return
setCategories(response.items)
setTotalCounts((prev) => ({ ...prev, categories: response.totalCount }))
} catch (err) {
fail('Failed to load categories', err)
} finally {
@ -73,7 +60,6 @@ export function useForumData() {
const response = await forumService.getTopics({ categoryId })
if (!mounted.current) return
setTopics(response.items)
if (!categoryId) setTotalCounts((prev) => ({ ...prev, topics: response.totalCount }))
} catch (err) {
fail('Failed to load topics', err)
} finally {
@ -90,7 +76,6 @@ export function useForumData() {
const response = await forumService.getPosts({ topicId })
if (!mounted.current) return
setPosts(response.items)
if (!topicId) setTotalCounts((prev) => ({ ...prev, posts: response.totalCount }))
} catch (err) {
fail('Failed to load posts', err)
} finally {
@ -111,87 +96,11 @@ export function useForumData() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// ---- Category operations -------------------------------------------------
const createCategory = useCallback(
async (categoryData: CreateCategoryRequest) => {
try {
const newCategory = await forumService.createCategory(categoryData)
setCategories((prev) => [...prev, newCategory])
setTotalCounts((prev) => ({ ...prev, categories: prev.categories + 1 }))
return newCategory
} catch (err) {
fail('Failed to create category', err)
throw err
}
},
[fail],
)
const updateCategory = useCallback(
async (id: string, updates: Partial<ForumCategory>) => {
try {
const updatedCategory = await forumService.updateCategory(id, updates)
setCategories((prev) => prev.map((cat) => (cat.id === id ? updatedCategory : cat)))
return updatedCategory
} catch (err) {
fail('Failed to update category', err)
throw err
}
},
[fail],
)
const updateCategoryLockState = useCallback(
async (id: string) => {
try {
const updated = await forumService.updateCategoryLockState(id)
setCategories((prev) => prev.map((cat) => (cat.id === id ? updated : cat)))
return updated
} catch (err) {
fail('Failed to update category lock state', err)
throw err
}
},
[fail],
)
const updateCategoryActiveState = useCallback(
async (id: string) => {
try {
const updated = await forumService.updateCategoryActiveState(id)
setCategories((prev) => prev.map((cat) => (cat.id === id ? updated : cat)))
return updated
} catch (err) {
fail('Failed to update category active state', err)
throw err
}
},
[fail],
)
const deleteCategory = useCallback(
async (id: string) => {
try {
await forumService.deleteCategory(id)
// Kategori silindiğinde bağlı topic/post sayıları da değişir; tek kaynak sunucu.
await reloadAll()
} catch (err) {
fail('Failed to delete category', err)
throw err
}
},
[fail, reloadAll],
)
// ---- Topic operations ----------------------------------------------------
const createTopic = useCallback(
async (topicData: CreateTopicRequest) => {
try {
const newTopic = await forumService.createTopic(topicData)
setTopics((prev) => [...prev, newTopic])
setTotalCounts((prev) => ({ ...prev, topics: prev.topics + 1 }))
setCategories((prev) =>
prev.map((cat) =>
cat.id === topicData.categoryId ? { ...cat, topicCount: cat.topicCount + 1 } : cat,
@ -206,89 +115,11 @@ export function useForumData() {
[fail],
)
const updateTopic = useCallback(
async (id: string, updates: Partial<ForumTopic>) => {
try {
const updatedTopic = await forumService.updateTopic(id, updates)
setTopics((prev) => prev.map((topic) => (topic.id === id ? updatedTopic : topic)))
return updatedTopic
} catch (err) {
fail('Failed to update topic', err)
throw err
}
},
[fail],
)
const deleteTopic = useCallback(
async (id: string) => {
try {
await forumService.deleteTopic(id)
await reloadAll()
} catch (err) {
fail('Failed to delete topic', err)
throw err
}
},
[fail, reloadAll],
)
const applyTopicUpdate = useCallback((updated: ForumTopic) => {
setTopics((prev) => prev.map((topic) => (topic.id === updated.id ? updated : topic)))
return updated
}, [])
const topicAction = useCallback(
async (
id: string,
action: (id: string) => Promise<ForumTopic>,
errorMessage: string,
): Promise<ForumTopic> => {
try {
return applyTopicUpdate(await action(id))
} catch (err) {
fail(errorMessage, err)
throw err
}
},
[applyTopicUpdate, fail],
)
const pinTopic = useCallback(
(id: string) => topicAction(id, (x) => forumService.pinTopic(x), 'Failed to pin topic'),
[topicAction],
)
const unpinTopic = useCallback(
(id: string) => topicAction(id, (x) => forumService.unpinTopic(x), 'Failed to unpin topic'),
[topicAction],
)
const lockTopic = useCallback(
(id: string) => topicAction(id, (x) => forumService.lockTopic(x), 'Failed to lock topic'),
[topicAction],
)
const unlockTopic = useCallback(
(id: string) => topicAction(id, (x) => forumService.unlockTopic(x), 'Failed to unlock topic'),
[topicAction],
)
const solvedTopic = useCallback(
(id: string) =>
topicAction(id, (x) => forumService.solvedTopic(x), 'Failed to mark topic as solved'),
[topicAction],
)
const unsolvedTopic = useCallback(
(id: string) =>
topicAction(id, (x) => forumService.unsolvedTopic(x), 'Failed to mark topic as unsolved'),
[topicAction],
)
// ---- Post operations -----------------------------------------------------
const createPost = useCallback(
async (postData: CreatePostRequest) => {
try {
const newPost = await forumService.createPost(postData)
setPosts((prev) => [...prev, newPost])
setTotalCounts((prev) => ({ ...prev, posts: prev.posts + 1 }))
setTopics((prev) =>
prev.map((t) => (t.id === postData.topicId ? { ...t, replyCount: t.replyCount + 1 } : t)),
@ -312,33 +143,6 @@ export function useForumData() {
[fail, topics],
)
const updatePost = useCallback(
async (id: string, updates: Partial<ForumPost>) => {
try {
const updatedPost = await forumService.updatePost(id, updates)
setPosts((prev) => prev.map((post) => (post.id === id ? updatedPost : post)))
return updatedPost
} catch (err) {
fail('Failed to update post', err)
throw err
}
},
[fail],
)
const deletePost = useCallback(
async (id: string) => {
try {
await forumService.deletePost(id)
await reloadAll()
} catch (err) {
fail('Failed to delete post', err)
throw err
}
},
[fail, reloadAll],
)
const postAction = useCallback(
async (
id: string,
@ -365,24 +169,6 @@ export function useForumData() {
(id: string) => postAction(id, (x) => forumService.unlikePost(x), 'Failed to unlike post'),
[postAction],
)
const markPostAsAcceptedAnswer = useCallback(
(id: string) =>
postAction(
id,
(x) => forumService.markPostAsAcceptedAnswer(x),
'Failed to mark post as accepted answer',
),
[postAction],
)
const unmarkPostAsAcceptedAnswer = useCallback(
(id: string) =>
postAction(
id,
(x) => forumService.unmarkPostAsAcceptedAnswer(x),
'Failed to unmark post as accepted answer',
),
[postAction],
)
const clearError = useCallback(() => setError(null), [])
@ -391,7 +177,6 @@ export function useForumData() {
categories,
topics,
posts,
totalCounts,
loading,
error,
@ -401,32 +186,13 @@ export function useForumData() {
loadPosts,
reloadAll,
// Category operations
createCategory,
updateCategory,
updateCategoryLockState,
updateCategoryActiveState,
deleteCategory,
// Topic operations
createTopic,
updateTopic,
deleteTopic,
pinTopic,
unpinTopic,
lockTopic,
unlockTopic,
solvedTopic,
unsolvedTopic,
// Post operations
createPost,
updatePost,
deletePost,
likePost,
unlikePost,
markPostAsAcceptedAnswer,
unmarkPostAsAcceptedAnswer,
// Utility
clearError,