import { useState } from "react"; import { Form, Link, redirect, useActionData, useLoaderData } from "react-router"; import type { ActionFunctionArgs } from "react-router"; import { createBand, getIpAddress, listArtists } from "~/lib/db.server"; import { ARTIST_ROLES, LINK_TYPES } from "~/lib/constants"; export function loader() { return { artists: listArtists() }; } export async function action({ request }: ActionFunctionArgs) { 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 artists: { id: string; role: string | null }[] = JSON.parse( (fd.get("artists") 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 }; const id = crypto.randomUUID(); try { createBand({ id, slug, name, area, description, status, links, artists, 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/${id}`); } function toSlug(s: string) { return s.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^\w぀-ヿ一-鿿＀-￯-]/g, "").replace(/^-+|-+$/g, ""); } type PickedArtist = { id: string; name: string; roles: string[] }; type PendingRole = { type: string; custom: string }; const DEFAULT_PENDING: PendingRole = { type: ARTIST_ROLES[0], custom: "" }; export default function BandNew() { const { artists } = useLoaderData(); const actionData = useActionData(); const errors = actionData?.errors ?? {}; const [name, setName] = useState(""); const [slug, setSlug] = useState(""); const [slugManual, setSlugManual] = useState(false); const [links, setLinks] = useState<{ label: string; url: string }[]>([]); const [picked, setPicked] = useState([]); const [pending, setPending] = useState>({}); const available = artists.filter((a) => !picked.some((p) => p.id === a.id)); function addRole(artistId: string) { const pend = pending[artistId] ?? DEFAULT_PENDING; const role = pend.type === "other" ? pend.custom.trim() : pend.type; if (!role) return; setPicked((prev) => prev.map((a) => a.id === artistId ? { ...a, roles: [...a.roles, role] } : a)); } function removeRole(artistId: string, idx: number) { setPicked((prev) => prev.map((a) => a.id === artistId ? { ...a, roles: a.roles.filter((_, i) => i !== idx) } : a)); } return (

New Band

({ id: p.id, role: p.roles.join(", ") || null })))} />
{ 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}

}