erp-platform/ui/src/views/forum/admin/TopicManagement.tsx

374 lines
14 KiB
TypeScript
Raw Normal View History

2025-06-23 14:58:13 +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';
interface TopicManagementProps {
topics: ForumTopic[];
categories: ForumCategory[];
loading: boolean;
onCreateTopic: (topic: {
title: string;
content: string;
categoryId: string;
isPinned?: boolean;
isLocked?: boolean;
}) => Promise<void>;
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<void>;
onDeleteTopic: (id: string) => Promise<void>;
onPinTopic: (id: string) => Promise<void>;
onUnpinTopic: (id: string) => Promise<void>;
onLockTopic: (id: string) => Promise<void>;
onUnlockTopic: (id: string) => Promise<void>;
onMarkTopicAsSolved: (id: string) => Promise<void>;
onMarkTopicAsUnsolved: (id: string) => Promise<void>;
}
export function TopicManagement({
topics,
categories,
loading,
onCreateTopic,
onUpdateTopic,
onDeleteTopic,
onPinTopic,
onUnpinTopic,
onLockTopic,
onUnlockTopic,
onMarkTopicAsSolved,
onMarkTopicAsUnsolved
}: TopicManagementProps) {
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null);
const [formData, setFormData] = useState({
title: '',
content: '',
categoryId: '',
isPinned: false,
isLocked: false,
isSolved: false,
});
const [submitting, setSubmitting] = useState(false);
const resetForm = () => {
setFormData({
title: '',
content: '',
categoryId: '',
isPinned: false,
isLocked: false,
isSolved: false,
});
setShowCreateForm(false);
setEditingTopic(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (submitting) return;
try {
setSubmitting(true);
if (editingTopic) {
await onUpdateTopic(editingTopic.id, formData);
} else {
await onCreateTopic(formData);
}
resetForm();
} catch (error) {
console.error('Error submitting form:', error);
} finally {
setSubmitting(false);
}
};
const handleEdit = (topic: ForumTopic) => {
setEditingTopic(topic);
setFormData({
title: topic.title,
content: topic.content,
categoryId: topic.categoryId,
isPinned: topic.isPinned,
isLocked: topic.isLocked,
isSolved: topic.isSolved,
});
setShowCreateForm(true);
};
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 handleDelete = async (id: string) => {
if (confirm('Are you sure you want to delete this topic? This will also delete all posts in this topic.')) {
try {
await onDeleteTopic(id);
} catch (error) {
console.error('Error deleting topic:', error);
}
}
};
const getCategoryName = (categoryId: string) => {
const category = categories.find(c => c.id === categoryId);
return category ? category.name : 'Unknown Category';
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(date);
};
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>
<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>
{categories.map(category => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea
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>
<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>
<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>
{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
.sort((a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime())
.map((topic) => (
<div key={topic.id} className="p-6 hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between">
<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" />}
<h4 className="text-lg font-semibold text-gray-900 line-clamp-1">{topic.title}</h4>
</div>
<p className="text-gray-600 mb-3 line-clamp-2">{topic.content}</p>
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center space-x-4">
<span className="font-medium">{getCategoryName(topic.categoryId)}</span>
<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>
<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'}
>
{topic.isPinned ? <PinOff className="w-4 h-4" /> : <Pin className="w-4 h-4" />}
</button>
<button
onClick={() => handleLock(topic)}
className={`p-2 rounded-lg transition-colors ${
topic.isLocked
? 'text-yellow-600 hover:bg-yellow-100'
: 'text-green-600 hover:bg-green-100'
}`}
title={topic.isLocked ? 'Unlock Topic' : 'Lock Topic'}
>
{topic.isLocked ? <Lock className="w-4 h-4" /> : <Unlock className="w-4 h-4" />}
</button>
<button
onClick={() => handleSolved(topic)}
className={`p-2 rounded-lg transition-colors ${
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'}
>
{topic.isSolved ? <CheckCircle className="w-4 h-4" /> : <Circle className="w-4 h-4" />}
</button>
<button
onClick={() => handleEdit(topic)}
className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
title="Edit Topic"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(topic.id)}
className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
title="Delete Topic"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}