152 lines
7.6 KiB
TypeScript
152 lines
7.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import AdminForm from "@/components/admin/admin-form";
|
|
import FileUpload from "@/components/admin/file-upload";
|
|
import { createMelody, updateMelody } from "@/app/admin/(protected)/melodies/actions";
|
|
import { melodySchema, type MelodyInput } from "@/lib/validations/melody";
|
|
|
|
type CategoryOption = { id: string; nameAr: string; nameEn: string };
|
|
|
|
type MelodyFormProps = {
|
|
mode: "create" | "edit";
|
|
melodyId?: string;
|
|
categories: CategoryOption[];
|
|
defaultValues: MelodyInput;
|
|
};
|
|
|
|
const statusLabels: Record<MelodyInput["status"], string> = {
|
|
DRAFT: "Draft",
|
|
PUBLISHED: "Published",
|
|
ARCHIVED: "Archived",
|
|
};
|
|
|
|
export default function MelodyForm({ mode, melodyId, categories, defaultValues }: MelodyFormProps) {
|
|
const router = useRouter();
|
|
const [serverError, setServerError] = useState<string | null>(null);
|
|
const form = useForm<MelodyInput>({ resolver: zodResolver(melodySchema), defaultValues });
|
|
|
|
const onSubmit = async (values: MelodyInput) => {
|
|
setServerError(null);
|
|
const result = mode === "create" ? await createMelody(values) : await updateMelody(melodyId ?? "", values);
|
|
if (result.error) {
|
|
setServerError(result.error);
|
|
return;
|
|
}
|
|
router.push("/admin/melodies");
|
|
router.refresh();
|
|
};
|
|
|
|
return (
|
|
<AdminForm
|
|
form={form}
|
|
onSubmit={onSubmit}
|
|
submitLabel={mode === "create" ? "Create melody" : "Save changes"}
|
|
submittingLabel="Saving…"
|
|
className="max-w-3xl"
|
|
>
|
|
<div className="grid gap-5 md:grid-cols-2">
|
|
<FormField control={form.control} name="status" render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Status</FormLabel>
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<FormControl><SelectTrigger><SelectValue placeholder="Choose a status" /></SelectTrigger></FormControl>
|
|
<SelectContent>{Object.entries(statusLabels).map(([value, label]) => <SelectItem key={value} value={value}>{label}</SelectItem>)}</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)} />
|
|
<FormField control={form.control} name="categoryId" render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Melody category</FormLabel>
|
|
<Select value={field.value || "__none"} onValueChange={(value) => field.onChange(value === "__none" ? "" : value)}>
|
|
<FormControl><SelectTrigger><SelectValue placeholder="Choose a category" /></SelectTrigger></FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="__none">Choose a category</SelectItem>
|
|
{categories.map((category) => <SelectItem key={category.id} value={category.id}>{category.nameEn} · {category.nameAr}</SelectItem>)}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)} />
|
|
</div>
|
|
|
|
<FormField control={form.control} name="slug" render={({ field }) => (
|
|
<FormItem><FormLabel>Slug</FormLabel><FormControl><Input placeholder="melody-slug" {...field} /></FormControl><FormMessage /></FormItem>
|
|
)} />
|
|
|
|
<div className="grid gap-5 md:grid-cols-2">
|
|
<FormField control={form.control} name="titleEn" render={({ field }) => (
|
|
<FormItem><FormLabel>English title</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
|
)} />
|
|
<FormField control={form.control} name="titleAr" render={({ field }) => (
|
|
<FormItem><FormLabel>Arabic title</FormLabel><FormControl><Input dir="rtl" {...field} /></FormControl><FormMessage /></FormItem>
|
|
)} />
|
|
</div>
|
|
|
|
<div className="grid gap-5 md:grid-cols-2">
|
|
<FormField control={form.control} name="descEn" render={({ field }) => (
|
|
<FormItem><FormLabel>English description</FormLabel><FormControl><textarea className="min-h-32 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" {...field} value={field.value ?? ""} /></FormControl><FormMessage /></FormItem>
|
|
)} />
|
|
<FormField control={form.control} name="descAr" render={({ field }) => (
|
|
<FormItem><FormLabel>Arabic description</FormLabel><FormControl><textarea dir="rtl" className="min-h-32 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" {...field} value={field.value ?? ""} /></FormControl><FormMessage /></FormItem>
|
|
)} />
|
|
</div>
|
|
|
|
<FormField control={form.control} name="audioFile" render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Audio file path</FormLabel>
|
|
<FormControl><Input placeholder="/api/uploads/audios/..." {...field} /></FormControl>
|
|
<FormMessage />
|
|
<FileUpload id="melody-audio-file" kind="audio" label="Upload audio file" onUploaded={(files) => field.onChange(files[0]?.url ?? field.value)} />
|
|
</FormItem>
|
|
)} />
|
|
|
|
<div className="grid gap-5 md:grid-cols-2">
|
|
<FormField control={form.control} name="coverImage" render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Cover image path</FormLabel>
|
|
<FormControl><Input placeholder="/api/uploads/images/..." {...field} value={field.value ?? ""} /></FormControl>
|
|
<FormMessage />
|
|
<FileUpload id="melody-cover-image" kind="image" label="Upload cover image" onUploaded={(files) => field.onChange(files[0]?.url ?? field.value ?? "")} />
|
|
</FormItem>
|
|
)} />
|
|
<FormField control={form.control} name="durationSec" render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Duration (seconds)</FormLabel>
|
|
<FormControl><Input type="number" min={0} max={86400} value={field.value ?? ""} onChange={(event) => field.onChange(event.target.value ? event.target.valueAsNumber : null)} /></FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)} />
|
|
</div>
|
|
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
<FormField control={form.control} name="isDownloadable" render={({ field }) => (
|
|
<FormItem className="flex items-center gap-3 rounded-md border border-border p-3">
|
|
<FormControl><Input type="checkbox" checked={field.value} onChange={(event) => field.onChange(event.target.checked)} className="h-4 w-4" /></FormControl>
|
|
<FormLabel className="m-0">Allow audio download</FormLabel>
|
|
</FormItem>
|
|
)} />
|
|
<FormField control={form.control} name="isFeatured" render={({ field }) => (
|
|
<FormItem className="flex items-center gap-3 rounded-md border border-border p-3">
|
|
<FormControl><Input type="checkbox" checked={field.value} onChange={(event) => field.onChange(event.target.checked)} className="h-4 w-4" /></FormControl>
|
|
<FormLabel className="m-0">Feature this melody</FormLabel>
|
|
</FormItem>
|
|
)} />
|
|
</div>
|
|
|
|
<FormField control={form.control} name="sortOrder" render={({ field }) => (
|
|
<FormItem><FormLabel>Sort order</FormLabel><FormControl><Input type="number" min={0} max={9999} value={field.value} onChange={(event) => field.onChange(event.target.valueAsNumber || 0)} /></FormControl><FormMessage /></FormItem>
|
|
)} />
|
|
|
|
{serverError ? <p className="text-sm font-medium text-destructive">{serverError}</p> : null}
|
|
</AdminForm>
|
|
);
|
|
}
|