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:
2026-08-05 20:53:40 +02:00
parent d87f3033c6
commit c26f41511f
122 changed files with 15068 additions and 41 deletions
+154
View File
@@ -0,0 +1,154 @@
"use client";
import { useState } from "react";
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 { createCategory, updateCategory } from "@/app/admin/(protected)/categories/actions";
import { categorySchema, type CategoryInput } from "@/lib/validations/category";
import { useRouter } from "next/navigation";
type CategoryFormProps = {
mode: "create" | "edit";
categoryId?: string;
defaultValues: CategoryInput;
};
const kindLabels: Record<CategoryInput["kind"], string> = {
PROJECT: "Projects",
MELODY: "Melodies",
};
export default function CategoryForm({ mode, categoryId, defaultValues }: CategoryFormProps) {
const router = useRouter();
const [serverError, setServerError] = useState<string | null>(null);
const form = useForm<CategoryInput>({
resolver: zodResolver(categorySchema),
defaultValues,
});
const onSubmit = async (values: CategoryInput) => {
setServerError(null);
const result =
mode === "create" ? await createCategory(values) : await updateCategory(categoryId ?? "", values);
if (result.error) {
setServerError(result.error);
return;
}
router.push("/admin/categories");
router.refresh();
};
return (
<AdminForm
form={form}
onSubmit={onSubmit}
submitLabel={mode === "create" ? "Create category" : "Save changes"}
submittingLabel="Saving…"
className="max-w-2xl"
>
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem>
<FormLabel>Section</FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Choose a section" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(kindLabels).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input placeholder="web-design" autoComplete="off" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid gap-5 sm:grid-cols-2">
<FormField
control={form.control}
name="nameAr"
render={({ field }) => (
<FormItem>
<FormLabel>Arabic name</FormLabel>
<FormControl>
<Input dir="rtl" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="nameEn"
render={({ field }) => (
<FormItem>
<FormLabel>English name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="order"
render={({ field }) => (
<FormItem>
<FormLabel>Display order</FormLabel>
<FormControl>
<Input
type="number"
min={0}
max={9999}
inputMode="numeric"
{...field}
onChange={(event) => field.onChange(event.target.valueAsNumber || 0)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{serverError ? <p className="text-sm font-medium text-destructive">{serverError}</p> : null}
</AdminForm>
);
}