317 lines
12 KiB
TypeScript
317 lines
12 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
||
import { HelpCircle, Plus, Edit2, Trash2, X, GripVertical, ToggleLeft, ToggleRight } from 'lucide-react';
|
||
import { toast } from 'react-hot-toast';
|
||
import api from '../services/api';
|
||
import Spinner from '../components/ui/Spinner';
|
||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||
|
||
interface FAQ {
|
||
id: string;
|
||
question: string;
|
||
answer: string;
|
||
order: number;
|
||
isActive: boolean;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
export default function FAQManager() {
|
||
const [faqs, setFaqs] = useState<FAQ[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [editingFaq, setEditingFaq] = useState<FAQ | null>(null);
|
||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
|
||
const [formData, setFormData] = useState({
|
||
question: '',
|
||
answer: '',
|
||
order: 0,
|
||
isActive: true,
|
||
});
|
||
|
||
const fetchFaqs = useCallback(async () => {
|
||
try {
|
||
setIsLoading(true);
|
||
const res = await api.get('/faq/admin');
|
||
const data = Array.isArray(res.data) ? res.data : (res.data?.data || []);
|
||
setFaqs(data);
|
||
} catch (err) {
|
||
console.error('Failed to fetch FAQs:', err);
|
||
toast.error('خطا در دریافت سوالات متداول');
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchFaqs();
|
||
}, [fetchFaqs]);
|
||
|
||
const openModal = (item: FAQ | null = null) => {
|
||
if (item) {
|
||
setEditingFaq(item);
|
||
setFormData({
|
||
question: item.question,
|
||
answer: item.answer,
|
||
order: item.order,
|
||
isActive: item.isActive,
|
||
});
|
||
} else {
|
||
setEditingFaq(null);
|
||
setFormData({
|
||
question: '',
|
||
answer: '',
|
||
order: faqs.length,
|
||
isActive: true,
|
||
});
|
||
}
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
const handleSave = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!formData.question.trim() || !formData.answer.trim()) {
|
||
toast.error('سوال و جواب اجباری هستند');
|
||
return;
|
||
}
|
||
try {
|
||
setIsSaving(true);
|
||
if (editingFaq) {
|
||
await api.put(`/faq/${editingFaq.id}`, formData);
|
||
toast.success('سوال با موفقیت بروزرسانی شد');
|
||
} else {
|
||
await api.post('/faq', formData);
|
||
toast.success('سوال جدید اضافه شد');
|
||
}
|
||
setIsModalOpen(false);
|
||
fetchFaqs();
|
||
} catch (err) {
|
||
console.error('Failed to save FAQ:', err);
|
||
toast.error('خطا در ذخیره سوال');
|
||
} finally {
|
||
setIsSaving(false);
|
||
}
|
||
};
|
||
|
||
const toggleActive = async (item: FAQ) => {
|
||
try {
|
||
await api.put(`/faq/${item.id}`, { isActive: !item.isActive });
|
||
toast.success(item.isActive ? 'غیرفعال شد' : 'فعال شد');
|
||
fetchFaqs();
|
||
} catch {
|
||
toast.error('خطا در تغییر وضعیت');
|
||
}
|
||
};
|
||
|
||
const confirmDelete = async () => {
|
||
if (!deleteTargetId) return;
|
||
try {
|
||
await api.delete(`/faq/${deleteTargetId}`);
|
||
toast.success('سوال حذف شد');
|
||
fetchFaqs();
|
||
} catch {
|
||
toast.error('خطا در حذف');
|
||
} finally {
|
||
setDeleteTargetId(null);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||
<div>
|
||
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
|
||
<HelpCircle className="w-6 h-6 text-orange-500" />
|
||
سوالات متداول (FAQ)
|
||
</h2>
|
||
<p className="text-gray-500 font-medium mt-1">
|
||
سوالات و پاسخهایی که در صفحه اصلی سایت نمایش داده میشوند — با Schema.org FAQPage برای SEO
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => openModal()}
|
||
className="bg-orange-500 hover:bg-orange-600 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-orange-200"
|
||
>
|
||
<Plus className="w-5 h-5" />
|
||
افزودن سوال جدید
|
||
</button>
|
||
</div>
|
||
|
||
{/* Info Box */}
|
||
<div className="bg-orange-50 border border-orange-200 rounded-xl p-4 text-sm text-orange-800">
|
||
<strong>💡 نکته SEO:</strong> سوالات فعال به صورت خودکار با markup <code className="bg-orange-100 px-1 rounded">FAQPage</code> در صفحه اصلی نمایش داده میشوند و در نتایج گوگل به عنوان Rich Snippet ظاهر میشوند.
|
||
</div>
|
||
|
||
{/* List */}
|
||
<div className="space-y-3">
|
||
{isLoading ? (
|
||
<div className="flex justify-center py-12">
|
||
<Spinner size="lg" className="text-orange-500" />
|
||
</div>
|
||
) : faqs.length === 0 ? (
|
||
<div className="bg-white rounded-2xl border border-gray-200 py-16 text-center">
|
||
<HelpCircle className="w-12 h-12 text-gray-300 mx-auto mb-3" />
|
||
<p className="text-gray-500 font-medium">هیچ سوالی ثبت نشده است</p>
|
||
<button
|
||
onClick={() => openModal()}
|
||
className="mt-4 text-orange-500 font-bold hover:underline text-sm"
|
||
>
|
||
اولین سوال را اضافه کنید
|
||
</button>
|
||
</div>
|
||
) : (
|
||
faqs.map((faq, idx) => (
|
||
<div
|
||
key={faq.id}
|
||
className={`bg-white rounded-2xl border p-5 transition-all ${
|
||
faq.isActive ? 'border-gray-200' : 'border-dashed border-gray-300 opacity-60'
|
||
}`}
|
||
>
|
||
<div className="flex items-start gap-4">
|
||
<div className="flex items-center gap-2 shrink-0 mt-1">
|
||
<GripVertical className="w-4 h-4 text-gray-300" />
|
||
<span className="text-xs font-black text-gray-400 w-5 text-center">{idx + 1}</span>
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<span className="font-black text-gray-900 text-sm">{faq.question}</span>
|
||
{!faq.isActive && (
|
||
<span className="px-2 py-0.5 bg-gray-100 text-gray-500 text-[10px] font-bold rounded">
|
||
غیرفعال
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2">{faq.answer}</p>
|
||
</div>
|
||
<div className="flex items-center gap-1 shrink-0">
|
||
<button
|
||
onClick={() => toggleActive(faq)}
|
||
className={`p-2 rounded-lg transition-colors ${
|
||
faq.isActive
|
||
? 'text-green-600 hover:bg-green-50'
|
||
: 'text-gray-400 hover:bg-gray-50'
|
||
}`}
|
||
title={faq.isActive ? 'غیرفعال کردن' : 'فعال کردن'}
|
||
>
|
||
{faq.isActive ? <ToggleRight className="w-5 h-5" /> : <ToggleLeft className="w-5 h-5" />}
|
||
</button>
|
||
<button
|
||
onClick={() => openModal(faq)}
|
||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||
>
|
||
<Edit2 className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => setDeleteTargetId(faq.id)}
|
||
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* Form Modal */}
|
||
{isModalOpen && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
|
||
<div className="bg-white rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||
<h3 className="text-lg font-bold text-gray-900">
|
||
{editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'}
|
||
</h3>
|
||
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg">
|
||
<X className="w-5 h-5 text-gray-500" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSave} className="p-6 space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-bold text-gray-700 mb-1">
|
||
سوال <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
required
|
||
value={formData.question}
|
||
onChange={(e) => setFormData({ ...formData, question: e.target.value })}
|
||
placeholder="مثال: آیا محصولات کنینا برای سگهای حساس مناسب است؟"
|
||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-bold text-gray-700 mb-1">
|
||
پاسخ <span className="text-red-500">*</span>
|
||
</label>
|
||
<textarea
|
||
required
|
||
rows={5}
|
||
value={formData.answer}
|
||
onChange={(e) => setFormData({ ...formData, answer: e.target.value })}
|
||
placeholder="پاسخ کامل، واضح و مفید..."
|
||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm resize-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-bold text-gray-700 mb-1">ترتیب نمایش</label>
|
||
<input
|
||
type="number"
|
||
value={formData.order}
|
||
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
|
||
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
|
||
/>
|
||
</div>
|
||
<div className="flex items-end">
|
||
<label className="flex items-center justify-between p-3 bg-orange-50 rounded-xl border border-orange-200 cursor-pointer w-full">
|
||
<span className="text-xs font-bold text-orange-700">✅ فعال و قابل نمایش</span>
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.isActive}
|
||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||
className="rounded text-orange-500 focus:ring-orange-400"
|
||
/>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsModalOpen(false)}
|
||
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
|
||
>
|
||
انصراف
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={isSaving}
|
||
className="px-6 py-2.5 rounded-xl bg-orange-500 text-white font-bold hover:bg-orange-600 transition-colors shadow-md shadow-orange-200 flex items-center gap-2"
|
||
>
|
||
{isSaving && <Spinner size="sm" />}
|
||
ذخیره
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{deleteTargetId && (
|
||
<ConfirmModal
|
||
isOpen={true}
|
||
title="حذف سوال"
|
||
message="آیا از حذف این سوال متداول اطمینان دارید؟"
|
||
onConfirm={confirmDelete}
|
||
onCancel={() => setDeleteTargetId(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|