diff options
| author | yyamashita <yyamashita@mosquit.one> | 2026-05-10 00:21:04 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@mosquit.one> | 2026-05-10 00:21:04 +0900 |
| commit | 184e6947707ecdf07dfa3a5cbc6e51cf9440e93a (patch) | |
| tree | 77a75c2225ad7beafecac15ef90d0cc6cfe5871b /app/routes/band-by-uuid.tsx | |
| parent | 0e12e7238f48ffc2a5d35dae059c2f00c7250f3b (diff) | |
Add members table with membership period and note support
Replace band_artists + member_periods with a single members table
(id, band_id, artist_id, role, since, until, note, order_index).
Each row represents one membership period, so rejoining artists
get multiple rows. Existing band_artists data is auto-migrated on
startup. Export format bumped to version 3.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/routes/band-by-uuid.tsx')
| -rw-r--r-- | app/routes/band-by-uuid.tsx | 63 |
1 files changed, 50 insertions, 13 deletions
diff --git a/app/routes/band-by-uuid.tsx b/app/routes/band-by-uuid.tsx index 99a8b5c..2335e14 100644 --- a/app/routes/band-by-uuid.tsx +++ b/app/routes/band-by-uuid.tsx @@ -3,8 +3,9 @@ import type { LoaderFunctionArgs } from "react-router"; import { getBandById, getBandLinks, - getBandArtists, + getBandMembers, getBandRevisions, + type BandMemberRow, } from "~/lib/db.server"; import { LINK_TYPE_LABEL } from "~/lib/constants"; @@ -12,9 +13,9 @@ 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 artists = getBandArtists(band.id); + const members = getBandMembers(band.id); const revisions = getBandRevisions(band.id); - return { band, links, artists, latest: revisions[0] ?? null }; + return { band, links, members, latest: revisions[0] ?? null }; } const STATUS_LABEL: Record<string, string> = { @@ -23,8 +24,19 @@ const STATUS_LABEL: Record<string, string> = { disbanded: "解散", }; +function periodLabel(m: BandMemberRow): string | null { + if (!m.since && !m.until) return null; + const from = m.since || "?"; + const to = m.until || "現在"; + return `${from} 〜 ${to}`; +} + export default function BandDetail() { - const { band, links, artists, latest } = useLoaderData<typeof loader>(); + const { band, links, members, latest } = useLoaderData<typeof loader>(); + + // group members by artist_id for display + const artistIds = [...new Set(members.map((m) => m.artist_id))]; + return ( <main> <div className="detail-header"> @@ -44,18 +56,43 @@ export default function BandDetail() { </div> </div> - {artists.length > 0 && ( + {artistIds.length > 0 && ( <section> <h2>メンバー</h2> <ul className="member-list"> - {artists.map((a) => ( - <li key={a.artist_id}> - <Link to={`/artists/of/${a.artist_id}`}>{a.artist_name}</Link> - {a.role && a.role.split(", ").filter(Boolean).map((r, i) => ( - <span key={i} className="badge">{r}</span> - ))} - </li> - ))} + {artistIds.map((artistId) => { + const group = members.filter((m) => m.artist_id === artistId); + const first = group[0]; + return ( + <li key={artistId}> + <div className="member-main"> + <Link to={`/artists/of/${artistId}`}>{first.artist_name}</Link> + {group.flatMap((m) => + m.role ? m.role.split(", ").filter(Boolean).map((r, i) => ( + <span key={`${m.id}-${i}`} className="badge">{r}</span> + )) : [] + ).filter((_, i, arr) => { + // deduplicate role badges across periods (keep unique labels) + return true; + })} + </div> + {group.some((m) => m.since || m.until || m.note) && ( + <ul className="period-list"> + {group.map((m) => { + const label = periodLabel(m); + if (!label && !m.note) return null; + return ( + <li key={m.id} className="period-item"> + {label && <span className="period-range">{label}</span>} + {m.note && <span className="period-note">{m.note}</span>} + </li> + ); + })} + </ul> + )} + </li> + ); + })} </ul> </section> )} |
