summaryrefslogtreecommitdiff
path: root/app/routes/band-by-uuid.tsx
diff options
context:
space:
mode:
authoryyamashita <yyamashita@mosquit.one>2026-05-10 22:46:59 +0900
committeryyamashita <yyamashita@mosquit.one>2026-05-10 22:46:59 +0900
commitec2417fc3e7029cb6fa84aa184daac2768ddad85 (patch)
tree3d4b0d8ad3b90d3bb99f9c2932637f756090b57a /app/routes/band-by-uuid.tsx
parent184e6947707ecdf07dfa3a5cbc6e51cf9440e93a (diff)
Separate current/former members with calculable period dates
- Add MemberGroup/BandGroup types and groupBandMembers/groupArtistMembers helpers - Calculate membership duration in months from YYYY-MM since/until values - Band view splits members into 在籍中 / 元メンバー sections with duration label - Artist view splits bands into 在籍中 / 元在籍 sections with duration label - Change since/until inputs to type="month" for structured data entry 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.tsx97
1 files changed, 52 insertions, 45 deletions
diff --git a/app/routes/band-by-uuid.tsx b/app/routes/band-by-uuid.tsx
index 2335e14..603629e 100644
--- a/app/routes/band-by-uuid.tsx
+++ b/app/routes/band-by-uuid.tsx
@@ -5,9 +5,11 @@ import {
getBandLinks,
getBandMembers,
getBandRevisions,
- type BandMemberRow,
+ groupBandMembers,
+ type MemberGroup,
} from "~/lib/db.server";
import { LINK_TYPE_LABEL } from "~/lib/constants";
+import { formatDuration } from "~/lib/utils";
export async function loader({ params }: LoaderFunctionArgs) {
const band = getBandById(params.uuid!);
@@ -15,7 +17,8 @@ export async function loader({ params }: LoaderFunctionArgs) {
const links = getBandLinks(band.id);
const members = getBandMembers(band.id);
const revisions = getBandRevisions(band.id);
- return { band, links, members, latest: revisions[0] ?? null };
+ const grouped = groupBandMembers(members);
+ return { band, links, grouped, latest: revisions[0] ?? null };
}
const STATUS_LABEL: Record<string, string> = {
@@ -24,18 +27,45 @@ 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}`;
+function periodRange(since: string, until: string): string | null {
+ if (!since && !until) return null;
+ return `${since || "?"} 〜 ${until || "現在"}`;
}
-export default function BandDetail() {
- const { band, links, members, latest } = useLoaderData<typeof loader>();
+function MemberItem({ group }: { group: MemberGroup }) {
+ const roles = [...new Set(group.periods.flatMap((p) =>
+ p.role ? p.role.split(", ").filter(Boolean) : []
+ ))];
+ const hasPeriodInfo = group.periods.some((p) => p.since || p.until || p.note);
+ return (
+ <li>
+ <div className="member-main">
+ <Link to={`/artists/of/${group.artist_id}`}>{group.artist_name}</Link>
+ {roles.map((r, i) => <span key={i} className="badge">{r}</span>)}
+ {group.duration_months !== null && (
+ <span className="muted">{formatDuration(group.duration_months)}</span>
+ )}
+ </div>
+ {hasPeriodInfo && (
+ <ul className="period-list">
+ {group.periods.map((p) => {
+ const range = periodRange(p.since, p.until);
+ if (!range && !p.note) return null;
+ return (
+ <li key={p.id} className="period-item">
+ {range && <span className="period-range">{range}</span>}
+ {p.note && <span className="period-note">{p.note}</span>}
+ </li>
+ );
+ })}
+ </ul>
+ )}
+ </li>
+ );
+}
- // group members by artist_id for display
- const artistIds = [...new Set(members.map((m) => m.artist_id))];
+export default function BandDetail() {
+ const { band, links, grouped, latest } = useLoaderData<typeof loader>();
return (
<main>
@@ -56,43 +86,20 @@ export default function BandDetail() {
</div>
</div>
- {artistIds.length > 0 && (
+ {grouped.current.length > 0 && (
<section>
<h2>メンバー</h2>
<ul className="member-list">
- {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>
- );
- })}
+ {grouped.current.map((g) => <MemberItem key={g.artist_id} group={g} />)}
+ </ul>
+ </section>
+ )}
+
+ {grouped.former.length > 0 && (
+ <section>
+ <h2>元メンバー</h2>
+ <ul className="member-list former">
+ {grouped.former.map((g) => <MemberItem key={g.artist_id} group={g} />)}
</ul>
</section>
)}