import { useQueryClient } from "@tanstack/react-query"; import { X } from "lucide-react"; import { type FC, type FormEvent, useState } from "react"; import { getGetLibraryQueryKey, useCreateCustomGame, useUpdateCustomGame, } from "@/api/gen/library/library"; import type { CustomInput } from "@/api/gen/model/customInput"; import type { GameEntry } from "@/api/gen/model/gameEntry"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { m } from "@/paraglide/messages"; import { customId } from "./helpers"; interface FormState { title: string; portrait: string; hero: string; header: string; logo: string; command: string; } const emptyForm: FormState = { title: "", portrait: "", hero: "", header: "", logo: "", command: "", }; function formFrom(entry: GameEntry): FormState { return { title: entry.title, portrait: entry.art.portrait ?? "", hero: entry.art.hero ?? "", header: entry.art.header ?? "", logo: entry.art.logo ?? "", command: entry.launch?.kind === "command" ? entry.launch.value : "", }; } /** Map the form to the API body — only attach `launch` when a command was given. `update_custom` * REPLACES the whole `art`, so every field the form knows must round-trip (else editing a game with * a `logo` would silently drop it). */ function toInput(f: FormState): CustomInput { const trim = (s: string) => { const t = s.trim(); return t ? t : undefined; }; const command = f.command.trim(); return { title: f.title.trim(), art: { portrait: trim(f.portrait), hero: trim(f.hero), header: trim(f.header), logo: trim(f.logo), }, launch: command ? { kind: "command", value: command } : null, }; } /** What the form targets: an existing custom entry to edit, or "new" for a fresh add. */ export type FormTarget = GameEntry | "new"; /** * Container: the add/edit form — owns the create + update mutations and derives the * initial field state from the target. Kept entirely separate from the overview grid * (own file, own queries) so the two concerns don't share a component. */ export const GameFormSection: FC<{ target: FormTarget; onClose: () => void; }> = ({ target, onClose }) => { const qc = useQueryClient(); const create = useCreateCustomGame(); const update = useUpdateCustomGame(); const invalidate = () => qc.invalidateQueries({ queryKey: getGetLibraryQueryKey() }); const onSubmit = async (data: CustomInput) => { if (target === "new") await create.mutateAsync({ data }).then(invalidate); else await update.mutateAsync({ id: customId(target), data }).then(invalidate); onClose(); }; return ( ); }; /** * The add/edit form card. Owns only its own field state (re-seeded per mount — the * parent keys it by target); reports a ready-to-send `CustomInput` on submit. */ export const GameForm: FC<{ initial: FormState; mode: "add" | "edit"; onSubmit: (data: CustomInput) => void; onCancel: () => void; isSaving: boolean; }> = ({ initial, mode, onSubmit, onCancel, isSaving }) => { const [form, setForm] = useState(initial); const handleSubmit = (e: FormEvent) => { e.preventDefault(); const data = toInput(form); if (!data.title) return; onSubmit(data); }; return ( {mode === "edit" ? m.library_edit_title() : m.library_add_title()}
setForm((f) => ({ ...f, title: e.target.value })) } />
setForm((f) => ({ ...f, portrait: e.target.value })) } />
setForm((f) => ({ ...f, hero: e.target.value }))} />
setForm((f) => ({ ...f, header: e.target.value })) } />
setForm((f) => ({ ...f, logo: e.target.value }))} />
setForm((f) => ({ ...f, command: e.target.value })) } />

{m.library_field_command_help()}

); };