@@ -1,4 +1,4 @@
import { useState } from "react" ;
import { useState , useMemo } from "react" ;
import { useParams , Link } from "react-router-dom" ;
import { useQuery , useMutation , useQueryClient } from "@tanstack/react-query" ;
import { getBand } from "../api/bands" ;
@@ -21,34 +21,37 @@ interface SessionSummary {
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 ) ;
const d = new Date ( iso . slice ( 0 , 10 ) + "T12:00:00" );
return d . toLocaleDateString ( undefined , { year : "numeric" , month : "short" , day : "numeric" } ) ;
}
function weekday ( iso : string ) : string {
return new Date ( iso ) .toLocaleDateString ( undefined , { weekday : "short" } ) ;
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 qc = useQueryClient ( ) ;
const [ tab , setTab ] = useState < "dates" | "search" > ( "dates" ) ;
const [ showCreate , setShowCreate ] = useState ( false ) ;
const [ t itle, setTitle ] = useState ( "" ) ;
const [ newT itle, setNew Title ] = useState ( "" ) ;
const [ error , setError ] = useState < string | null > ( null ) ;
const [ scanning , setScanning ] = useState ( false ) ;
const [ scanProgress , setScanProgress ] = useState < string | null > ( null ) ;
const [ scanMsg , setScanMsg ] = useState < string | null > ( null ) ;
// Search state
const [ searchQ , setSearchQ ] = useState ( "" ) ;
const [ searchKey , setSearchKey ] = useState ( "" ) ;
const [ searchBpmMin , setSearchBpmMin ] = useState ( "" ) ;
const [ searchBpmMax , setSearchBpmMax ] = useState ( "" ) ;
const [ searchTagInput , setSearchTagInput ] = useState ( "" ) ;
const [ searchTags , setSearchTags ] = useState < string [ ] > ( [ ] ) ;
const [ searchDirty , setSearchDirty ] = useState ( false ) ;
const [ librarySearch , setLibrarySearch ] = useState ( "" ) ;
const [ activePill , setActivePill ] = useState < FilterPill > ( "all" ) ;
const { data : band , isLoading } = useQuery ( {
queryKey : [ "band" , bandId ] ,
@@ -59,35 +62,41 @@ export function BandPage() {
const { data : sessions } = useQuery ( {
queryKey : [ "sessions" , bandId ] ,
queryFn : ( ) = > api . get < SessionSummary [ ] > ( ` /bands/ ${ bandId } /sessions ` ) ,
enabled : ! ! bandId && tab === "dates" ,
enabled : ! ! bandId ,
} ) ;
const { data : unattributedSongs } = useQuery ( {
queryKey : [ "songs-unattributed" , bandId ] ,
queryFn : ( ) = > api . get < SongSummary [ ] > ( ` /bands/ ${ bandId } /songs/search?unattributed=true ` ) ,
enabled : ! ! bandId && tab === "dates" ,
enabled : ! ! bandId ,
} ) ;
// Search results — only fetch when user has triggered a search
const searchParams = new URLSearchParams ( ) ;
if ( s earchQ ) searchParams . set ( "q" , searchQ ) ;
if ( searchKey ) searchParams . set ( "key" , searchKey ) ;
if ( searchBpmMin ) searchParams . set ( "bpm_min" , searchBpmMin ) ;
if ( searchBpmMax ) searchParams . set ( "bpm_max" , searchBpmMax ) ;
searchTags . forEach ( ( t ) = > searchParams . append ( "tags" , t ) ) ;
const { data : searchResults , isFetching : searchFetching } = useQuery ( {
queryKey : [ "songs-search" , bandId , searchParams . toString ( ) ] ,
queryFn : ( ) = > api . get < SongSummary [ ] > ( ` /bands/ ${ bandId } /songs/search? ${ searchParams } ` ) ,
enabled : ! ! bandId && tab === "search" && searchDirty ,
const filteredSessions = useMemo ( ( ) = > {
return ( sessions ? ? [ ] ) . filter ( ( s ) = > {
if ( ! libraryS earch) return true ;
const haystack = [ s. label ? ? "" , s. date , formatDate ( s . date ) ] . join ( " " ) . toLowerCase ( ) ;
return haystack . includes ( librarySearch . toLowerCa se ( ) ) ;
} ) ;
} , [ 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 ] ) ;
const createMutation = useMutation ( {
mutationFn : ( ) = > api . post ( ` /bands/ ${ bandId } /songs ` , { title } ) ,
mutationFn : ( ) = > api . post ( ` /bands/ ${ bandId } /songs ` , { title : newTitle } ) ,
onSuccess : ( ) = > {
qc . invalidateQueries ( { queryKey : [ "sessions" , bandId ] } ) ;
setShowCreate ( false ) ;
setTitle ( "" ) ;
setNew Title ( "" ) ;
setError ( null ) ;
} ,
onError : ( err ) = > setError ( err instanceof Error ? err . message : "Failed to create song" ) ,
@@ -152,46 +161,53 @@ export function BandPage() {
}
}
function addTag() {
const t = searchTagInput . trim ( ) ;
if ( t && ! searchTags . includes ( t ) ) setSearchTags ( ( prev ) = > [ . . . prev , t ] ) ;
setSearchTagInput ( "" ) ;
}
function removeTag ( t : string ) {
setSearchTags ( ( prev ) = > prev . filter ( ( x ) = > x !== t ) ) ;
}
if ( isLoading ) return < div style = { { color : "var(--text-muted)" , padding : 32 } } > Loading . . . < / div > ;
if ( ! band ) return < div style = { { color : "var(--danger)" , padding : 32 } } > Band not found < / div > ;
const hasResults = filteredSessions . length > 0 || filteredUnattributed . length > 0 ;
return (
< div style = { { padding : "20px 32px ", maxWidth : 760 , margin : "0 auto" } } >
{ /* ── Page header ───────────────────────────────────────── */ }
< div style = { { display : "flex" , justifyContent : "space-between" , alignItems : "flex-start" , marginBottom : 24 } } >
< div >
< h1 style = { { color : "#eeeef2" , fontSize : 17 , fontWeight : 500 , margin : "0 0 4px" } } > { band . name } < / h1 >
{ band . genre_tags . length > 0 && (
< div style = { { display : "flex ", gap : 4 , marginTop : 6 } } >
{ band . genre_tags . map ( ( t : string ) = > (
< span
key = { t }
style = { {
background : "rgba(140,90,220,0.1)" ,
color : "#a878e8" ,
fontSize : 10 ,
padding : "1px 7px" ,
borderRadius : 12 ,
} }
< div style = { { display : "flex" , flexDirection : "column" , height : "100% ", maxWidth : 760 , margin : "0 auto" } } >
{ /* ── Header ─────────────────────────────────────────────── */ }
< div style = { { padding : "18px 26px 0" , flexShrink : 0 , borderBottom : "1px solid rgba(255,255,255,0.06)" } } >
{ /* Title row + search + actions */ }
< div style = { { display : "flex" , alignItems : "center" , gap : 10 , marginBottom : 11 } } >
< h1 style = { { fontSize : 17 , fontWeight : 500 , color : "#eeeef2 ", margin : 0 , flexShrink : 0 } } >
Library
< / h1 >
{ /* Search input */ }
< div style = { { position : "relative" , flex : 1 , maxWidth : 280 } } >
< svg
style = { { position : "absolute" , left : 10 , top : "50%" , transform : "translateY(-50%)" , opacity : 0.3 , pointerEvents : "none" , color : "#eeeef2" } }
width = "13" height = "13" viewBox = "0 0 13 13" fill = "none" stroke = "currentColor" strokeWidth = "1.5"
>
{ t }
< /span >
) ) }
< / div >
) }
< circle cx = "5.5" cy = "5.5" r = "3.5" / >
< path d = "M8.5 8.5l3 3" strokeLinecap = "round" / >
< / svg >
< input
value = { librarySearch }
onChange = { ( e ) = > 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)" ) }
/ >
< / div >
< div style = { { display : "flex" , gap : 8 , flexShrink : 0 } } >
< div style = { { marginLeft : "auto" , display : "flex" , gap : 8 , flexShrink : 0 } } >
< button
onClick = { startScan }
disabled = { scanning }
@@ -199,12 +215,11 @@ export function BandPage() {
background : "none" ,
border : "1px solid rgba(255,255,255,0.09)" ,
borderRadius : 6 ,
color : "#4dba85" ,
color : scanning ? "rgba(255,255,255,0.28)" : "#4dba85" ,
cursor : scanning ? "default" : "pointer" ,
padding : "6 px 14 px" ,
padding : "5 px 12 px" ,
fontSize : 12 ,
fontFamily : "inherit" ,
opacity : scanning ? 0.6 : 1 ,
} }
>
{ scanning ? "Scanning…" : "⟳ Scan Nextcloud" }
@@ -217,468 +232,220 @@ export function BandPage() {
borderRadius : 6 ,
color : "#e8a22a" ,
cursor : "pointer" ,
padding : "6 px 14 px" ,
padding : "5 px 12 px" ,
fontSize : 12 ,
fontWeight : 600 ,
fontFamily : "inherit" ,
} }
>
+ New Song
+ Upload
< / button >
< / div >
< / div >
{ /* Filter pills */ }
< div style = { { display : "flex" , gap : 5 , flexWrap : "wrap" , paddingBottom : 14 } } >
{ PILLS . map ( ( pill ) = > {
const active = activePill === pill ;
return (
< button
key = { pill }
onClick = { ( ) = > setActivePill ( pill ) }
style = { {
padding : "3px 10px" ,
borderRadius : 20 ,
cursor : "pointer" ,
border : ` 1px solid ${ active ? "rgba(232,162,42,0.28)" : "rgba(255,255,255,0.08)" } ` ,
background : active ? "rgba(232,162,42,0.1)" : "transparent" ,
color : active ? "#e8a22a" : "rgba(255,255,255,0.3)" ,
fontSize : 11 ,
fontFamily : "inherit" ,
transition : "all 0.12s" ,
textTransform : "capitalize" ,
} }
>
{ pill }
< / button >
) ;
} ) }
< / div >
< / div >
{ /* ── Scan feedback ─────────────────────────────────────── */ }
{ scanning && scanProgress && (
< div
style = { {
background : "rgba(255,255,255,0.03)" ,
border : "1px solid rgba(255,255,255,0.07)" ,
borderRadius : 8 ,
color : "rgba(255,255,255,0.42)" ,
fontSize : 12 ,
padding : "8px 14px" ,
marginBottom : 10 ,
fontFamily : "monospace" ,
} }
>
< div style = { { padding : "10px 26px 0" , flexShrink : 0 } } >
< div style = { { background : "rgba(255,255,255,0.03)" , border : "1px solid rgba(255,255,255,0.07)" , borderRadius : 8 , color : "rgba(255,255,255,0.42)" , fontSize : 12 , padding : "8px 14px" , fontFamily : "monospace" } } >
{ scanProgress }
< / div >
< / div >
) }
{ scanMsg && (
< div
style = { {
background : "rgba(61,200,120,0.06)" ,
border : "1px solid rgba(61,200,120,0.25)" ,
borderRadius : 8 ,
color : "#4dba85" ,
fontSize : 12 ,
padding : "8px 14px" ,
marginBottom : 14 ,
} }
>
< div style = { { padding : "10px 26px 0" , flexShrink : 0 } } >
< div style = { { background : "rgba(61,200,120,0.06)" , border : "1px solid rgba(61,200,120,0.25)" , borderRadius : 8 , color : "#4dba85" , fontSize : 12 , padding : "8px 14px" } } >
{ scanMsg }
< / div >
< / div >
) }
{ /* ── New song form ──────── ─────────────────────────────── */ }
{ /* ── New song / upload form ─────────────────────────────── */ }
{ showCreate && (
< div
style = { {
background : "rgba(255,255,255,0.025)" ,
border : "1px solid rgba(255,255,255,0.07)" ,
borderRadius : 8 ,
padding : 20 ,
marginBottom : 18 ,
} }
>
< div style = { { padding : "14px 26px 0" , flexShrink : 0 } } >
< div style = { { background : "rgba(255,255,255,0.025)" , border : "1px solid rgba(255,255,255,0.07)" , borderRadius : 8 , padding : 18 } } >
{ error && < p style = { { color : "#e07070" , fontSize : 13 , marginBottom : 12 } } > { error } < / p > }
< label style = { { display : "block" , color : "rgba(255,255,255,0.28)" , fontSize : 11 , letterSpacing : "0.6px" , textTransform : "uppercase" , marginBottom : 6 } } >
Song title
< / label >
< input
value = { t itle}
onChange = { ( e ) = > setTitle ( e . target . value ) }
onKeyDown = { ( e ) = > e . key === "Enter" && t itle && createMutation . mutate ( ) }
style = { {
width : "100%" ,
padding : "8px 12px" ,
background : "rgba(255,255,255,0.05)" ,
border : "1px solid rgba(255,255,255,0.08)" ,
borderRadius : 7 ,
color : "#eeeef2" ,
marginBottom : 12 ,
fontSize : 14 ,
fontFamily : "inherit" ,
boxSizing : "border-box" ,
outline : "none" ,
} }
value = { newT itle}
onChange = { ( e ) = > setNew Title ( e . target . value ) }
onKeyDown = { ( e ) = > e . key === "Enter" && newT itle && createMutation . mutate ( ) }
style = { { width : "100%" , padding : "8px 12px" , background : "rgba(255,255,255,0.05)" , border : "1px solid rgba(255,255,255,0.08)" , borderRadius : 7 , color : "#eeeef2" , marginBottom : 12 , fontSize : 14 , fontFamily : "inherit" , boxSizing : "border-box" , outline : "none" } }
autoFocus
/ >
< div style = { { display : "flex" , gap : 8 } } >
< button
onClick = { ( ) = > createMutation . mutate ( ) }
disabled = { ! t itle}
style = { {
background : "rgba(232,162,42,0.14)" ,
border : "1px solid rgba(232,162,42,0.28)" ,
borderRadius : 6 ,
color : "#e8a22a" ,
cursor : title ? "pointer" : "default" ,
padding : "7px 18px" ,
fontWeight : 600 ,
fontSize : 13 ,
fontFamily : "inherit" ,
opacity : title ? 1 : 0.4 ,
} }
disabled = { ! newT itle}
style = { { background : "rgba(232,162,42,0.14)" , border : "1px solid rgba(232,162,42,0.28)" , borderRadius : 6 , color : "#e8a22a" , cursor : newTitle ? "pointer" : "default" , padding : "7px 18px" , fontWeight : 600 , fontSize : 13 , fontFamily : "inherit" , opacity : newTitle ? 1 : 0.4 } }
>
Create
< / button >
< button
onClick = { ( ) = > { setShowCreate ( false ) ; setError ( null ) ; } }
style = { {
background : "none" ,
border : "1px solid rgba(255,255,255,0.09)" ,
borderRadius : 6 ,
color : "rgba(255,255,255,0.42)" ,
cursor : "pointer" ,
padding : "7px 18px" ,
fontSize : 13 ,
fontFamily : "inherit" ,
} }
style = { { background : "none" , border : "1px solid rgba(255,255,255,0.09)" , borderRadius : 6 , color : "rgba(255,255,255,0.42)" , cursor : "pointer" , padding : "7px 18px" , fontSize : 13 , fontFamily : "inherit" } }
>
Cancel
< / button >
< / div >
< / div >
< / div >
) }
{ /* ── Tabs ────────────── ────────────────────────────────── */ }
< div style = { { display : "flex" , borderBottom : "1px solid rgba(255,255,255,0.06)" , marginBottom : 18 } } >
{ ( [ "dates" , "search" ] as const ) . map ( ( t ) = > (
< button
key = { t }
onClick = { ( ) = > setTab ( t ) }
style = { {
background : "none" ,
border : "none" ,
borderBottom : ` 2px solid ${ tab === t ? "#e8a22a" : "transparent" } ` ,
color : tab === t ? "#e8a22a" : "rgba(255,255,255,0.35)" ,
cursor : "pointer" ,
padding : "8px 16px" ,
fontSize : 13 ,
fontWeight : tab === t ? 600 : 400 ,
marginBottom : - 1 ,
fontFamily : "inherit" ,
transition : "color 0.12s" ,
} }
>
{ t === "dates" ? "By Date" : "Search" }
< / button >
) ) }
{ /* ── Scrollable content ────────────────────────────────── */ }
< div style = { { flex : 1 , overflowY : "auto" , padding : "4px 26px 26px" } } >
{ /* Sessions — one date group per session */ }
{ filteredSessions . map ( ( s ) = > (
< div key = { s . id } style = { { marginTop : 18 } } >
{ /* Date group header */ }
< div style = { { display : "flex" , alignItems : "center" , gap : 10 , marginBottom : 8 } } >
< span style = { { fontSize : 10 , fontWeight : 500 , color : "rgba(255,255,255,0.32)" , textTransform : "uppercase" , letterSpacing : "0.6px" , whiteSpace : "nowrap" } } >
{ formatDateLabel ( s . date ) } { s . label ? ` — ${ s . label } ` : "" }
< / span >
< div style = { { flex : 1 , height : 1 , background : "rgba(255,255,255,0.05)" } } / >
< span style = { { fontSize : 10 , color : "rgba(255,255,255,0.18)" , whiteSpace : "nowrap" } } >
{ s . recording_count } recording { s . recording_count !== 1 ? "s" : "" }
< / span >
< / div >
{ /* ── By Date tab ───────────────────────────────────────── */ }
{ tab === "dates" && (
< div style = { { display : "grid" , gap : 4 } } >
{ sessions ? . map ( ( s ) = > (
{ /* Session row */ }
< Link
key = { s . id }
to = { ` /bands/ ${ bandId } /sessions/ ${ s . id } ` }
style = { {
background : "rgba(255,255,255,0.02)" ,
border : "1px solid rgba(255,255,255,0.05)" ,
borderRadius : 8 ,
padding : "13px 16px" ,
textDecoration : "none" ,
color : "#eeeef2" ,
display : "flex" ,
justifyContent : "space-between" ,
alignItems : "center" ,
gap : 12 ,
gap : 11 ,
padding : "9px 13px" ,
borderRadius : 8 ,
background : "rgba(255,255,255,0.02)" ,
border : "1px solid rgba(255,255,255,0.04)" ,
textDecoration : "none" ,
cursor : "pointer" ,
transition : "background 0.12s, border-color 0.12s" ,
} }
onMouseEnter = { ( e ) = > {
( e . currentTarget as HTMLElement ) . style . background = "rgba(255,255,255,0.045 )" ;
( e . currentTarget as HTMLElement ) . style . borderColor = "rgba(255,255,255,0.09 )" ;
( 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.05 )" ;
( e . currentTarget as HTMLElement ) . style . borderColor = "rgba(255,255,255,0.04 )" ;
} }
>
< div style = { { display : "flex" , alignItems : "center" , gap : 10 , minWidth : 0 } } >
< span
style = { {
fontFamily : "monospace" ,
color : "rgba(255,255,255,0.28)" ,
fontSize : 10 ,
flexShrink : 0 ,
} }
>
{ weekday ( s . date ) }
{ /* Session name */ }
< span style = { { flex : 1 , fontSize : 13 , color : "#c8c8d0" , fontFamily : "'SF Mono','Fira Code',monospace" , overflow : "hidden" , textOverflow : "ellipsis" , whiteSpace : "nowrap" } } >
{ s . label ? ? formatDate ( s . date ) }
< / span >
< span style = { { fontWeight : 500 , color : "#d8d8e4" } } > { formatDate ( s . date ) } < / span >
{ s . label && (
< span style = { { color : "#4dba85" , fontSize : 12 , overflow : "hidden" , textOverflow : "ellipsis ", whiteSpace : "nowrap" } } >
{ s . label }
< / span >
) }
< / div >
< span
style = { {
color : "rgba(255,255,255,0.28)" ,
fontSize : 12 ,
whiteSpace : "nowrap" ,
flexShrink : 0 ,
} }
>
{ s . recording_count } recording { s . recording_count !== 1 ? "s" : "" }
{ /* Recording count */ }
< span style = { { fontSize : 11 , color : "rgba(255,255,255,0.28) ", whiteSpace : "nowrap" , flexShrink : 0 } } >
{ s . recording_count }
< / span >
< / Link >
< / div >
) ) }
{ sessions ? . length === 0 && ! unattributedSongs ? . length && (
< p style = { { color : "rgba(255,255,255,0.28)" , fontSize : 13 , padding : "8px 0" } } >
No sessions yet . Scan Nextcloud or create a song to get started .
< / p >
) }
{ /* Unattributed songs */ }
{ ! ! unattributedSongs ? . length && (
< div style = { { marginTop : sessions?.length ? 24 : 0 } } >
< div
style = { {
color : "rgba(255,255,255,0.2)" ,
fontSize : 10 ,
fontFamily : "monospace" ,
letterSpacing : 1 ,
textTransform : "uppercase" ,
marginBottom : 8 ,
paddingLeft : 2 ,
} }
>
Unattributed Recordings
{ /* Unattributed recordings */ }
{ filteredUnattributed . length > 0 && (
< div style = { { marginTop : filteredSessions.length > 0 ? 28 : 18 } } >
{ /* Section header */ }
< div style = { { display : "flex" , alignItems : "center" , gap : 10 , marginBottom : 8 } } >
< span style = { { fontSize : 10 , fontWeight : 500 , color : "rgba(255,255,255,0.32)" , textTransform : "uppercase" , letterSpacing : "0.6px" , whiteSpace : "nowrap" } } >
Unattributed
< / span >
< div style = { { flex : 1 , height : 1 , background : "rgba(255,255,255,0.05)" } } / >
< span style = { { fontSize : 10 , color : "rgba(255,255,255,0.18)" , whiteSpace : "nowrap" } } >
{ filteredUnattributed . length } track { filteredUnattributed . length !== 1 ? "s" : "" }
< / span >
< / div >
< div style = { { display : "grid" , gap : 4 } } >
{ unattributedSongs . map ( ( song ) = > (
< div style = { { display : "grid" , gap : 3 } } >
{ filteredUnattributed . map ( ( song ) = > (
< Link
key = { song . id }
to = { ` /bands/ ${ bandId } /songs/ ${ song . id } ` }
style = { {
background : "rgba(255,255,255,0.02)" ,
border : "1px solid rgba(255,255,255,0.05 )" ,
borderRadius : 8 ,
padding : "13px 16px" ,
textDecoration : "none" ,
color : "#eeeef2 " ,
display : "flex " ,
justifyContent : "space-between" ,
alignItems : "center" ,
gap : 12 ,
style = { { display : "flex" , alignItems : "center" , gap : 11 , padding : "9px 13px" , borderRadius : 8 , background : "rgba(255,255,255,0.02)" , border : "1px solid rgba(255,255,255,0.04)" , textDecoration : "none" , transition : "background 0.12s, border-color 0.12s" } }
onMouseEnter = { ( e ) = > {
( 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) ";
} }
>
< div style = { { minWidth : 0 } } >
< div style = { { fontWeight : 500 , marginBottom : 4 , c olor : "#d8d8e4" } } > { song . title } < / div >
< div style = { { flex : 1 , minWidth : 0 } } >
< div style = { { fontSize : 13 , color : "#c8c8d0" , fontFamily : "'SF Mono','Fira Code',monospace" , overf low : "hidden" , textOverflow : "ellipsis" , whiteSpace : "nowrap" , marginBottom : 3 } } >
{ song . title }
< / div >
< div style = { { display : "flex" , gap : 4 , flexWrap : "wrap" } } >
{ song . tags . map ( ( t ) = > (
< span
key = { t }
style = { {
background : "rgba(61,200,120,0.08)" ,
color : "#4dba85" ,
fontSize : 9 ,
padding : "1px 6px" ,
borderRadius : 3 ,
fontFamily : "monospace" ,
} }
>
< span key = { t } style = { { background : "rgba(61,200,120,0.08)" , color : "#4dba85" , fontSize : 9 , padding : "1px 6px" , borderRadius : 3 , fontFamily : "monospace" } } >
{ t }
< / span >
) ) }
< / div >
< / div >
< div style = { { color : "rgba(255,255,255,0.28)" , fontSize : 12 , whiteSpace : "nowrap" , flexShrink : 0 } } >
< span
style = { {
background : "rgba(255,255,255,0.05)" ,
borderRadius : 4 ,
padding : "2px 6px" ,
marginRight : 8 ,
fontFamily : "monospace" ,
fontSize : 10 ,
} }
>
{ song . status }
< / span >
{ song . version_count } version { song . version_count !== 1 ? "s" : "" }
< / div >
< / Link >
) ) }
< / div >
< / div >
) }
< / div >
) }
{ /* ── Search tab ────────────────────────────────────────── */ }
{ tab === "search" && (
< div >
< div
style = { {
background : "rgba(255,255,255,0.025)" ,
border : "1px solid rgba(255,255,255,0.06)" ,
borderRadius : 8 ,
padding : 16 ,
marginBottom : 16 ,
} }
>
< div style = { { display : "grid" , gridTemplateColumns : "1fr 1fr" , gap : 10 , marginBottom : 10 } } >
< div >
< label style = { { display : "block" , color : "rgba(255,255,255,0.28)" , fontSize : 10 , letterSpacing : "0.6px" , textTransform : "uppercase" , marginBottom : 4 } } >
Title
< / label >
< input
value = { searchQ }
onChange = { ( e ) = > setSearchQ ( e . target . value ) }
onKeyDown = { ( e ) = > { if ( e . key === "Enter" ) { setSearchDirty ( true ) ; qc . invalidateQueries ( { queryKey : [ "songs-search" , bandId ] } ) ; } } }
placeholder = "Search by name…"
style = { { width : "100%" , padding : "8px 12px" , background : "rgba(255,255,255,0.05)" , border : "1px solid rgba(255,255,255,0.08)" , borderRadius : 7 , color : "#eeeef2" , fontSize : 13 , fontFamily : "inherit" , boxSizing : "border-box" , outline : "none" } }
/ >
< / div >
< div >
< label style = { { display : "block" , color : "rgba(255,255,255,0.28)" , fontSize : 10 , letterSpacing : "0.6px" , textTransform : "uppercase" , marginBottom : 4 } } >
Key
< / label >
< input
value = { searchKey }
onChange = { ( e ) = > setSearchKey ( e . target . value ) }
placeholder = "e.g. Am, C, F#"
style = { { width : "100%" , padding : "8px 12px" , background : "rgba(255,255,255,0.05)" , border : "1px solid rgba(255,255,255,0.08)" , borderRadius : 7 , color : "#eeeef2" , fontSize : 13 , fontFamily : "inherit" , boxSizing : "border-box" , outline : "none" } }
/ >
< / div >
< div >
< label style = { { display : "block" , color : "rgba(255,255,255,0.28)" , fontSize : 10 , letterSpacing : "0.6px" , textTransform : "uppercase" , marginBottom : 4 } } >
BPM min
< / label >
< input
value = { searchBpmMin }
onChange = { ( e ) = > setSearchBpmMin ( e . target . value ) }
type = "number"
min = { 0 }
placeholder = "e.g. 80"
style = { { width : "100%" , padding : "8px 12px" , background : "rgba(255,255,255,0.05)" , border : "1px solid rgba(255,255,255,0.08)" , borderRadius : 7 , color : "#eeeef2" , fontSize : 13 , fontFamily : "inherit" , boxSizing : "border-box" , outline : "none" } }
/ >
< / div >
< div >
< label style = { { display : "block" , color : "rgba(255,255,255,0.28)" , fontSize : 10 , letterSpacing : "0.6px" , textTransform : "uppercase" , marginBottom : 4 } } >
BPM max
< / label >
< input
value = { searchBpmMax }
onChange = { ( e ) = > setSearchBpmMax ( e . target . value ) }
type = "number"
min = { 0 }
placeholder = "e.g. 140"
style = { { width : "100%" , padding : "8px 12px" , background : "rgba(255,255,255,0.05)" , border : "1px solid rgba(255,255,255,0.08)" , borderRadius : 7 , color : "#eeeef2" , fontSize : 13 , fontFamily : "inherit" , boxSizing : "border-box" , outline : "none" } }
/ >
< / div >
< / div >
< div style = { { marginBottom : 10 } } >
< label style = { { display : "block" , color : "rgba(255,255,255,0.28)" , fontSize : 10 , letterSpacing : "0.6px" , textTransform : "uppercase" , marginBottom : 4 } } >
Tags ( must have all )
< / label >
< div style = { { display : "flex" , gap : 6 , flexWrap : "wrap" , marginBottom : 6 } } >
{ searchTags . map ( ( t ) = > (
< span
key = { t }
style = { {
background : "rgba(61,200,120,0.08)" ,
color : "#4dba85" ,
fontSize : 11 ,
padding : "2px 8px" ,
borderRadius : 12 ,
display : "flex" ,
alignItems : "center" ,
gap : 4 ,
} }
>
{ t }
< button
onClick = { ( ) = > removeTag ( t ) }
style = { { background : "none" , border : "none" , color : "#4dba85" , cursor : "pointer" , fontSize : 12 , padding : 0 , lineHeight : 1 , fontFamily : "inherit" } }
>
×
< / button >
< / span >
) ) }
< / div >
< div style = { { display : "flex" , gap : 6 } } >
< input
value = { searchTagInput }
onChange = { ( e ) = > setSearchTagInput ( e . target . value ) }
onKeyDown = { ( e ) = > e . key === "Enter" && addTag ( ) }
placeholder = "Add tag…"
style = { { flex : 1 , padding : "6px 10px" , background : "rgba(255,255,255,0.05)" , border : "1px solid rgba(255,255,255,0.08)" , borderRadius : 7 , color : "#eeeef2" , fontSize : 12 , fontFamily : "inherit" , outline : "none" } }
/ >
< button
onClick = { addTag }
style = { { background : "none" , border : "1px solid rgba(255,255,255,0.09)" , borderRadius : 6 , color : "rgba(255,255,255,0.42)" , cursor : "pointer" , padding : "6px 10px" , fontSize : 12 , fontFamily : "inherit" } }
>
+
< / button >
< / div >
< / div >
< button
onClick = { ( ) = > { setSearchDirty ( true ) ; qc . invalidateQueries ( { queryKey : [ "songs-search" , bandId ] } ) ; } }
style = { {
background : "rgba(232,162,42,0.14)" ,
border : "1px solid rgba(232,162,42,0.28)" ,
borderRadius : 6 ,
color : "#e8a22a" ,
cursor : "pointer" ,
padding : "7px 18px" ,
fontSize : 13 ,
fontWeight : 600 ,
fontFamily : "inherit" ,
} }
>
Search
< / button >
< / div >
{ searchFetching && < p style = { { color : "rgba(255,255,255,0.28)" , fontSize : 13 } } > Searching … < / p > }
{ ! searchFetching && searchDirty && (
< div style = { { display : "grid" , gap : 6 } } >
{ searchResults ? . map ( ( song ) = > (
< Link
key = { song . id }
to = { ` /bands/ ${ bandId } /songs/ ${ song . id } ` }
style = { {
background : "rgba(255,255,255,0.02)" ,
border : "1px solid rgba(255,255,255,0.05)" ,
borderRadius : 8 ,
padding : "13px 16px" ,
textDecoration : "none" ,
color : "#eeeef2" ,
display : "flex" ,
justifyContent : "space-between" ,
alignItems : "center" ,
gap : 12 ,
} }
>
< div style = { { minWidth : 0 } } >
< div style = { { fontWeight : 500 , marginBottom : 4 , color : "#d8d8e4" } } > { song . title } < / div >
< div style = { { display : "flex" , gap : 4 , flexWrap : "wrap" } } >
{ song . tags . map ( ( t ) = > (
< span key = { t } style = { { background : "rgba(61,200,120,0.08)" , color : "#4dba85" , fontSize : 9 , padding : "1px 6px" , borderRadius : 3 , fontFamily : "monospace" } } > { t } < / span >
) ) }
{ song . global_key && (
< span style = { { background : "rgba(255,255,255,0.05)" , color : "rgba(255,255,255,0.28)" , fontSize : 9 , padding : "1px 6px" , borderRadius : 3 , fontFamily : "monospace" } } > { song . global_key } < / span >
< span style = { { background : "rgba(255,255,255,0.05)" , color : "rgba(255,255,255,0.28)" , fontSize : 9 , padding : "1px 6px" , borderRadius : 3 , fontFamily : "monospace" } } >
{ song . global_key }
< / span >
) }
{ song . global_bpm && (
< span style = { { background : "rgba(255,255,255,0.05)" , color : "rgba(255,255,255,0.28)" , fontSize : 9 , padding : "1px 6px" , borderRadius : 3 , fontFamily : "monospace" } } > { song . global_bpm . toFixed ( 0 ) } BPM < / span >
< span style = { { background : "rgba(255,255,255,0.05)" , color : "rgba(255,255,255,0.28)" , fontSize : 9 , padding : "1px 6px" , borderRadius : 3 , fontFamily : "monospace" } } >
{ song . global_bpm . toFixed ( 0 ) } BPM
< / span >
) }
< / div >
< / div >
< div style = { { color : "rgba(255,255,255,0.28)" , fontSize : 12 , whiteSpace : "nowrap" , flexShrink : 0 } } >
< span style = { { background : "rgba(255,255,255,0.05 )" , borderRadius : 4 , padding : "2px 6px" , marginRight : 8 , fontFamily : "monospace" , fontSize : 1 0 } } > { song . status } < / span >
{ song . version_count } version { song . version_count !== 1 ? "s" : "" }
< / div >
< span style = { { fontSize : 11 , color : "rgba(255,255,255,0.28 )" , whiteSpace : "nowrap" , flexShrink : 0 } } >
{ song . version_count } ver { song . version_count !== 1 ? "s" : "" }
< / span >
< / Link >
) ) }
{ searchResults ? . length === 0 && (
< p style = { { color : "rgba(255,255,255,0.28)" , fontSize : 13 } } > No songs match your filters . < / p >
) }
< / div >
< / div >
) }
{ ! searchDirty && (
< p style = { { color : "rgba(255,255,255,0.28)" , fontSize : 13 } } > Enter filters above and hit Search . < / p >
{ /* Empty state */ }
{ ! hasResults && (
< p style = { { color : "rgba(255,255,255,0.28)" , fontSize : 13 , padding : "24px 0 8px" } } >
{ librarySearch
? "No results match your search."
: "No sessions yet. Scan Nextcloud or create a song to get started." }
< / p >
) }
< / div >
) }
< / div >
) ;
}