seed/data Seeder Forum
This commit is contained in:
parent
ed143c01d0
commit
3ec9a94860
23 changed files with 11652 additions and 12745 deletions
|
|
@ -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` | `[]` |
|
||||
|
|
|
|||
|
|
@ -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ı. |
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
48
configs/seeds/host/data/App.ForumManagement.Categories.json
Normal file
48
configs/seeds/host/data/App.ForumManagement.Categories.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>>({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue