import { useState } from "react"; import { useParams, Link } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getBand } from "../api/bands"; import { api } from "../api/client"; interface SongSummary { id: string; title: string; status: string; version_count: number; } interface BandMember { id: string; display_name: string; email: string; role: string; joined_at: string; } interface BandInvite { id: string; token: string; role: string; expires_at: string; } export function BandPage() { const { bandId } = useParams<{ bandId: string }>(); const qc = useQueryClient(); const [showCreate, setShowCreate] = useState(false); const [title, setTitle] = useState(""); const [error, setError] = useState(null); const [scanMsg, setScanMsg] = useState(null); const [inviteLink, setInviteLink] = useState(null); const [editingFolder, setEditingFolder] = useState(false); const [folderInput, setFolderInput] = useState(""); const { data: band, isLoading } = useQuery({ queryKey: ["band", bandId], queryFn: () => getBand(bandId!), enabled: !!bandId, }); const { data: songs } = useQuery({ queryKey: ["songs", bandId], queryFn: () => api.get(`/bands/${bandId}/songs`), enabled: !!bandId, }); const { data: members } = useQuery({ queryKey: ["members", bandId], queryFn: () => api.get(`/bands/${bandId}/members`), enabled: !!bandId, }); const createMutation = useMutation({ mutationFn: () => api.post(`/bands/${bandId}/songs`, { title }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["songs", bandId] }); setShowCreate(false); setTitle(""); setError(null); }, onError: (err) => setError(err instanceof Error ? err.message : "Failed to create song"), }); const scanMutation = useMutation({ mutationFn: () => api.post(`/bands/${bandId}/nc-scan`, {}), onSuccess: (imported) => { qc.invalidateQueries({ queryKey: ["songs", bandId] }); setScanMsg( imported.length > 0 ? `Imported ${imported.length} new song${imported.length !== 1 ? "s" : ""} from Nextcloud.` : "No new audio files found in Nextcloud." ); setTimeout(() => setScanMsg(null), 4000); }, onError: (err) => setScanMsg(err instanceof Error ? err.message : "Scan failed"), }); const inviteMutation = useMutation({ mutationFn: () => api.post(`/bands/${bandId}/invites`, {}), onSuccess: (invite) => { const url = `${window.location.origin}/invite/${invite.token}`; setInviteLink(url); navigator.clipboard.writeText(url).catch(() => {}); }, }); const removeMemberMutation = useMutation({ mutationFn: (memberId: string) => api.delete(`/bands/${bandId}/members/${memberId}`), onSuccess: () => qc.invalidateQueries({ queryKey: ["members", bandId] }), }); const updateFolderMutation = useMutation({ mutationFn: (nc_folder_path: string) => api.patch(`/bands/${bandId}`, { nc_folder_path }), onSuccess: () => { qc.invalidateQueries({ queryKey: ["band", bandId] }); setEditingFolder(false); }, }); const amAdmin = members?.some((m) => m.role === "admin") ?? false; if (isLoading) return
Loading...
; if (!band) return
Band not found
; return (
← All Bands {/* ── Band header ── */}

{band.name}

{band.genre_tags.length > 0 && (
{band.genre_tags.map((t: string) => ( {t} ))}
)}
{/* ── Nextcloud folder ── */}
NEXTCLOUD SCAN FOLDER
{band.nc_folder_path ?? `bands/${band.slug}/`}
{amAdmin && !editingFolder && ( )}
{editingFolder && (
setFolderInput(e.target.value)} placeholder={`bands/${band.slug}/`} style={{ width: "100%", padding: "7px 10px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, fontFamily: "monospace", boxSizing: "border-box" }} />
)}
{/* ── Members ── */}

Members

{inviteLink && (

Invite link (copied to clipboard, valid 72h):

{inviteLink}
)}
{members?.map((m) => (
{m.display_name} {m.email}
{m.role} {amAdmin && m.role !== "admin" && ( )}
))}
{/* ── Songs ── */}

Songs

{scanMsg && (
{scanMsg}
)} {showCreate && (
{error &&

{error}

} setTitle(e.target.value)} onKeyDown={(e) => e.key === "Enter" && title && createMutation.mutate()} style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", marginBottom: 12, fontSize: 14, boxSizing: "border-box" }} autoFocus />

A folder bands/{band.slug}/songs/{title.toLowerCase().replace(/\s+/g, "-") || "…"}/ will be created in Nextcloud.

)}
{songs?.map((song) => ( {song.title} {song.status} {song.version_count} version{song.version_count !== 1 ? "s" : ""} ))} {songs?.length === 0 && (

No songs yet. Create one or scan Nextcloud to import from {band.nc_folder_path ?? `bands/${band.slug}/`}.

)}
); }