From ec2417fc3e7029cb6fa84aa184daac2768ddad85 Mon Sep 17 00:00:00 2001
From: yyamashita <yyamashita@mosquit.one>
Date: Sun, 10 May 2026 22:46:59 +0900
Subject: Separate current/former members with calculable period dates
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 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>
---
 app/routes/artist-by-uuid.tsx | 98 ++++++++++++++++++++++++++-----------------
 1 file changed, 59 insertions(+), 39 deletions(-)

(limited to 'app/routes/artist-by-uuid.tsx')

diff --git a/app/routes/artist-by-uuid.tsx b/app/routes/artist-by-uuid.tsx
index a65525b..7ddf318 100644
--- a/app/routes/artist-by-uuid.tsx
+++ b/app/routes/artist-by-uuid.tsx
@@ -1,6 +1,14 @@
 import { data, Link, useLoaderData } from "react-router";
 import type { LoaderFunctionArgs } from "react-router";
-import { getArtistById, getArtistLinks, getArtistMembers, getArtistRevisions, type ArtistMemberRow } from "~/lib/db.server";
+import {
+  getArtistById,
+  getArtistLinks,
+  getArtistMembers,
+  getArtistRevisions,
+  groupArtistMembers,
+  type BandGroup,
+} from "~/lib/db.server";
+import { formatDuration } from "~/lib/utils";
 
 export async function loader({ params }: LoaderFunctionArgs) {
   const artist = getArtistById(params.uuid!);
@@ -8,21 +16,49 @@ export async function loader({ params }: LoaderFunctionArgs) {
   const links = getArtistLinks(artist.id);
   const memberships = getArtistMembers(artist.id);
   const revisions = getArtistRevisions(artist.id);
-  return { artist, links, memberships, latest: revisions[0] ?? null };
+  const grouped = groupArtistMembers(memberships);
+  return { artist, links, grouped, latest: revisions[0] ?? null };
 }
 
-function periodLabel(m: ArtistMemberRow): 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 ArtistDetail() {
-  const { artist, links, memberships, latest } = useLoaderData<typeof loader>();
+function BandItem({ group }: { group: BandGroup }) {
+  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={`/bands/of/${group.band_id}`}>{group.band_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 by band_id
-  const bandIds = [...new Set(memberships.map((m) => m.band_id))];
+export default function ArtistDetail() {
+  const { artist, links, grouped, latest } = useLoaderData<typeof loader>();
 
   return (
     <main>
@@ -34,36 +70,20 @@ export default function ArtistDetail() {
         </div>
       </div>
 
-      {bandIds.length > 0 && (
+      {grouped.current.length > 0 && (
         <section>
-          <h2>バンド</h2>
+          <h2>在籍中のバンド</h2>
           <ul className="member-list">
-            {bandIds.map((bandId) => {
-              const group = memberships.filter((m) => m.band_id === bandId);
-              const first = group[0];
-              return (
-                <li key={bandId}>
-                  <div className="member-main">
-                    <Link to={`/bands/of/${bandId}`}>{first.band_name}</Link>
-                    {first.role && <span className="muted">{first.role}</span>}
-                  </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) => <BandItem key={g.band_id} group={g} />)}
+          </ul>
+        </section>
+      )}
+
+      {grouped.former.length > 0 && (
+        <section>
+          <h2>元在籍バンド</h2>
+          <ul className="member-list former">
+            {grouped.former.map((g) => <BandItem key={g.band_id} group={g} />)}
           </ul>
         </section>
       )}
-- 
cgit v1.2.3

