import { useState } from "react"; import { data, Form, Link, redirect, useActionData, useLoaderData } from "react-router"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router"; import { getBandById, getBandLinks, getBandMembers, getIpAddress, listArtists, updateBand, type MemberInput, } from "~/lib/db.server"; import { ARTIST_ROLES, LINK_TYPES } from "~/lib/constants"; export async function loader({ params }: LoaderFunctionArgs) { const band = getBandById(params.uuid!); if (!band) throw data("Not found", { status: 404 }); const links = getBandLinks(band.id); const members = getBandMembers(band.id); const allArtists = listArtists(); return { band, links, members, allArtists }; } export async function action({ params, request }: ActionFunctionArgs) { const band = getBandById(params.uuid!); if (!band) 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 area = (fd.get("area") as string).trim() || null; const description = (fd.get("description") as string).trim() || null; const status = (fd.get("status") as string) || "active"; const message = (fd.get("message") as string).trim(); const links: { label: string; url: string }[] = JSON.parse((fd.get("links") as string) || "[]"); const members: MemberInput[] = JSON.parse((fd.get("members") 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 { updateBand(band.id, { slug, name, area, description, status, links, members, message, ip_address: getIpAddress(request) }); } catch (e) { if (e instanceof Error && e.message.includes("UNIQUE constraint failed: bands.slug")) { return { errors: { slug: "このslugは既に使用されています" } }; } throw e; } return redirect(`/bands/of/${band.id}`); } function toSlug(s: string) { return s.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^\w぀-ヿ一-鿿＀-￯-]/g, "").replace(/^-+|-+$/g, ""); } type MemberEntry = { key: string; artist_id: string; artist_name: string; roles: string[]; since: string; until: string; note: string; pendingRole: { type: string; custom: string }; }; const DEFAULT_PENDING = { type: ARTIST_ROLES[0], custom: "" }; export default function BandEdit() { const { band, links: initLinks, members: initMembers, allArtists } = useLoaderData(); const actionData = useActionData(); const errors = actionData?.errors ?? {}; const [name, setName] = useState(band.name); const [slug, setSlug] = useState(band.slug); const [slugManual, setSlugManual] = useState(true); const [links, setLinks] = useState(initLinks.map((l) => ({ label: l.label, url: l.url }))); const [entries, setEntries] = useState( initMembers.map((m) => ({ key: crypto.randomUUID(), artist_id: m.artist_id, artist_name: m.artist_name, roles: m.role ? m.role.split(", ").filter(Boolean) : [], since: m.since, until: m.until, note: m.note, pendingRole: { ...DEFAULT_PENDING }, })) ); const usedArtistIds = new Set(entries.map((e) => e.artist_id)); const available = allArtists.filter((a) => !usedArtistIds.has(a.id)); const artistIds = [...new Set(entries.map((e) => e.artist_id))]; function addArtist(artistId: string) { const a = allArtists.find((x) => x.id === artistId); if (!a) return; setEntries((prev) => [ ...prev, { key: crypto.randomUUID(), artist_id: a.id, artist_name: a.name, roles: [], since: "", until: "", note: "", pendingRole: { ...DEFAULT_PENDING } }, ]); } function addPeriod(artistId: string) { const ref = entries.find((e) => e.artist_id === artistId); if (!ref) return; setEntries((prev) => [ ...prev, { key: crypto.randomUUID(), artist_id: ref.artist_id, artist_name: ref.artist_name, roles: [], since: "", until: "", note: "", pendingRole: { ...DEFAULT_PENDING } }, ]); } function removeArtist(artistId: string) { setEntries((prev) => prev.filter((e) => e.artist_id !== artistId)); } function removeEntry(key: string) { setEntries((prev) => prev.filter((e) => e.key !== key)); } function updateEntry(key: string, field: K, value: MemberEntry[K]) { setEntries((prev) => prev.map((e) => e.key === key ? { ...e, [field]: value } : e)); } function addRole(key: string) { const entry = entries.find((e) => e.key === key); if (!entry) return; const role = entry.pendingRole.type === "other" ? entry.pendingRole.custom.trim() : entry.pendingRole.type; if (!role) return; setEntries((prev) => prev.map((e) => e.key === key ? { ...e, roles: [...e.roles, role] } : e)); } function removeRole(key: string, idx: number) { setEntries((prev) => prev.map((e) => e.key === key ? { ...e, roles: e.roles.filter((_, i) => i !== idx) } : e)); } const serialized: MemberInput[] = entries.map((e) => ({ artist_id: e.artist_id, role: e.roles.join(", ") || null, since: e.since, until: e.until, note: e.note, })); return (

Edit Band

{ setName(e.target.value); if (!slugManual) setSlug(toSlug(e.target.value)); }} /> {errors.name &&

{errors.name}

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

{errors.slug}

}