feat: full site build — Project/Melody schema (Option A), admin CRUD, public sections, uploads, email+SMTP, internal analytics, legal pages, docs
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user