diff --git a/api/src/Kurs.Platform.Application/Forum/ForumAppService.cs b/api/src/Kurs.Platform.Application/Forum/ForumAppService.cs index 8e6159e6..7477039c 100644 --- a/api/src/Kurs.Platform.Application/Forum/ForumAppService.cs +++ b/api/src/Kurs.Platform.Application/Forum/ForumAppService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; using Volo.Abp.Application.Dtos; using Volo.Abp.Authorization; using Volo.Abp.Domain.Entities; @@ -159,7 +158,8 @@ public class ForumAppService : PlatformAppService, IForumAppService input.Slug, input.Description, input.Icon, - input.DisplayOrder + input.DisplayOrder, + input.TenantId ) { IsActive = input.IsActive, @@ -192,15 +192,17 @@ public class ForumAppService : PlatformAppService, IForumAppService { var category = await _categoryRepository.GetAsync(id); category.IsLocked = !category.IsLocked; + await _categoryRepository.UpdateAsync(category); return ObjectMapper.Map(category); } - [Authorize("App.ForumManagement.Update")] + [Authorize("App.ForumManagement.Update")] public async Task UpdateCategoryActiveAsync(Guid id) { var category = await _categoryRepository.GetAsync(id); category.IsActive = !category.IsActive; + await _categoryRepository.UpdateAsync(category); return ObjectMapper.Map(category); } @@ -281,7 +283,8 @@ public class ForumAppService : PlatformAppService, IForumAppService input.Content, input.CategoryId, CurrentUser.Id.Value, - CurrentUser.Name + CurrentUser.Name, + input.TenantId ) { IsPinned = input.IsPinned, @@ -380,7 +383,8 @@ public class ForumAppService : PlatformAppService, IForumAppService input.Content, CurrentUser.Id.Value, CurrentUser.Name, - input.ParentPostId + input.ParentPostId, + input.TenantId ); await _postRepository.InsertAsync(post, autoSave: true); @@ -473,24 +477,6 @@ public class ForumAppService : PlatformAppService, IForumAppService await _categoryRepository.UpdateAsync(category); } - // Like/Unlike topic - public async Task LikeTopicAsync(Guid id) - { - var topic = await _topicRepository.GetAsync(id); - topic.LikeCount++; - await _topicRepository.UpdateAsync(topic); - return ObjectMapper.Map(topic); - } - - public async Task UnlikeTopicAsync(Guid id) - { - var topic = await _topicRepository.GetAsync(id); - topic.LikeCount = Math.Max(0, topic.LikeCount - 1); - await _topicRepository.UpdateAsync(topic); - return ObjectMapper.Map(topic); - } - - // Like/Unlike posts public async Task LikePostAsync(Guid id) { var post = await _postRepository.GetAsync(id); @@ -523,6 +509,96 @@ public class ForumAppService : PlatformAppService, IForumAppService return ObjectMapper.Map(post); } + public async Task MarkPostAsync(Guid id) + { + var post = await _postRepository.GetAsync(id); + post.IsAcceptedAnswer = true; + + await _postRepository.UpdateAsync(post); + + return ObjectMapper.Map(post); + } + + public async Task UnmarkPostAsync(Guid id) + { + var post = await _postRepository.GetAsync(id); + post.IsAcceptedAnswer = false; + await _postRepository.UpdateAsync(post); + + return ObjectMapper.Map(post); + } + + // Like/Unlike topic + public async Task LikeTopicAsync(Guid id) + { + var topic = await _topicRepository.GetAsync(id); + topic.LikeCount++; + + await _topicRepository.UpdateAsync(topic); + return ObjectMapper.Map(topic); + } + + public async Task UnlikeTopicAsync(Guid id) + { + var topic = await _topicRepository.GetAsync(id); + topic.LikeCount = Math.Max(0, topic.LikeCount - 1); + + await _topicRepository.UpdateAsync(topic); + return ObjectMapper.Map(topic); + } + + public async Task PinTopicAsync(Guid id) + { + var topic = await _topicRepository.GetAsync(id); + topic.IsPinned = true; + + await _topicRepository.UpdateAsync(topic); + return ObjectMapper.Map(topic); + } + + public async Task UnpinTopicAsync(Guid id) + { + var topic = await _topicRepository.GetAsync(id); + topic.IsPinned = false; + await _topicRepository.UpdateAsync(topic); + return ObjectMapper.Map(topic); + } + + public async Task LockTopicAsync(Guid id) + { + var topic = await _topicRepository.GetAsync(id); + topic.IsLocked = true; + + await _topicRepository.UpdateAsync(topic); + return ObjectMapper.Map(topic); + } + + public async Task UnlockTopicAsync(Guid id) + { + var topic = await _topicRepository.GetAsync(id); + topic.IsLocked = false; + await _topicRepository.UpdateAsync(topic); + return ObjectMapper.Map(topic); + } + + public async Task SolvedTopicAsync(Guid id) + { + var topic = await _topicRepository.GetAsync(id); + topic.IsSolved = true; + + await _topicRepository.UpdateAsync(topic); + return ObjectMapper.Map(topic); + } + + public async Task UnsolvedTopicAsync(Guid id) + { + var topic = await _topicRepository.GetAsync(id); + topic.IsSolved = false; + + await _topicRepository.UpdateAsync(topic); + return ObjectMapper.Map(topic); + } + // Statistics public async Task GetForumStatsAsync() { diff --git a/api/src/Kurs.Platform.DbMigrator/Kurs.Platform.DbMigrator.csproj b/api/src/Kurs.Platform.DbMigrator/Kurs.Platform.DbMigrator.csproj index eeefebb6..b09a0b47 100644 --- a/api/src/Kurs.Platform.DbMigrator/Kurs.Platform.DbMigrator.csproj +++ b/api/src/Kurs.Platform.DbMigrator/Kurs.Platform.DbMigrator.csproj @@ -5,6 +5,7 @@ Exe net9.0 + 467bbc0f-83d0-40d0-a9f2-230c8620bdad diff --git a/ui/dev-dist/sw.js b/ui/dev-dist/sw.js index 71fbc8e3..0b99fe8b 100644 --- a/ui/dev-dist/sw.js +++ b/ui/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.mvu82hb2mqg" + "revision": "0.m83v8d1s4bo" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/ui/src/services/forumService.ts b/ui/src/services/forumService.ts index 5ffb3dce..9c47d535 100644 --- a/ui/src/services/forumService.ts +++ b/ui/src/services/forumService.ts @@ -159,14 +159,6 @@ class ForumService { }) } - // async toggleCategoryStatus(id: string): Promise { - // const response = await apiService.fetchData({ - // url: `/api/app/forum/categories/${id}/toggle-status`, - // method: 'POST', - // }) - // return response.data - // } - // Topics async getTopics(params: TopicListParams = {}): Promise> { const response = await apiService.fetchData>({ @@ -210,9 +202,25 @@ class ForumService { }) } + async likeTopic(id: string): Promise { + const response = await apiService.fetchData({ + url: `/api/app/forum/${id}/like-topic`, + method: 'POST', + }) + return response.data + } + + async unlikeTopic(id: string): Promise { + const response = await apiService.fetchData({ + url: `/api/app/forum/${id}/unlike-topic`, + method: 'POST', + }) + return response.data + } + async pinTopic(id: string): Promise { const response = await apiService.fetchData({ - url: `/api/app/forum/topics/${id}/pin`, + url: `/api/app/forum/${id}/pin-topic`, method: 'POST', }) return response.data @@ -220,7 +228,7 @@ class ForumService { async unpinTopic(id: string): Promise { const response = await apiService.fetchData({ - url: `/api/app/forum/topics/${id}/unpin`, + url: `/api/app/forum/${id}/unpin-topic`, method: 'POST', }) return response.data @@ -228,7 +236,7 @@ class ForumService { async lockTopic(id: string): Promise { const response = await apiService.fetchData({ - url: `/api/app/forum/topics/${id}/lock`, + url: `/api/app/forum/${id}/lock-topic`, method: 'POST', }) return response.data @@ -236,23 +244,23 @@ class ForumService { async unlockTopic(id: string): Promise { const response = await apiService.fetchData({ - url: `/api/app/forum/topics/${id}/unlock`, + url: `/api/app/forum/${id}/unlock-topic`, method: 'POST', }) return response.data } - async markTopicAsSolved(id: string): Promise { + async solvedTopic(id: string): Promise { const response = await apiService.fetchData({ - url: `/api/app/forum/topics/${id}/mark-solved`, + url: `/api/app/forum/${id}/solved-topic`, method: 'POST', }) return response.data } - async markTopicAsUnsolved(id: string): Promise { + async unsolvedTopic(id: string): Promise { const response = await apiService.fetchData({ - url: `/api/app/forum/topics/${id}/mark-unsolved`, + url: `/api/app/forum/${id}/unsolved-topic`, method: 'POST', }) return response.data @@ -287,7 +295,7 @@ class ForumService { async updatePost(id: string, data: Partial): Promise { const response = await apiService.fetchData({ - url: `/api/app/forum/posts/${id}`, + url: `/api/app/forum/${id}/post`, method: 'PUT', data, }) @@ -296,7 +304,7 @@ class ForumService { async deletePost(id: string): Promise { await apiService.fetchData({ - url: `/api/app/forum/posts/${id}`, + url: `/api/app/forum/${id}/post`, method: 'DELETE', }) } @@ -317,25 +325,9 @@ class ForumService { return response.data } - async likeTopic(id: string): Promise { - const response = await apiService.fetchData({ - url: `/api/app/forum/${id}/like-topic`, - method: 'POST', - }) - return response.data - } - - async unlikeTopic(id: string): Promise { - const response = await apiService.fetchData({ - url: `/api/app/forum/${id}/unlike-topic`, - method: 'POST', - }) - return response.data - } - async markPostAsAcceptedAnswer(id: string): Promise { const response = await apiService.fetchData({ - url: `/api/app/forum/posts/${id}/mark-accepted`, + url: `/api/app/forum/${id}/mark-post`, method: 'POST', }) return response.data @@ -343,7 +335,7 @@ class ForumService { async unmarkPostAsAcceptedAnswer(id: string): Promise { const response = await apiService.fetchData({ - url: `/api/app/forum/posts/${id}/unmark-accepted`, + url: `/api/app/forum/${id}/unmark-post`, method: 'POST', }) return response.data diff --git a/ui/src/views/forum/Management.tsx b/ui/src/views/forum/Management.tsx index 002ab9ed..4c00e1f4 100644 --- a/ui/src/views/forum/Management.tsx +++ b/ui/src/views/forum/Management.tsx @@ -24,8 +24,8 @@ export function Management() { unpinTopic, lockTopic, unlockTopic, - markTopicAsSolved, - markTopicAsUnsolved, + solvedTopic, + unsolvedTopic, createPost, updatePost, deletePost, @@ -91,8 +91,8 @@ export function Management() { onUnpinTopic={(id) => unpinTopic(id).then(() => {})} onLockTopic={(id) => lockTopic(id).then(() => {})} onUnlockTopic={(id) => unlockTopic(id).then(() => {})} - onMarkTopicAsSolved={(id) => markTopicAsSolved(id).then(() => {})} - onMarkTopicAsUnsolved={(id) => markTopicAsUnsolved(id).then(() => {})} + onMarkTopicAsSolved={(id) => solvedTopic(id).then(() => {})} + onMarkTopicAsUnsolved={(id) => unsolvedTopic(id).then(() => {})} onCreatePost={(data) => createPost(data).then(() => {})} onUpdatePost={(id, data) => updatePost(id, data).then(() => {})} onDeletePost={(id) => deletePost(id).then(() => {})} diff --git a/ui/src/views/forum/admin/AdminView.tsx b/ui/src/views/forum/admin/AdminView.tsx index eec8770a..1ccbcc66 100644 --- a/ui/src/views/forum/admin/AdminView.tsx +++ b/ui/src/views/forum/admin/AdminView.tsx @@ -1,56 +1,52 @@ -import React, { useState } from 'react'; -import { Folder, MessageSquare, FileText, Plus, BarChart3 } from 'lucide-react'; -import { CategoryManagement } from './CategoryManagement'; -import { TopicManagement } from './TopicManagement'; -import { PostManagement } from './PostManagement'; -import { AdminStats } from './AdminStats'; -import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'; +import React, { useState } from 'react' +import { Folder, MessageSquare, FileText, Plus, BarChart3 } from 'lucide-react' +import { CategoryManagement } from './CategoryManagement' +import { TopicManagement } from './TopicManagement' +import { PostManagement } from './PostManagement' +import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum' +import { AdminStats } from './Dashboard' interface AdminViewProps { - categories: ForumCategory[]; - topics: ForumTopic[]; - posts: ForumPost[]; - loading: boolean; + categories: ForumCategory[] + topics: ForumTopic[] + posts: ForumPost[] + loading: boolean onCreateCategory: (category: { - name: string; - slug: string; - description: string; - icon: string; - displayOrder: number; - isActive: boolean; - isLocked: boolean; - }) => Promise; - onUpdateCategory: (id: string, category: Partial) => Promise; - onUpdateCategoryLockState: (id: string) => Promise; - onUpdateCategoryActiveState: (id: string) => Promise; - onDeleteCategory: (id: string) => Promise; + name: string + slug: string + description: string + icon: string + displayOrder: number + isActive: boolean + isLocked: boolean + }) => Promise + onUpdateCategory: (id: string, category: Partial) => Promise + onUpdateCategoryLockState: (id: string) => Promise + onUpdateCategoryActiveState: (id: string) => Promise + onDeleteCategory: (id: string) => Promise onCreateTopic: (topic: { - title: string; - content: string; - categoryId: string; - isPinned?: boolean; - isLocked?: boolean; - }) => Promise; - onUpdateTopic: (id: string, topic: Partial) => Promise; - onDeleteTopic: (id: string) => Promise; - onPinTopic: (id: string) => Promise; - onUnpinTopic: (id: string) => Promise; - onLockTopic: (id: string) => Promise; - onUnlockTopic: (id: string) => Promise; - onMarkTopicAsSolved: (id: string) => Promise; - onMarkTopicAsUnsolved: (id: string) => Promise; - onCreatePost: (post: { - topicId: string; - content: string; - parentPostId?: string; - }) => Promise; - onUpdatePost: (id: string, post: Partial) => Promise; - onDeletePost: (id: string) => Promise; - onMarkPostAsAcceptedAnswer: (id: string) => Promise; - onUnmarkPostAsAcceptedAnswer: (id: string) => Promise; + title: string + content: string + categoryId: string + isPinned?: boolean + isLocked?: boolean + }) => Promise + onUpdateTopic: (id: string, topic: Partial) => Promise + onDeleteTopic: (id: string) => Promise + onPinTopic: (id: string) => Promise + onUnpinTopic: (id: string) => Promise + onLockTopic: (id: string) => Promise + onUnlockTopic: (id: string) => Promise + onMarkTopicAsSolved: (id: string) => Promise + onMarkTopicAsUnsolved: (id: string) => Promise + onCreatePost: (post: { topicId: string; content: string; parentPostId?: string }) => Promise + onUpdatePost: (id: string, post: Partial) => Promise + onDeletePost: (id: string) => Promise + onMarkPostAsAcceptedAnswer: (id: string) => Promise + onUnmarkPostAsAcceptedAnswer: (id: string) => Promise } -type AdminSection = 'stats' | 'categories' | 'topics' | 'posts'; +type AdminSection = 'stats' | 'categories' | 'topics' | 'posts' export function AdminView({ categories, @@ -75,16 +71,16 @@ export function AdminView({ onUpdatePost, onDeletePost, onMarkPostAsAcceptedAnswer, - onUnmarkPostAsAcceptedAnswer + onUnmarkPostAsAcceptedAnswer, }: AdminViewProps) { - const [activeSection, setActiveSection] = useState('stats'); + const [activeSection, setActiveSection] = useState('stats') const navigationItems = [ { id: 'stats' as AdminSection, label: 'Dashboard', icon: BarChart3 }, { id: 'categories' as AdminSection, label: 'Categories', icon: Folder }, { id: 'topics' as AdminSection, label: 'Topics', icon: MessageSquare }, { id: 'posts' as AdminSection, label: 'Posts', icon: FileText }, - ]; + ] return (
@@ -93,7 +89,7 @@ export function AdminView({
@@ -117,7 +113,7 @@ export function AdminView({ {activeSection === 'stats' && ( )} - + {activeSection === 'categories' && ( )} - + {activeSection === 'topics' && ( )} - + {activeSection === 'posts' && (
- ); -} \ No newline at end of file + ) +} diff --git a/ui/src/views/forum/admin/AdminStats.tsx b/ui/src/views/forum/admin/Dashboard.tsx similarity index 100% rename from ui/src/views/forum/admin/AdminStats.tsx rename to ui/src/views/forum/admin/Dashboard.tsx diff --git a/ui/src/views/forum/admin/PostManagement.tsx b/ui/src/views/forum/admin/PostManagement.tsx index 4200b9b8..ae92f185 100644 --- a/ui/src/views/forum/admin/PostManagement.tsx +++ b/ui/src/views/forum/admin/PostManagement.tsx @@ -1,6 +1,8 @@ import React, { useState } from 'react' import { Plus, Edit2, Trash2, CheckCircle, Circle, Heart, Loader2 } from 'lucide-react' import { ForumPost, ForumTopic } from '@/proxy/forum/forum' +import ReactQuill from 'react-quill' +import 'react-quill/dist/quill.snow.css' interface PostManagementProps { posts: ForumPost[] @@ -23,11 +25,11 @@ export function PostManagement({ onMarkPostAsAcceptedAnswer, onUnmarkPostAsAcceptedAnswer, }: PostManagementProps) { + const [content, setContent] = useState('') const [showCreateForm, setShowCreateForm] = useState(false) const [editingPost, setEditingPost] = useState(null) const [formData, setFormData] = useState({ topicId: '', - content: '', isAcceptedAnswer: false, }) const [submitting, setSubmitting] = useState(false) @@ -35,7 +37,6 @@ export function PostManagement({ const resetForm = () => { setFormData({ topicId: '', - content: '', isAcceptedAnswer: false, }) setShowCreateForm(false) @@ -44,15 +45,18 @@ export function PostManagement({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - if (submitting) return + if (submitting || !content.trim()) return try { setSubmitting(true) + const data = { ...formData, content: content.trim() } + if (editingPost) { - await onUpdatePost(editingPost.id, formData) + await onUpdatePost(editingPost.id, data) } else { - await onCreatePost(formData) + await onCreatePost(data) } + resetForm() } catch (error) { console.error('Error submitting form:', error) @@ -65,9 +69,9 @@ export function PostManagement({ setEditingPost(post) setFormData({ topicId: post.topicId, - content: post.content, isAcceptedAnswer: post.isAcceptedAnswer, }) + setContent(post.content) setShowCreateForm(true) } @@ -151,12 +155,12 @@ export function PostManagement({
-