From b8d24d292d99c8da285092ce923b5e2b546d8f45 Mon Sep 17 00:00:00 2001 From: yyamashita Date: Sat, 9 May 2026 00:27:19 +0900 Subject: Implement band/artist management with version history Full CRUD for bands and artists: UUID + slug URLs, dynamic link editor, band-artist associations with roles, per-edit revision snapshots (message + IP). Add README and CLAUDE.md. Co-Authored-By: Claude Sonnet 4.6 --- app/routes/artist-edit.tsx | 165 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 app/routes/artist-edit.tsx (limited to 'app/routes/artist-edit.tsx') diff --git a/app/routes/artist-edit.tsx b/app/routes/artist-edit.tsx new file mode 100644 index 0000000..f2e5c18 --- /dev/null +++ b/app/routes/artist-edit.tsx @@ -0,0 +1,165 @@ +import { useState } from "react"; +import { data, Form, Link, redirect, useActionData, useLoaderData } from "react-router"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router"; +import { getArtistById, getArtistLinks, getIpAddress, updateArtist } from "~/lib/db.server"; + +export async function loader({ params }: LoaderFunctionArgs) { + const artist = getArtistById(params.uuid!); + if (!artist) throw data("Not found", { status: 404 }); + const links = getArtistLinks(artist.id); + return { artist, links }; +} + +export async function action({ params, request }: ActionFunctionArgs) { + const artist = getArtistById(params.uuid!); + if (!artist) throw data("Not found", { status: 404 }); + + const fd = await request.formData(); + const name = (fd.get("name") as string).trim(); + const slug = (fd.get("slug") as string).trim(); + const message = (fd.get("message") as string).trim(); + const links: { label: string; url: string }[] = JSON.parse( + (fd.get("links") as string) || "[]" + ); + + const errors: Record = {}; + if (!name) errors.name = "必須です"; + if (!slug) errors.slug = "必須です"; + if (!message) errors.message = "必須です"; + if (Object.keys(errors).length > 0) return { errors }; + + try { + updateArtist(artist.id, { slug, name, links, message, ip_address: getIpAddress(request) }); + } catch (e) { + if (e instanceof Error && e.message.includes("UNIQUE constraint failed: artists.slug")) { + return { errors: { slug: "このslugは既に使用されています" } }; + } + throw e; + } + return redirect(`/artists/of/${artist.id}`); +} + +function toSlug(s: string) { + return s.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^\w぀-ヿ一-鿿＀-￯-]/g, "").replace(/^-+|-+$/g, ""); +} + +export default function ArtistEdit() { + const { artist, links: initLinks } = useLoaderData(); + const actionData = useActionData(); + const errors = actionData?.errors ?? {}; + + const [name, setName] = useState(artist.name); + const [slug, setSlug] = useState(artist.slug); + const [slugManual, setSlugManual] = useState(true); + const [links, setLinks] = useState(initLinks.map((l) => ({ label: l.label, url: l.url }))); + + return ( +
+
+ + ← + +

Edit Artist

+
+ +
+ + +
+ + { + setName(e.target.value); + if (!slugManual) setSlug(toSlug(e.target.value)); + }} + className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500" + /> + {errors.name &&

{errors.name}

} +
+ +
+ + { setSlugManual(true); setSlug(e.target.value); }} + className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 font-mono text-sm" + /> + {errors.slug &&

{errors.slug}

} +
+ +
+ +
+ {links.map((link, i) => ( +
+ setLinks(links.map((l, idx) => idx === i ? { ...l, label: e.target.value } : l))} + placeholder="ラベル (例: X)" + className="w-28 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm" + /> + setLinks(links.map((l, idx) => idx === i ? { ...l, url: e.target.value } : l))} + placeholder="https://..." + className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm" + /> + +
+ ))} +
+ +
+ +
+ + + {errors.message &&

{errors.message}

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