diff --git a/app/admin/categories/categories-admin-client.tsx b/app/admin/categories/categories-admin-client.tsx new file mode 100644 index 0000000..79dca24 --- /dev/null +++ b/app/admin/categories/categories-admin-client.tsx @@ -0,0 +1,250 @@ +'use client'; + +import { useEffect, useState, useTransition } from 'react'; +import { + listCategories, + createCategory, + updateCategory, + deleteCategory +} from '@/lib/actions/admin'; +import { Plus, Trash2, Edit2, X, Check } from 'lucide-react'; + +interface Category { + id: string; + name: string; + slug: string; + description: string | null; + createdAt: Date; +} + +export default function CategoriesAdminClient() { + const [categories, setCategories] = useState([]); + const [isPending, startTransition] = useTransition(); + const [feedback, setFeedback] = useState<{ type: 'success' | 'error'; msg: string } | null>(null); + + // State for Create/Edit + const [isEditing, setIsEditing] = useState(null); // ID of category or 'new' + const [formData, setFormData] = useState({ name: '', slug: '', description: '' }); + + function loadCategories() { + startTransition(async () => { + const data = await listCategories(); + setCategories(data as Category[]); + }); + } + + useEffect(() => { + loadCategories(); + }, []); + + const handleEdit = (cat: Category) => { + setIsEditing(cat.id); + setFormData({ + name: cat.name, + slug: cat.slug, + description: cat.description || '' + }); + }; + + const handleCancel = () => { + setIsEditing(null); + setFormData({ name: '', slug: '', description: '' }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + startTransition(async () => { + let result; + if (isEditing === 'new') { + result = await createCategory(formData); + } else if (isEditing) { + result = await updateCategory(isEditing, formData); + } + + if (result?.error) { + setFeedback({ type: 'error', msg: result.error }); + } else { + setFeedback({ type: 'success', msg: isEditing === 'new' ? 'Categoria criada!' : 'Categoria atualizada!' }); + handleCancel(); + loadCategories(); + } + setTimeout(() => setFeedback(null), 3000); + }); + }; + + const handleDelete = async (id: string) => { + if (!confirm('Tens a certeza que queres eliminar esta categoria?')) return; + + startTransition(async () => { + const result = await deleteCategory(id); + if (result.error) { + setFeedback({ type: 'error', msg: result.error }); + } else { + setFeedback({ type: 'success', msg: 'Categoria eliminada.' }); + loadCategories(); + } + setTimeout(() => setFeedback(null), 3000); + }); + }; + + const generateSlug = (name: string) => { + return name.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); + }; + + return ( +
+ {/* Header */} +
+
+

+ Gestão de Categorias +

+

+ Define e organiza os tópicos oficiais da plataforma. +

+
+ + {!isEditing && ( + + )} +
+ + {/* Feedback */} + {feedback && ( +
+ {feedback.msg} +
+ )} + + {/* Form Editor (Inline) */} + {isEditing && ( +
+

+ {isEditing === 'new' ? 'Criar Nova Categoria' : 'Editar Categoria'} +

+
+
+ + { + const val = e.target.value; + setFormData({ ...formData, name: val, slug: generateSlug(val) }); + }} + placeholder="Ex: Sistemas Distribuídos" + className="h-10 w-full rounded-lg border border-border bg-background px-3 text-sm focus:border-primary outline-none" + /> +
+
+ + setFormData({ ...formData, slug: e.target.value })} + className="h-10 w-full rounded-lg border border-border bg-background px-3 text-sm font-mono focus:border-primary outline-none" + /> +
+
+
+ +