Files
v2.admin.gaertan.art/src/components/artworks/single/EditArtworkForm.tsx

406 lines
15 KiB
TypeScript

"use client"
import { updateArtwork } from "@/actions/artworks/updateArtwork";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import MultipleSelector from "@/components/ui/multiselect";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import type { Tag } from "@/generated/prisma/client";
import { artworkSchema } from "@/schemas/artworks/imageSchema";
import type { ArtworkWithRelations, CategoryWithTags } from "@/types/Artwork";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { z } from "zod/v4";
export default function EditArtworkForm({ artwork, categories, tags }:
{
artwork: ArtworkWithRelations,
categories: CategoryWithTags[]
tags: Tag[]
}) {
const router = useRouter();
const form = useForm<z.infer<typeof artworkSchema>>({
resolver: zodResolver(artworkSchema),
defaultValues: {
name: artwork.name,
needsWork: artwork.needsWork ?? true,
nsfw: artwork.nsfw ?? false,
published: artwork.published ?? false,
setAsHeader: artwork.setAsHeader ?? false,
altText: artwork.altText || "",
description: artwork.description || "",
notes: artwork.notes || "",
month: artwork.month || undefined,
year: artwork.year || undefined,
creationDate: artwork.creationDate ? new Date(artwork.creationDate) : undefined,
categoryIds: artwork.categories?.map(cat => cat.id) ?? [],
tagIds: artwork.tagsV2?.map(tag => tag.id) ?? [],
newCategoryNames: [],
newTagNames: []
}
})
async function onSubmit(values: z.infer<typeof artworkSchema>) {
const updatedArtwork = await updateArtwork(values, artwork.id)
if (updatedArtwork) {
toast.success("Artwork updated")
router.push(`/artworks`)
}
}
return (
<div className="flex flex-col gap-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* String */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Image name</FormLabel>
<FormControl>
<Input {...field} placeholder="The public display name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="altText"
render={({ field }) => (
<FormItem>
<FormLabel>Alt Text</FormLabel>
<FormControl>
<Textarea {...field} placeholder="Alt for this image" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} placeholder="A descriptive text to the image" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notes"
render={({ field }) => (
<FormItem>
<FormLabel>Internal notes</FormLabel>
<FormControl>
<Textarea {...field} placeholder="Any note to the image" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Number */}
<FormField
control={form.control}
name="month"
render={({ field }) => (
<FormItem>
<FormLabel>Creation Month</FormLabel>
<FormControl>
<Input
{...field}
type="number"
value={field.value ?? ''}
onChange={(e) =>
field.onChange(e.target.value === '' ? undefined : +e.target.value)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="year"
render={({ field }) => (
<FormItem>
<FormLabel>Creation Year</FormLabel>
<FormControl>
<Input
{...field}
type="number"
value={field.value ?? ''}
onChange={(e) =>
field.onChange(e.target.value === '' ? undefined : +e.target.value)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Date
<FormField
control={form.control}
name="creationDate"
render={({ field }) => (
<FormItem className="flex flex-col gap-1">
<FormLabel>Creation Date</FormLabel>
<div className="flex items-center gap-2">
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
{field.value ? format(field.value, "PPP") : "Pick a date"}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={(date) => {
field.onChange(date)
}}
initialFocus
fromYear={1990}
toYear={2030}
captionLayout="dropdown"
/>
</PopoverContent>
</Popover>
</div>
<FormMessage />
</FormItem>
)}
/> */}
{/* Select */}
<FormField
control={form.control}
name="categoryIds"
render={({ field }) => {
const existingOptions = categories.map((cat) => ({
label: cat.name,
value: cat.id,
}));
const selectedCategoryIds = field.value ?? [];
const selectedOptions = categories
.filter((cat) => selectedCategoryIds.includes(cat.id))
.map((cat) => ({ label: cat.name, value: cat.id }));
// Also include any "new" selections so they stay visible after selection
const newCategoryNames = form.watch("newCategoryNames") ?? [];
const newSelectedOptions = newCategoryNames.map((name) => ({
label: `Create: ${name}`,
value: `__new__:${name}`,
}));
return (
<FormItem>
<FormLabel>Categories</FormLabel>
<FormControl>
<MultipleSelector
options={existingOptions}
placeholder="Select or type to create categories"
hidePlaceholderWhenSelected
selectFirstItem
value={[...selectedOptions, ...newSelectedOptions]}
creatable
createOption={(raw) => ({
value: `__new__:${raw}`,
label: `Create: ${raw}`,
})}
onChange={(options) => {
const values = options.map((o) => o.value);
const existingIds = values.filter((v) => !v.startsWith("__new__:"));
const newNames = values
.filter((v) => v.startsWith("__new__:"))
.map((v) => v.replace("__new__:", "").trim())
.filter(Boolean);
field.onChange(existingIds);
form.setValue("newCategoryNames", Array.from(new Set(newNames)), {
shouldDirty: true,
shouldValidate: true,
});
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="tagIds"
render={({ field }) => {
const selectedTagIds = field.value ?? [];
const selectedCategoryIds = form.watch("categoryIds") ?? [];
const newTagNames = form.watch("newTagNames") ?? [];
// Tag IDs connected to selected categories
const preferredTagIds = new Set<string>();
for (const cat of categories) {
if (!selectedCategoryIds.includes(cat.id)) continue;
for (const link of cat.tagLinks) preferredTagIds.add(link.tagId);
}
// Existing tag options with groups
const tagOptions = tags
.map((t) => {
let group = "Other tags";
if (selectedTagIds.includes(t.id)) group = "Selected";
else if (preferredTagIds.has(t.id)) group = "From selected categories";
return { label: t.name, value: t.id, group };
})
.sort((a, b) => a.label.localeCompare(b.label));
// Selected existing tags
const selectedExistingOptions = tags
.filter((t) => selectedTagIds.includes(t.id))
.map((t) => ({ label: t.name, value: t.id }));
// Selected "new" tags (so they remain visible)
const selectedNewOptions = newTagNames.map((name) => ({
label: `Create: ${name}`,
value: `__new__:${name}`,
group: "Selected",
}));
return (
<FormItem>
<FormLabel>Tags</FormLabel>
<FormControl>
<MultipleSelector
options={tagOptions}
groupBy="group"
groupOrder={["Selected", "From selected categories", "Other tags"]}
showSelectedInDropdown
placeholder="Select or type to create tags"
hidePlaceholderWhenSelected
selectFirstItem
value={[...selectedExistingOptions, ...selectedNewOptions]}
creatable
createOption={(raw) => ({
value: `__new__:${raw}`,
label: `Create: ${raw}`,
group: "Selected",
})}
onChange={(options) => {
const values = options.map((o) => o.value);
const existingIds = values.filter((v) => !v.startsWith("__new__:"));
const newNames = values
.filter((v) => v.startsWith("__new__:"))
.map((v) => v.replace("__new__:", "").trim())
.filter(Boolean);
field.onChange(existingIds);
form.setValue("newTagNames", Array.from(new Set(newNames)), {
shouldDirty: true,
shouldValidate: true,
});
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
{/* Boolean */}
<FormField
control={form.control}
name="nsfw"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>NSFW</FormLabel>
<FormDescription>This image contains sensitive or adult content.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="setAsHeader"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Set as header image</FormLabel>
<FormDescription>Will be the main banner image. Choose a fitting one.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="needsWork"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Needs some work</FormLabel>
<FormDescription></FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="published"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Publish</FormLabel>
<FormDescription>Will this image be published.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
</div>
</form>
</Form>
</div >
);
}