import { useState, useMemo } from "react"; import { useParams, Link } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { getBand } from "../api/bands"; import { api } from "../api/client"; interface SongSummary { id: string; title: string; status: string; tags: string[]; global_key: string | null; global_bpm: number | null; version_count: number; } interface SessionSummary { id: string; date: string; label: string | null; recording_count: number; } type FilterPill = "all" | "full band" | "guitar" | "vocals" | "drums" | "keys" | "commented"; const PILLS: FilterPill[] = ["all", "full band", "guitar", "vocals", "drums", "keys", "commented"]; function formatDate(iso: string): string { const d = new Date(iso.slice(0, 10) + "T12:00:00"); return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); } function formatDateLabel(iso: string): string { const d = new Date(iso.slice(0, 10) + "T12:00:00"); const today = new Date(); today.setHours(12, 0, 0, 0); const diffDays = Math.round((today.getTime() - d.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays === 0) { return "Today — " + d.toLocaleDateString(undefined, { month: "short", day: "numeric" }); } return d.toLocaleDateString(undefined, { month: "short", day: "numeric" }); } export function BandPage() { const { bandId } = useParams<{ bandId: string }>(); const [librarySearch, setLibrarySearch] = useState(""); const [activePill, setActivePill] = useState("all"); const { data: band, isLoading } = useQuery({ queryKey: ["band", bandId], queryFn: () => getBand(bandId!), enabled: !!bandId, }); const { data: sessions } = useQuery({ queryKey: ["sessions", bandId], queryFn: () => api.get(`/bands/${bandId}/sessions`), enabled: !!bandId, }); const { data: unattributedSongs } = useQuery({ queryKey: ["songs-unattributed", bandId], queryFn: () => api.get(`/bands/${bandId}/songs/search?unattributed=true`), enabled: !!bandId, }); const filteredSessions = useMemo(() => { return (sessions ?? []).filter((s) => { if (!librarySearch) return true; const haystack = [s.label ?? "", s.date, formatDate(s.date)].join(" ").toLowerCase(); return haystack.includes(librarySearch.toLowerCase()); }); }, [sessions, librarySearch]); const filteredUnattributed = useMemo(() => { return (unattributedSongs ?? []).filter((song) => { const matchesSearch = !librarySearch || song.title.toLowerCase().includes(librarySearch.toLowerCase()); const matchesPill = activePill === "all" || activePill === "commented" || song.tags.some((t) => t.toLowerCase() === activePill); return matchesSearch && matchesPill; }); }, [unattributedSongs, librarySearch, activePill]); if (isLoading) return
Loading...
; if (!band) return
Band not found
; const hasResults = filteredSessions.length > 0 || filteredUnattributed.length > 0; return (
{/* ── Header ─────────────────────────────────────────────── */}
{/* Title row + search + actions */}

Library

{/* Search input */}
setLibrarySearch(e.target.value)} placeholder="Search recordings, comments…" style={{ width: "100%", padding: "7px 12px 7px 30px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.08)", borderRadius: 7, color: "#e2e2e8", fontSize: 13, fontFamily: "inherit", outline: "none", boxSizing: "border-box", }} onFocus={(e) => (e.currentTarget.style.borderColor = "rgba(232,162,42,0.35)")} onBlur={(e) => (e.currentTarget.style.borderColor = "rgba(255,255,255,0.08)")} />
{/* Filter pills */}
{PILLS.map((pill) => { const active = activePill === pill; return ( ); })}
{/* ── Scrollable content ────────────────────────────────── */}
{/* Sessions — one date group per session */} {filteredSessions.map((s) => (
{/* Date group header */}
{formatDateLabel(s.date)}{s.label ? ` — ${s.label}` : ""}
{s.recording_count} recording{s.recording_count !== 1 ? "s" : ""}
{/* Session row */} { (e.currentTarget as HTMLElement).style.background = "rgba(255,255,255,0.048)"; (e.currentTarget as HTMLElement).style.borderColor = "rgba(255,255,255,0.08)"; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = "rgba(255,255,255,0.02)"; (e.currentTarget as HTMLElement).style.borderColor = "rgba(255,255,255,0.04)"; }} > {/* Session name */} {s.label ?? formatDate(s.date)} {/* Recording count */} {s.recording_count}
))} {/* Unattributed recordings */} {filteredUnattributed.length > 0 && (
0 ? 28 : 18 }}> {/* Section header */}
Unattributed
{filteredUnattributed.length} track{filteredUnattributed.length !== 1 ? "s" : ""}
{filteredUnattributed.map((song) => ( { (e.currentTarget as HTMLElement).style.background = "rgba(255,255,255,0.048)"; (e.currentTarget as HTMLElement).style.borderColor = "rgba(255,255,255,0.08)"; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = "rgba(255,255,255,0.02)"; (e.currentTarget as HTMLElement).style.borderColor = "rgba(255,255,255,0.04)"; }} >
{song.title}
{song.tags.map((t) => ( {t} ))} {song.global_key && ( {song.global_key} )} {song.global_bpm && ( {song.global_bpm.toFixed(0)} BPM )}
{song.version_count} ver{song.version_count !== 1 ? "s" : ""} ))}
)} {/* Empty state */} {!hasResults && (

{librarySearch ? "No results match your search." : "No sessions yet. Go to Storage settings to scan your Nextcloud folder."}

)}
); }