diff options
| -rw-r--r-- | app/lib/db.server.ts | 25 | ||||
| -rw-r--r-- | app/routes/band-by-uuid.tsx | 14 | ||||
| -rw-r--r-- | app/routes/band-edit.tsx | 27 | ||||
| -rw-r--r-- | app/routes/band-new.tsx | 26 | ||||
| -rw-r--r-- | app/routes/home.tsx | 48 |
5 files changed, 112 insertions, 28 deletions
diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts index dd9184d..bf63e70 100644 --- a/app/lib/db.server.ts +++ b/app/lib/db.server.ts @@ -74,6 +74,13 @@ function initSchema(db: Database.Database) { ); CREATE INDEX IF NOT EXISTS idx_band_links_band_id ON band_links(band_id); + `); + + // migrations + try { db.exec("ALTER TABLE bands ADD COLUMN description TEXT"); } catch { /* already exists */ } + try { db.exec("ALTER TABLE bands ADD COLUMN status TEXT NOT NULL DEFAULT 'active'"); } catch { /* already exists */ } + + db.exec(` CREATE INDEX IF NOT EXISTS idx_artist_links_artist_id ON artist_links(artist_id); CREATE INDEX IF NOT EXISTS idx_band_artists_band_id ON band_artists(band_id); CREATE INDEX IF NOT EXISTS idx_band_artists_artist_id ON band_artists(artist_id); @@ -87,6 +94,8 @@ export interface Band { slug: string; name: string; area: string | null; + description: string | null; + status: string; created_at: string; } @@ -208,6 +217,8 @@ export interface CreateBandInput { slug: string; name: string; area: string | null; + description: string | null; + status: string; links: { label: string; url: string }[]; artists: { id: string; role: string | null }[]; message: string; @@ -217,8 +228,8 @@ export interface CreateBandInput { export function createBand(input: CreateBandInput): Band { const db = getDb(); return db.transaction(() => { - db.prepare("INSERT INTO bands (id, slug, name, area) VALUES (?, ?, ?, ?)").run( - input.id, input.slug, input.name, input.area + db.prepare("INSERT INTO bands (id, slug, name, area, description, status) VALUES (?, ?, ?, ?, ?, ?)").run( + input.id, input.slug, input.name, input.area, input.description, input.status ); input.links.forEach((link, i) => { db.prepare( @@ -236,6 +247,8 @@ export function createBand(input: CreateBandInput): Band { const snapshot = JSON.stringify({ name: band.name, area: band.area, + description: band.description, + status: band.status, links: links.map((l) => ({ label: l.label, url: l.url })), artists: artists.map((a) => ({ id: a.artist_id, name: a.artist_name, role: a.role })), }); @@ -250,6 +263,8 @@ export interface UpdateBandInput { slug: string; name: string; area: string | null; + description: string | null; + status: string; links: { label: string; url: string }[]; artists: { id: string; role: string | null }[]; message: string; @@ -259,8 +274,8 @@ export interface UpdateBandInput { export function updateBand(id: string, input: UpdateBandInput): void { const db = getDb(); db.transaction(() => { - db.prepare("UPDATE bands SET slug = ?, name = ?, area = ? WHERE id = ?").run( - input.slug, input.name, input.area, id + db.prepare("UPDATE bands SET slug = ?, name = ?, area = ?, description = ?, status = ? WHERE id = ?").run( + input.slug, input.name, input.area, input.description, input.status, id ); db.prepare("DELETE FROM band_links WHERE band_id = ?").run(id); input.links.forEach((link, i) => { @@ -280,6 +295,8 @@ export function updateBand(id: string, input: UpdateBandInput): void { const snapshot = JSON.stringify({ name: band.name, area: band.area, + description: band.description, + status: band.status, links: links.map((l) => ({ label: l.label, url: l.url })), artists: artists.map((a) => ({ id: a.artist_id, name: a.artist_name, role: a.role })), }); diff --git a/app/routes/band-by-uuid.tsx b/app/routes/band-by-uuid.tsx index c55472e..1945a57 100644 --- a/app/routes/band-by-uuid.tsx +++ b/app/routes/band-by-uuid.tsx @@ -16,6 +16,12 @@ export async function loader({ params }: LoaderFunctionArgs) { return { band, links, artists, latest: revisions[0] ?? null }; } +const STATUS_LABEL: Record<string, string> = { + active: "活動中", + hiatus: "活動休止", + disbanded: "解散", +}; + export default function BandDetail() { const { band, links, artists, latest } = useLoaderData<typeof loader>(); return ( @@ -23,7 +29,13 @@ export default function BandDetail() { <div className="flex items-start justify-between mb-6"> <div> <h1 className="text-2xl font-bold">{band.name}</h1> - {band.area && <p className="text-gray-400 mt-1 text-sm">{band.area}</p>} + <div className="flex items-center gap-3 mt-1 text-sm text-gray-400"> + {band.area && <span>{band.area}</span>} + <span>{STATUS_LABEL[band.status] ?? band.status}</span> + </div> + {band.description && ( + <p className="mt-3 text-gray-300 text-sm leading-relaxed">{band.description}</p> + )} </div> <div className="flex items-center gap-3 text-sm shrink-0 ml-4"> <Link diff --git a/app/routes/band-edit.tsx b/app/routes/band-edit.tsx index 70e1d60..99d4ee7 100644 --- a/app/routes/band-edit.tsx +++ b/app/routes/band-edit.tsx @@ -27,6 +27,8 @@ export async function action({ params, request }: ActionFunctionArgs) { 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) || "[]" @@ -42,7 +44,7 @@ export async function action({ params, request }: ActionFunctionArgs) { if (Object.keys(errors).length > 0) return { errors }; try { - updateBand(band.id, { slug, name, area, links, artists, message, ip_address: getIpAddress(request) }); + updateBand(band.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は既に使用されています" } }; @@ -130,6 +132,29 @@ export default function BandEdit() { </div> <div> + <label className="block text-sm font-medium text-gray-300 mb-1">ステータス</label> + <select + name="status" + defaultValue={band.status} + className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500" + > + <option value="active">活動中</option> + <option value="hiatus">活動休止</option> + <option value="disbanded">解散</option> + </select> + </div> + + <div> + <label className="block text-sm font-medium text-gray-300 mb-1">説明</label> + <textarea + name="description" + rows={3} + defaultValue={band.description ?? ""} + 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 resize-none" + /> + </div> + + <div> <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label> <div className="space-y-2"> {links.map((link, i) => ( diff --git a/app/routes/band-new.tsx b/app/routes/band-new.tsx index b0325a3..b4d49e2 100644 --- a/app/routes/band-new.tsx +++ b/app/routes/band-new.tsx @@ -12,6 +12,8 @@ export async function action({ request }: ActionFunctionArgs) { 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) || "[]" @@ -28,7 +30,7 @@ export async function action({ request }: ActionFunctionArgs) { const id = crypto.randomUUID(); try { - createBand({ id, slug, name, area, links, artists, message, ip_address: getIpAddress(request) }); + 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は既に使用されています" } }; @@ -108,6 +110,28 @@ export default function BandNew() { </div> <div> + <label className="block text-sm font-medium text-gray-300 mb-1">ステータス</label> + <select + name="status" + defaultValue="active" + className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500" + > + <option value="active">活動中</option> + <option value="hiatus">活動休止</option> + <option value="disbanded">解散</option> + </select> + </div> + + <div> + <label className="block text-sm font-medium text-gray-300 mb-1">説明</label> + <textarea + name="description" + rows={3} + 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 resize-none" + /> + </div> + + <div> <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label> <div className="space-y-2"> {links.map((link, i) => ( diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 3df4ab5..5600645 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -5,38 +5,44 @@ export function loader() { return { bands: listBands() }; } +const STATUS_LABEL: Record<string, string> = { + active: "活動中", + hiatus: "活動休止", + disbanded: "解散", +}; + export default function Home() { const { bands } = useLoaderData<typeof loader>(); return ( - <main className="max-w-3xl mx-auto px-4 py-8"> - <div className="flex items-center justify-between mb-6"> - <h1 className="text-xl font-semibold">Bands</h1> - <Link to="/bands/new" className="text-sm text-blue-400 hover:text-blue-300"> - + New Band - </Link> - </div> + <main className="max-w-2xl mx-auto px-6 py-12"> {bands.length === 0 ? ( - <p className="text-gray-400"> - まだバンドがありません。{" "} - <Link to="/bands/new" className="text-blue-400 hover:text-blue-300"> + <p className="text-gray-600 text-sm"> + バンドがまだありません。{" "} + <Link to="/bands/new" className="underline"> 追加する </Link> </p> ) : ( - <ul className="divide-y divide-gray-800"> + <ul className="divide-y divide-gray-900"> {bands.map((band) => ( - <li key={band.id} className="py-3"> - <Link - to={`/bands/of/${band.id}`} - className="flex items-baseline gap-3 group" - > - <span className="font-medium group-hover:text-blue-300 transition-colors"> + <li key={band.id} className="py-4 flex items-start justify-between gap-6"> + <div className="min-w-0"> + <Link + to={`/bands/of/${band.id}`} + className="text-gray-100 hover:text-white font-medium" + > {band.name} - </span> - {band.area && ( - <span className="text-gray-400 text-sm">{band.area}</span> + </Link> + {band.description && ( + <p className="text-gray-500 text-sm mt-0.5 line-clamp-1"> + {band.description} + </p> )} - </Link> + </div> + <div className="flex items-center gap-4 text-sm text-gray-500 shrink-0 pt-0.5"> + {band.area && <span>{band.area}</span>} + <span>{STATUS_LABEL[band.status] ?? band.status}</span> + </div> </li> ))} </ul> |
