272 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|