2025-06-23 21:22:11 +00:00
|
|
|
import React, { useState } from 'react'
|
|
|
|
|
import {
|
|
|
|
|
Plus,
|
|
|
|
|
Edit2,
|
|
|
|
|
Trash2,
|
|
|
|
|
Lock,
|
|
|
|
|
Unlock,
|
|
|
|
|
Pin,
|
|
|
|
|
PinOff,
|
|
|
|
|
CheckCircle,
|
|
|
|
|
Circle,
|
|
|
|
|
Eye,
|
|
|
|
|
Loader2,
|
|
|
|
|
} from 'lucide-react'
|
|
|
|
|
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum'
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
interface TopicManagementProps {
|
2025-06-23 21:22:11 +00:00
|
|
|
topics: ForumTopic[]
|
|
|
|
|
categories: ForumCategory[]
|
|
|
|
|
loading: boolean
|
2025-06-23 14:58:13 +00:00
|
|
|
onCreateTopic: (topic: {
|
2025-06-23 21:22:11 +00:00
|
|
|
title: string
|
|
|
|
|
content: string
|
|
|
|
|
categoryId: string
|
|
|
|
|
isPinned?: boolean
|
|
|
|
|
isLocked?: boolean
|
|
|
|
|
}) => Promise<void>
|
|
|
|
|
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<void>
|
|
|
|
|
onDeleteTopic: (id: string) => Promise<void>
|
|
|
|
|
onPinTopic: (id: string) => Promise<void>
|
|
|
|
|
onUnpinTopic: (id: string) => Promise<void>
|
|
|
|
|
onLockTopic: (id: string) => Promise<void>
|
|
|
|
|
onUnlockTopic: (id: string) => Promise<void>
|
|
|
|
|
onMarkTopicAsSolved: (id: string) => Promise<void>
|
|
|
|
|
onMarkTopicAsUnsolved: (id: string) => Promise<void>
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-23 21:22:11 +00:00
|
|
|
export function TopicManagement({
|
|
|
|
|
topics,
|
|
|
|
|
categories,
|
2025-06-23 14:58:13 +00:00
|
|
|
loading,
|
2025-06-23 21:22:11 +00:00
|
|
|
onCreateTopic,
|
|
|
|
|
onUpdateTopic,
|
2025-06-23 14:58:13 +00:00
|
|
|
onDeleteTopic,
|
|
|
|
|
onPinTopic,
|
|
|
|
|
onUnpinTopic,
|
|
|
|
|
onLockTopic,
|
|
|
|
|
onUnlockTopic,
|
|
|
|
|
onMarkTopicAsSolved,
|
2025-06-23 21:22:11 +00:00
|
|
|
onMarkTopicAsUnsolved,
|
2025-06-23 14:58:13 +00:00
|
|
|
}: TopicManagementProps) {
|
2025-06-23 21:22:11 +00:00
|
|
|
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
|
|
|
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null)
|
2025-06-23 14:58:13 +00:00
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
|
title: '',
|
|
|
|
|
content: '',
|
|
|
|
|
categoryId: '',
|
|
|
|
|
isPinned: false,
|
|
|
|
|
isLocked: false,
|
|
|
|
|
isSolved: false,
|
2025-06-23 21:22:11 +00:00
|
|
|
})
|
|
|
|
|
const [submitting, setSubmitting] = useState(false)
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
setFormData({
|
|
|
|
|
title: '',
|
|
|
|
|
content: '',
|
|
|
|
|
categoryId: '',
|
|
|
|
|
isPinned: false,
|
|
|
|
|
isLocked: false,
|
|
|
|
|
isSolved: false,
|
2025-06-23 21:22:11 +00:00
|
|
|
})
|
|
|
|
|
setShowCreateForm(false)
|
|
|
|
|
setEditingTopic(null)
|
|
|
|
|
}
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
2025-06-23 21:22:11 +00:00
|
|
|
e.preventDefault()
|
|
|
|
|
if (submitting) return
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
try {
|
2025-06-23 21:22:11 +00:00
|
|
|
setSubmitting(true)
|
2025-06-23 14:58:13 +00:00
|
|
|
if (editingTopic) {
|
2025-06-23 21:22:11 +00:00
|
|
|
await onUpdateTopic(editingTopic.id, formData)
|
2025-06-23 14:58:13 +00:00
|
|
|
} else {
|
2025-06-23 21:22:11 +00:00
|
|
|
await onCreateTopic(formData)
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
2025-06-23 21:22:11 +00:00
|
|
|
resetForm()
|
2025-06-23 14:58:13 +00:00
|
|
|
} catch (error) {
|
2025-06-23 21:22:11 +00:00
|
|
|
console.error('Error submitting form:', error)
|
2025-06-23 14:58:13 +00:00
|
|
|
} finally {
|
2025-06-23 21:22:11 +00:00
|
|
|
setSubmitting(false)
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
2025-06-23 21:22:11 +00:00
|
|
|
}
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
const handleEdit = (topic: ForumTopic) => {
|
2025-06-23 21:22:11 +00:00
|
|
|
setEditingTopic(topic)
|
2025-06-23 14:58:13 +00:00
|
|
|
setFormData({
|
|
|
|
|
title: topic.title,
|
|
|
|
|
content: topic.content,
|
|
|
|
|
categoryId: topic.categoryId,
|
|
|
|
|
isPinned: topic.isPinned,
|
|
|
|
|
isLocked: topic.isLocked,
|
|
|
|
|
isSolved: topic.isSolved,
|
2025-06-23 21:22:11 +00:00
|
|
|
})
|
|
|
|
|
setShowCreateForm(true)
|
|
|
|
|
}
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
const handlePin = async (topic: ForumTopic) => {
|
|
|
|
|
try {
|
|
|
|
|
if (topic.isPinned) {
|
2025-06-23 21:22:11 +00:00
|
|
|
await onUnpinTopic(topic.id)
|
2025-06-23 14:58:13 +00:00
|
|
|
} else {
|
2025-06-23 21:22:11 +00:00
|
|
|
await onPinTopic(topic.id)
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-06-23 21:22:11 +00:00
|
|
|
console.error('Error toggling pin:', error)
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
2025-06-23 21:22:11 +00:00
|
|
|
}
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
const handleLock = async (topic: ForumTopic) => {
|
|
|
|
|
try {
|
|
|
|
|
if (topic.isLocked) {
|
2025-06-23 21:22:11 +00:00
|
|
|
await onUnlockTopic(topic.id)
|
2025-06-23 14:58:13 +00:00
|
|
|
} else {
|
2025-06-23 21:22:11 +00:00
|
|
|
await onLockTopic(topic.id)
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-06-23 21:22:11 +00:00
|
|
|
console.error('Error toggling lock:', error)
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
2025-06-23 21:22:11 +00:00
|
|
|
}
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
const handleSolved = async (topic: ForumTopic) => {
|
|
|
|
|
try {
|
|
|
|
|
if (topic.isSolved) {
|
2025-06-23 21:22:11 +00:00
|
|
|
await onMarkTopicAsUnsolved(topic.id)
|
2025-06-23 14:58:13 +00:00
|
|
|
} else {
|
2025-06-23 21:22:11 +00:00
|
|
|
await onMarkTopicAsSolved(topic.id)
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-06-23 21:22:11 +00:00
|
|
|
console.error('Error toggling solved status:', error)
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
2025-06-23 21:22:11 +00:00
|
|
|
}
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
const handleDelete = async (id: string) => {
|
2025-06-23 21:22:11 +00:00
|
|
|
if (
|
|
|
|
|
confirm(
|
|
|
|
|
'Are you sure you want to delete this topic? This will also delete all posts in this topic.',
|
|
|
|
|
)
|
|
|
|
|
) {
|
2025-06-23 14:58:13 +00:00
|
|
|
try {
|
2025-06-23 21:22:11 +00:00
|
|
|
await onDeleteTopic(id)
|
2025-06-23 14:58:13 +00:00
|
|
|
} catch (error) {
|
2025-06-23 21:22:11 +00:00
|
|
|
console.error('Error deleting topic:', error)
|
2025-06-23 14:58:13 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-23 21:22:11 +00:00
|
|
|
}
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
const getCategoryName = (categoryId: string) => {
|
2025-06-23 21:22:11 +00:00
|
|
|
const category = categories.find((c) => c.id === categoryId)
|
|
|
|
|
return category ? category.name : 'Unknown Category'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatDate = (value: string | Date) => {
|
|
|
|
|
const date = value instanceof Date ? value : new Date(value)
|
|
|
|
|
if (isNaN(date.getTime())) return 'Invalid Date'
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
return new Intl.DateTimeFormat('en', {
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
month: 'short',
|
|
|
|
|
day: 'numeric',
|
2025-06-23 21:22:11 +00:00
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
}).format(date)
|
|
|
|
|
}
|
2025-06-23 14:58:13 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<h2 className="text-2xl font-bold text-gray-900">Topic Management</h2>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowCreateForm(true)}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
className="flex items-center space-x-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-4 h-4" />
|
|
|
|
|
<span>Add Topic</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Create/Edit Form */}
|
|
|
|
|
{showCreateForm && (
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
|
|
|
{editingTopic ? 'Edit Topic' : 'Create New Topic'}
|
|
|
|
|
</h3>
|
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={formData.title}
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
|
|
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
|
|
|
|
<select
|
|
|
|
|
value={formData.categoryId}
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, categoryId: e.target.value })}
|
|
|
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
|
|
|
required
|
|
|
|
|
>
|
|
|
|
|
<option value="">Select a category</option>
|
2025-06-23 21:22:11 +00:00
|
|
|
{categories.map((category) => (
|
2025-06-23 14:58:13 +00:00
|
|
|
<option key={category.id} value={category.id}>
|
|
|
|
|
{category.name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
|
|
|
|
|
<textarea
|
|
|
|
|
value={formData.content}
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
|
|
|
|
rows={6}
|
|
|
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<div className="flex items-center space-x-6">
|
|
|
|
|
<label className="flex items-center">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={formData.isPinned}
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, isPinned: e.target.checked })}
|
|
|
|
|
className="mr-2"
|
|
|
|
|
/>
|
|
|
|
|
Pinned
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={formData.isLocked}
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, isLocked: e.target.checked })}
|
|
|
|
|
className="mr-2"
|
|
|
|
|
/>
|
|
|
|
|
Locked
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={formData.isSolved}
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, isSolved: e.target.checked })}
|
|
|
|
|
className="mr-2"
|
|
|
|
|
/>
|
|
|
|
|
Solved
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<div className="flex justify-end space-x-3">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={resetForm}
|
|
|
|
|
disabled={submitting}
|
|
|
|
|
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={submitting}
|
|
|
|
|
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
{submitting && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
|
|
|
<span>{editingTopic ? 'Update' : 'Create'}</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Topics List */}
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
|
|
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900">Topics ({topics.length})</h3>
|
|
|
|
|
</div>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
{loading ? (
|
|
|
|
|
<div className="p-8 text-center">
|
|
|
|
|
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600" />
|
|
|
|
|
<p className="text-gray-500">Loading topics...</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="divide-y divide-gray-200">
|
|
|
|
|
{topics
|
2025-06-23 21:22:11 +00:00
|
|
|
.sort(
|
|
|
|
|
(a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime(),
|
|
|
|
|
)
|
2025-06-23 14:58:13 +00:00
|
|
|
.map((topic) => (
|
|
|
|
|
<div key={topic.id} className="p-6 hover:bg-gray-50 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 && <Pin className="w-4 h-4 text-orange-500" />}
|
|
|
|
|
{topic.isLocked && <Lock className="w-4 h-4 text-gray-400" />}
|
|
|
|
|
{topic.isSolved && <CheckCircle className="w-4 h-4 text-emerald-500" />}
|
2025-06-23 21:22:11 +00:00
|
|
|
<h4 className="text-lg font-semibold text-gray-900 line-clamp-1">
|
|
|
|
|
{topic.title}
|
|
|
|
|
</h4>
|
2025-06-23 14:58:13 +00:00
|
|
|
</div>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<p className="text-gray-600 mb-3 line-clamp-2">{topic.content}</p>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<div className="flex items-center justify-between text-sm text-gray-500">
|
|
|
|
|
<div className="flex items-center space-x-4">
|
|
|
|
|
<span className="font-medium">{getCategoryName(topic.categoryId)}</span>
|
|
|
|
|
<span>by {topic.authorName}</span>
|
|
|
|
|
<span>{formatDate(topic.creationTime)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center space-x-4">
|
|
|
|
|
<div className="flex items-center space-x-1">
|
|
|
|
|
<Eye className="w-4 h-4" />
|
|
|
|
|
<span>{topic.viewCount}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span>{topic.replyCount} replies</span>
|
|
|
|
|
<span>{topic.likeCount} likes</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<div className="flex items-center space-x-2 ml-4">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handlePin(topic)}
|
|
|
|
|
className={`p-2 rounded-lg transition-colors ${
|
|
|
|
|
topic.isPinned
|
|
|
|
|
? 'text-orange-600 hover:bg-orange-100'
|
|
|
|
|
: 'text-gray-400 hover:bg-gray-100'
|
|
|
|
|
}`}
|
|
|
|
|
title={topic.isPinned ? 'Unpin Topic' : 'Pin Topic'}
|
|
|
|
|
>
|
2025-06-23 21:22:11 +00:00
|
|
|
{topic.isPinned ? (
|
|
|
|
|
<PinOff className="w-4 h-4" />
|
|
|
|
|
) : (
|
|
|
|
|
<Pin className="w-4 h-4" />
|
|
|
|
|
)}
|
2025-06-23 14:58:13 +00:00
|
|
|
</button>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleLock(topic)}
|
|
|
|
|
className={`p-2 rounded-lg transition-colors ${
|
|
|
|
|
topic.isLocked
|
|
|
|
|
? 'text-yellow-600 hover:bg-yellow-100'
|
|
|
|
|
: 'text-green-600 hover:bg-green-100'
|
|
|
|
|
}`}
|
|
|
|
|
title={topic.isLocked ? 'Unlock Topic' : 'Lock Topic'}
|
|
|
|
|
>
|
2025-06-23 21:22:11 +00:00
|
|
|
{topic.isLocked ? (
|
|
|
|
|
<Lock className="w-4 h-4" />
|
|
|
|
|
) : (
|
|
|
|
|
<Unlock className="w-4 h-4" />
|
|
|
|
|
)}
|
2025-06-23 14:58:13 +00:00
|
|
|
</button>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleSolved(topic)}
|
|
|
|
|
className={`p-2 rounded-lg transition-colors ${
|
|
|
|
|
topic.isSolved
|
|
|
|
|
? 'text-emerald-600 hover:bg-emerald-100'
|
|
|
|
|
: 'text-gray-400 hover:bg-gray-100'
|
|
|
|
|
}`}
|
|
|
|
|
title={topic.isSolved ? 'Mark as Unsolved' : 'Mark as Solved'}
|
|
|
|
|
>
|
2025-06-23 21:22:11 +00:00
|
|
|
{topic.isSolved ? (
|
|
|
|
|
<CheckCircle className="w-4 h-4" />
|
|
|
|
|
) : (
|
|
|
|
|
<Circle className="w-4 h-4" />
|
|
|
|
|
)}
|
2025-06-23 14:58:13 +00:00
|
|
|
</button>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleEdit(topic)}
|
|
|
|
|
className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
|
|
|
|
|
title="Edit Topic"
|
|
|
|
|
>
|
|
|
|
|
<Edit2 className="w-4 h-4" />
|
|
|
|
|
</button>
|
2025-06-23 21:22:11 +00:00
|
|
|
|
2025-06-23 14:58:13 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleDelete(topic.id)}
|
|
|
|
|
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
|
|
|
|
|
title="Delete Topic"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-23 21:22:11 +00:00
|
|
|
)
|
|
|
|
|
}
|