156 lines
4.9 KiB
TypeScript
Executable File
156 lines
4.9 KiB
TypeScript
Executable File
import { useState, useRef, useEffect } from "react";
|
|
import { useNavigate, useLocation, matchPath } from "react-router-dom";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { listBands } from "../api/bands";
|
|
import { getInitials } from "../utils";
|
|
|
|
export function TopBar() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
const { data: bands } = useQuery({ queryKey: ["bands"], queryFn: listBands });
|
|
|
|
// Derive active band from URL
|
|
const bandMatch = matchPath("/bands/:bandId/*", location.pathname) ?? matchPath("/bands/:bandId", location.pathname);
|
|
const activeBandId = bandMatch?.params?.bandId ?? null;
|
|
const activeBand = bands?.find((b) => b.id === activeBandId) ?? null;
|
|
|
|
// Close dropdown on outside click
|
|
useEffect(() => {
|
|
if (!dropdownOpen) return;
|
|
function handleClick(e: MouseEvent) {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
setDropdownOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handleClick);
|
|
return () => document.removeEventListener("mousedown", handleClick);
|
|
}, [dropdownOpen]);
|
|
|
|
return (
|
|
<header
|
|
style={{
|
|
position: "fixed",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 50,
|
|
background: "#0b0b0e",
|
|
borderBottom: "1px solid rgba(255,255,255,0.06)",
|
|
zIndex: 1000,
|
|
display: "flex",
|
|
justifyContent: "flex-end",
|
|
padding: "0 16px",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<div ref={dropdownRef} style={{ position: "relative" }}>
|
|
<button
|
|
onClick={() => setDropdownOpen((o) => !o)}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "6px 10px",
|
|
background: "rgba(255,255,255,0.05)",
|
|
border: "1px solid rgba(255,255,255,0.07)",
|
|
borderRadius: 8,
|
|
cursor: "pointer",
|
|
color: "#eeeef2",
|
|
textAlign: "left",
|
|
fontFamily: "inherit",
|
|
fontSize: 13,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 32,
|
|
height: 32,
|
|
background: "rgba(232,162,42,0.15)",
|
|
border: "1px solid rgba(232,162,42,0.3)",
|
|
borderRadius: "50%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: 12,
|
|
fontWeight: 700,
|
|
color: "#e8a22a",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{activeBand ? getInitials(activeBand.name) : "?"}
|
|
</div>
|
|
</button>
|
|
|
|
{dropdownOpen && bands && (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "calc(100% + 4px)",
|
|
right: 0,
|
|
width: 200,
|
|
background: "#18181e",
|
|
border: "1px solid rgba(255,255,255,0.1)",
|
|
borderRadius: 10,
|
|
padding: 6,
|
|
zIndex: 1001,
|
|
boxShadow: "0 8px 24px rgba(0,0,0,0.5)",
|
|
}}
|
|
>
|
|
{bands.map((band) => (
|
|
<button
|
|
key={band.id}
|
|
onClick={() => {
|
|
navigate(`/bands/${band.id}`);
|
|
setDropdownOpen(false);
|
|
}}
|
|
style={{
|
|
width: "100%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "8px 10px",
|
|
marginBottom: 2,
|
|
background: band.id === activeBandId ? "rgba(232,162,42,0.08)" : "transparent",
|
|
border: "none",
|
|
borderRadius: 6,
|
|
cursor: "pointer",
|
|
color: "#eeeef2",
|
|
textAlign: "left",
|
|
fontFamily: "inherit",
|
|
fontSize: 13,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: "50%",
|
|
background: "rgba(232,162,42,0.15)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: 10,
|
|
fontWeight: 700,
|
|
color: "#e8a22a",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{getInitials(band.name)}
|
|
</div>
|
|
<span style={{ flex: 1, fontSize: 13 }}>
|
|
{band.name}
|
|
</span>
|
|
{band.id === activeBandId && (
|
|
<span style={{ fontSize: 12, color: "#e8a22a", flexShrink: 0 }}>✓</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
);
|
|
} |