From e9e576abd9d6c6030aa4bb290e869890831488ad Mon Sep 17 00:00:00 2001 From: yyamashita Date: Mon, 11 May 2026 00:06:52 +0900 Subject: Add lists feature (band recommendation lists with history) New lists, list_entries, list_revisions tables; full CRUD routes under /lists; nav link in root. Co-Authored-By: Claude Sonnet 4.6 --- app/routes/list-edit.tsx | 137 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 app/routes/list-edit.tsx (limited to 'app/routes/list-edit.tsx') diff --git a/app/routes/list-edit.tsx b/app/routes/list-edit.tsx new file mode 100644 index 0000000..5d47737 --- /dev/null +++ b/app/routes/list-edit.tsx @@ -0,0 +1,137 @@ +import { useState } from "react"; +import { data, Form, Link, redirect, useActionData, useLoaderData } from "react-router"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router"; +import { + getBandListById, + getListEntries, + getIpAddress, + updateBandList, + type ListEntryInput, +} from "~/lib/db.server"; + +export async function loader({ params }: LoaderFunctionArgs) { + const list = getBandListById(params.uuid!); + if (!list) throw data("Not found", { status: 404 }); + const entries = getListEntries(list.id); + return { list, entries }; +} + +export async function action({ params, request }: ActionFunctionArgs) { + const list = getBandListById(params.uuid!); + if (!list) throw data("Not found", { status: 404 }); + + const fd = await request.formData(); + const title = (fd.get("title") as string).trim(); + const slug = (fd.get("slug") as string).trim(); + const description = (fd.get("description") as string).trim(); + const message = (fd.get("message") as string).trim(); + const entries: ListEntryInput[] = JSON.parse((fd.get("entries") as string) || "[]"); + + const errors: Record = {}; + if (!title) errors.title = "必須です"; + if (!slug) errors.slug = "必須です"; + if (!message) errors.message = "必須です"; + if (Object.keys(errors).length > 0) return { errors }; + + try { + updateBandList(list.id, { slug, title, description, entries, message, ip_address: getIpAddress(request) }); + } catch (e) { + if (e instanceof Error && e.message.includes("UNIQUE constraint failed: lists.slug")) { + return { errors: { slug: "このslugは既に使用されています" } }; + } + throw e; + } + return redirect(`/lists/of/${list.id}`); +} + +type EntryRow = { key: string; band_name: string; note: string }; + +export default function ListEdit() { + const { list, entries: initEntries } = useLoaderData(); + const actionData = useActionData(); + const errors = actionData?.errors ?? {}; + + const [title, setTitle] = useState(list.title); + const [slug, setSlug] = useState(list.slug); + const [description, setDescription] = useState(list.description); + const [entries, setEntries] = useState( + initEntries.map((e) => ({ key: crypto.randomUUID(), band_name: e.band_name, note: e.note })) + ); + + function addEntry() { + setEntries((prev) => [...prev, { key: crypto.randomUUID(), band_name: "", note: "" }]); + } + + function removeEntry(key: string) { + setEntries((prev) => prev.filter((e) => e.key !== key)); + } + + function updateEntry(key: string, field: "band_name" | "note", value: string) { + setEntries((prev) => prev.map((e) => e.key === key ? { ...e, [field]: value } : e)); + } + + return ( +
+
+ ← +

{list.title} — 編集

+
+ +
+ ({ band_name, note })))} /> + +
+ + setTitle(e.target.value)} /> + {errors.title &&

{errors.title}

} +
+ +
+ + setSlug(e.target.value)} className="mono" /> + {errors.slug &&

{errors.slug}

} +
+ +
+ + setDescription(e.target.value)} /> +
+ +
+ +
+ {entries.map((entry) => ( +
+ updateEntry(entry.key, "band_name", e.target.value)} + placeholder="バンド名" + /> + updateEntry(entry.key, "note", e.target.value)} + placeholder="メモ" + /> + +
+ ))} +
+ +
+ +
+ + + {errors.message &&

{errors.message}

} +
+ +
+ + キャンセル +
+
+
+ ); +} -- cgit v1.2.3