286 lines
14 KiB
JavaScript
286 lines
14 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Head, router } from '@inertiajs/react';
|
|
import AdminLayout from '@/Layouts/AdminLayout';
|
|
import {
|
|
Plus,
|
|
Search,
|
|
Edit2,
|
|
Trash2,
|
|
HelpCircle,
|
|
Info
|
|
} from 'lucide-react';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
|
|
export default function FaqsIndex({ faqs }) {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [editingFaq, setEditingFaq] = useState(null);
|
|
|
|
const [questionText, setQuestionText] = useState('');
|
|
const [answerText, setAnswerText] = useState('');
|
|
const [isPublished, setIsPublished] = useState(true);
|
|
const [error, setError] = useState('');
|
|
|
|
const handleOpenAdd = () => {
|
|
setEditingFaq(null);
|
|
setQuestionText('');
|
|
setAnswerText('');
|
|
setIsPublished(true);
|
|
setError('');
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
const handleOpenEdit = (faq) => {
|
|
setEditingFaq(faq);
|
|
setQuestionText(faq.question);
|
|
setAnswerText(faq.answer);
|
|
setIsPublished(faq.is_published);
|
|
setError('');
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
const handleSubmit = (e) => {
|
|
e.preventDefault();
|
|
if (!questionText.trim()) {
|
|
setError('Question is required.');
|
|
return;
|
|
}
|
|
if (!answerText.trim()) {
|
|
setError('Answer is required.');
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
question: questionText.trim(),
|
|
answer: answerText.trim(),
|
|
is_published: isPublished
|
|
};
|
|
|
|
if (editingFaq) {
|
|
router.post(`/admin/faqs/${editingFaq.id}`, payload, {
|
|
onSuccess: () => {
|
|
setIsDialogOpen(false);
|
|
},
|
|
onError: (err) => {
|
|
setError(Object.values(err)[0] || 'Failed to update FAQ.');
|
|
}
|
|
});
|
|
} else {
|
|
router.post('/admin/faqs', payload, {
|
|
onSuccess: () => {
|
|
setIsDialogOpen(false);
|
|
},
|
|
onError: (err) => {
|
|
setError(Object.values(err)[0] || 'Failed to add FAQ.');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDelete = (id) => {
|
|
if (confirm('Are you sure you want to delete this FAQ?')) {
|
|
router.delete(`/admin/faqs/${id}`);
|
|
}
|
|
};
|
|
|
|
const filteredFaqs = faqs.filter(faq =>
|
|
faq.question.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
faq.answer.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
return (
|
|
<AdminLayout title="FAQ Management">
|
|
<Head title="Master Data - FAQ Management" />
|
|
|
|
<div className="max-w-4xl mx-auto space-y-6">
|
|
{/* Header Actions */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div className="relative flex-1 max-w-sm">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Find FAQ..."
|
|
value={searchTerm}
|
|
onChange={e => setSearchTerm(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-teal-500/10 outline-none transition-all"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={handleOpenAdd}
|
|
className="bg-[#0F6E56] text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-all shadow-lg shadow-teal-500/10 cursor-pointer"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Add FAQ</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* FAQs Card */}
|
|
<div className="bg-white rounded-[24px] border border-slate-200 shadow-sm overflow-hidden">
|
|
<div className="p-6 border-b border-slate-50 bg-slate-50/30">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="p-2 bg-teal-50 rounded-lg">
|
|
<HelpCircle className="w-5 h-5 text-[#0F6E56]" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-sm font-black text-slate-900 uppercase tracking-tight font-sans">Frequently Asked Questions</h2>
|
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5 font-sans">Manage support & compliance FAQs displayed to users</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-white hover:bg-white">
|
|
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12 pl-6">Question & Answer</TableHead>
|
|
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12">Status</TableHead>
|
|
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12 text-right pr-6">Action</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredFaqs.length > 0 ? (
|
|
filteredFaqs.map((faq) => (
|
|
<TableRow key={faq.id} className="hover:bg-slate-50/50 transition-colors group">
|
|
<TableCell className="py-4 pl-6 max-w-[500px]">
|
|
<div className="space-y-1">
|
|
<div className="font-bold text-slate-900 text-sm leading-snug">
|
|
{faq.question.trim().endsWith('?') ? faq.question.trim() : faq.question.trim() + '?'}
|
|
</div>
|
|
<div className="text-slate-500 text-xs font-medium leading-relaxed line-clamp-2">{faq.answer}</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="align-middle">
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-tight ${
|
|
faq.is_published
|
|
? 'bg-emerald-50 text-emerald-600'
|
|
: 'bg-slate-100 text-slate-500'
|
|
}`}>
|
|
{faq.is_published ? 'Published' : 'Draft'}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="text-right pr-6 align-middle">
|
|
<div className="flex items-center justify-end space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button
|
|
onClick={() => handleOpenEdit(faq)}
|
|
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all cursor-pointer"
|
|
>
|
|
<Edit2 className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(faq.id)}
|
|
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all cursor-pointer"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={3} className="py-12 text-center text-slate-400 font-bold text-xs">
|
|
No FAQs found. Add one to get started!
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
|
|
<div className="p-6 bg-slate-50/50 border-t border-slate-100">
|
|
<div className="flex items-start space-x-3 text-slate-400">
|
|
<Info className="w-4 h-4 shrink-0 mt-0.5" />
|
|
<p className="text-[10px] font-bold uppercase tracking-wide leading-relaxed">
|
|
Tip: Draft FAQs will not be visible on user-facing support panels. Publish them to make them active.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* FAQ Add/Edit Modal */}
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogContent className="sm:max-w-[500px] rounded-[24px] bg-white">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl font-black text-slate-900 tracking-tight">
|
|
{editingFaq ? 'Edit FAQ' : 'New FAQ'}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="py-4 space-y-4">
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Question</label>
|
|
<input
|
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none"
|
|
placeholder="e.g. How do I request a refund?"
|
|
value={questionText}
|
|
onChange={e => setQuestionText(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Answer</label>
|
|
<textarea
|
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[120px] resize-none"
|
|
placeholder="Describe the answer in detail..."
|
|
value={answerText}
|
|
onChange={e => setAnswerText(e.target.value)}
|
|
/>
|
|
{error && (
|
|
<p className="text-xs text-rose-600 font-bold ml-1">{error}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3 pt-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsPublished(!isPublished)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
|
|
isPublished ? 'bg-[#0F6E56]' : 'bg-slate-200'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
isPublished ? 'translate-x-6' : 'translate-x-1'
|
|
}`}
|
|
/>
|
|
</button>
|
|
<span className="text-xs font-bold text-slate-600">Publish Immediately</span>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="sm:justify-end gap-3 pt-4 border-t border-slate-100">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsDialogOpen(false)}
|
|
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all cursor-pointer"
|
|
>
|
|
{editingFaq ? 'Update' : 'Add FAQ'}
|
|
</button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</AdminLayout>
|
|
);
|
|
}
|