kreativortex/app/[locale]/(app)/dashboard/menu/_components/MenuClient.tsx
Jessica Rekcah 4253483f44 jalan
2025-12-02 00:22:34 +07:00

272 lines
13 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { createMenu, updateMenu, deleteMenu } from '../actions';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Pencil, Trash2, Plus, X } from 'lucide-react';
// Schema for form - will be created dynamically with translations
const createFormSchema = (translations: Record<string, string>) => z.object({
nameId: z.string().min(1, translations.nameIdRequired),
nameEn: z.string().optional(),
descriptionId: z.string().min(1, translations.descriptionIdRequired),
descriptionEn: z.string().optional(),
slug: z.string().min(1, translations.slugRequired),
parentId: z.string().optional().nullable(),
isActive: z.boolean().default(true),
});
type Menu = {
id: string;
name: any;
slug: string;
description: any;
parentId: string | null;
isActive: boolean;
children?: Menu[];
parent?: { name: any } | null;
};
type FormData = {
nameId: string;
nameEn?: string;
descriptionId: string;
descriptionEn?: string;
slug: string;
parentId?: string | null;
isActive?: boolean;
};
export default function MenuClient({ initialMenus, translations }: { initialMenus: Menu[]; translations: Record<string, string> }) {
const [isEditing, setIsEditing] = useState(false);
const [currentMenu, setCurrentMenu] = useState<Menu | null>(null);
const [isFormOpen, setIsFormOpen] = useState(false);
const formSchema = createFormSchema(translations);
const {
register,
handleSubmit,
reset,
setValue,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
isActive: true,
},
});
const onSubmit = async (data: FormData) => {
const formData = new FormData();
formData.append('nameId', data.nameId);
if (data.nameEn) formData.append('nameEn', data.nameEn);
formData.append('descriptionId', data.descriptionId);
if (data.descriptionEn) formData.append('descriptionEn', data.descriptionEn);
formData.append('slug', data.slug);
if (data.parentId) formData.append('parentId', data.parentId);
else formData.append('parentId', 'null');
if (data.isActive) formData.append('isActive', 'on');
let result;
if (isEditing && currentMenu) {
result = await updateMenu(currentMenu.id, null, formData);
} else {
result = await createMenu(null, formData);
}
if (result.success) {
resetForm();
} else {
alert(result.error || translations.errorOccurred);
}
};
const handleEdit = (menu: Menu) => {
setCurrentMenu(menu);
setIsEditing(true);
setIsFormOpen(true);
setValue('nameId', menu.name.id || menu.name);
setValue('nameEn', menu.name.en || '');
setValue('descriptionId', menu.description.id || menu.description);
setValue('descriptionEn', menu.description.en || '');
setValue('slug', menu.slug);
setValue('parentId', menu.parentId || 'null');
setValue('isActive', menu.isActive);
};
const handleDelete = async (id: string) => {
if (confirm(translations.confirmDelete)) {
await deleteMenu(id);
}
};
const resetForm = () => {
reset({
isActive: true,
parentId: 'null'
});
setIsEditing(false);
setCurrentMenu(null);
setIsFormOpen(false);
};
const getLocalizedName = (name: any) => {
if (typeof name === 'string') return name;
return name.id || name.en || JSON.stringify(name);
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-white">{translations.title}</h2>
<Button onClick={() => { resetForm(); setIsFormOpen(true); }} className="bg-gold-500 hover:bg-gold-600 text-black">
<Plus className="w-4 h-4 mr-2" />
{translations.addButton}
</Button>
</div>
{isFormOpen && (
<div className="bg-white/5 border border-white/10 p-6 rounded-xl animate-in fade-in slide-in-from-top-4">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-semibold text-white">
{isEditing ? translations.editTitle : translations.addTitle}
</h3>
<Button variant="ghost" size="icon" onClick={resetForm}>
<X className="w-4 h-4 text-gray-400" />
</Button>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="nameId" className="text-gray-200">{translations.nameIdLabel}</Label>
<Input id="nameId" {...register('nameId')} className="bg-white/5 border-white/10 text-white" />
{errors.nameId && <p className="text-red-400 text-sm">{errors.nameId.message}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="nameEn" className="text-gray-200">{translations.nameEnLabel}</Label>
<Input id="nameEn" {...register('nameEn')} className="bg-white/5 border-white/10 text-white" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="descriptionId" className="text-gray-200">{translations.descriptionIdLabel}</Label>
<Textarea id="descriptionId" {...register('descriptionId')} className="bg-white/5 border-white/10 text-white" />
{errors.descriptionId && <p className="text-red-400 text-sm">{errors.descriptionId.message}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="descriptionEn" className="text-gray-200">{translations.descriptionEnLabel}</Label>
<Textarea id="descriptionEn" {...register('descriptionEn')} className="bg-white/5 border-white/10 text-white" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="slug" className="text-gray-200">{translations.slugLabel}</Label>
<Input id="slug" {...register('slug')} className="bg-white/5 border-white/10 text-white" />
{errors.slug && <p className="text-red-400 text-sm">{errors.slug.message}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="parentId" className="text-gray-200">{translations.parentLabel}</Label>
<select
id="parentId"
{...register('parentId')}
className="w-full h-10 px-3 py-2 rounded-md border border-white/10 bg-white/5 text-white focus:outline-none focus:ring-2 focus:ring-gold-500"
>
<option value="null" className="bg-gray-900">{translations.parentNone}</option>
{initialMenus.map((menu) => (
<option key={menu.id} value={menu.id} className="bg-gray-900">
{getLocalizedName(menu.name)}
</option>
))}
</select>
</div>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="isActive"
{...register('isActive')}
className="w-4 h-4 rounded border-gray-300 text-gold-600 focus:ring-gold-500"
/>
<Label htmlFor="isActive" className="text-gray-200">{translations.activeLabel}</Label>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={resetForm} className="border-white/10 text-gray-300 hover:bg-white/10 hover:text-white">
{translations.cancelButton}
</Button>
<Button type="submit" disabled={isSubmitting} className="bg-gold-500 hover:bg-gold-600 text-black">
{isSubmitting ? translations.saving : (isEditing ? translations.saveButton : translations.createButton)}
</Button>
</div>
</form>
</div>
)}
<div className="bg-white/5 border border-white/10 rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-white/10 bg-white/5">
<th className="p-4 text-gray-300 font-medium">{translations.tableName}</th>
<th className="p-4 text-gray-300 font-medium">{translations.tableSlug}</th>
<th className="p-4 text-gray-300 font-medium">{translations.tableParent}</th>
<th className="p-4 text-gray-300 font-medium">{translations.tableStatus}</th>
<th className="p-4 text-gray-300 font-medium text-right">{translations.tableActions}</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{initialMenus.map((menu) => (
<tr key={menu.id} className="hover:bg-white/5 transition-colors">
<td className="p-4 text-white font-medium">
{getLocalizedName(menu.name)}
<div className="text-xs text-gray-500 mt-1 truncate max-w-[200px]">
{getLocalizedName(menu.description)}
</div>
</td>
<td className="p-4 text-gray-300">{menu.slug}</td>
<td className="p-4 text-gray-300">
{menu.parent ? getLocalizedName(menu.parent.name) : '-'}
</td>
<td className="p-4">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${menu.isActive ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'
}`}>
{menu.isActive ? translations.statusActive : translations.statusInactive}
</span>
</td>
<td className="p-4 text-right space-x-2">
<Button variant="ghost" size="icon" onClick={() => handleEdit(menu)} className="hover:bg-white/10 hover:text-gold-400">
<Pencil className="w-4 h-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => handleDelete(menu.id)} className="hover:bg-white/10 hover:text-red-400">
<Trash2 className="w-4 h-4" />
</Button>
</td>
</tr>
))}
{initialMenus.length === 0 && (
<tr>
<td colSpan={5} className="p-8 text-center text-gray-500">
{translations.noMenus}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
}