view v2 update
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useLocation, matchPath } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { listBands } from "../api/bands";
|
||||
import { api } from "../api/client";
|
||||
import { logout } from "../api/auth";
|
||||
import { getInitials } from "../utils";
|
||||
import type { MemberRead } from "../api/auth";
|
||||
import { usePlayerStore } from "../stores/playerStore";
|
||||
import { useBandStore } from "../stores/bandStore";
|
||||
import { TopBandBar } from "./TopBandBar";
|
||||
|
||||
// ── Icons ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -36,17 +37,6 @@ function IconPlay() {
|
||||
);
|
||||
}
|
||||
|
||||
function IconMembers() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<circle cx="7" cy="6.5" r="2.5" stroke="currentColor" strokeWidth="1.4" />
|
||||
<path d="M1.5 14.5c0-2.761 2.462-4.5 5.5-4.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
|
||||
<circle cx="13" cy="11" r="2" stroke="currentColor" strokeWidth="1.4" />
|
||||
<path d="M16 16c0-1.657-1.343-2.5-3-2.5S10 14.343 10 16" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconSettings() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
@@ -56,17 +46,6 @@ function IconSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
function IconStorage() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor">
|
||||
<rect x="1" y="4" width="16" height="3.5" rx="1.5" />
|
||||
<rect x="1" y="10.5" width="16" height="3.5" rx="1.5" />
|
||||
<circle cx="14" cy="5.75" r="0.9" fill="#0c0e1a" />
|
||||
<circle cx="14" cy="12.25" r="0.9" fill="#0c0e1a" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconSignOut() {
|
||||
return (
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
|
||||
@@ -76,14 +55,6 @@ function IconSignOut() {
|
||||
);
|
||||
}
|
||||
|
||||
function IconChevron() {
|
||||
return (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M3 5l3 3 3-3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ── NavItem ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface NavItemProps {
|
||||
@@ -174,20 +145,13 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: bands } = useQuery({ queryKey: ["bands"], queryFn: listBands });
|
||||
const { data: me } = useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: () => api.get<MemberRead>("/auth/me"),
|
||||
});
|
||||
|
||||
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;
|
||||
const { activeBandId } = useBandStore();
|
||||
|
||||
const isLibrary = !!(
|
||||
matchPath({ path: "/bands/:bandId", end: true }, location.pathname) ||
|
||||
@@ -196,23 +160,10 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
const isPlayer = !!matchPath("/bands/:bandId/songs/:songId", location.pathname);
|
||||
const isSettings = location.pathname.startsWith("/settings");
|
||||
const isBandSettings = !!matchPath("/bands/:bandId/settings/*", location.pathname);
|
||||
const bandSettingsPanel = matchPath("/bands/:bandId/settings/:panel", location.pathname)?.params?.panel ?? null;
|
||||
|
||||
const { currentSongId, currentBandId: playerBandId, isPlaying: isPlayerPlaying } = usePlayerStore();
|
||||
const hasActiveSong = !!currentSongId && !!playerBandId;
|
||||
|
||||
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]);
|
||||
|
||||
const sidebarWidth = collapsed ? 68 : 230;
|
||||
const border = "rgba(255,255,255,0.06)";
|
||||
|
||||
@@ -260,93 +211,11 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Band switcher */}
|
||||
<div ref={dropdownRef} style={{ padding: "10px 12px", borderBottom: `1px solid ${border}`, position: "relative", flexShrink: 0 }}>
|
||||
<button
|
||||
onClick={() => setDropdownOpen((o) => !o)}
|
||||
title={collapsed ? (activeBand?.name ?? "Select band") : undefined}
|
||||
style={{
|
||||
width: "100%", display: "flex", alignItems: "center", gap: 8,
|
||||
padding: "7px 8px",
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
border: "1px solid rgba(255,255,255,0.07)",
|
||||
borderRadius: 8, cursor: "pointer", color: "#e8e9f0",
|
||||
textAlign: "left", fontFamily: "inherit",
|
||||
transition: "border-color 0.15s",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.borderColor = "rgba(255,255,255,0.12)")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.borderColor = "rgba(255,255,255,0.07)")}
|
||||
>
|
||||
<div style={{
|
||||
width: 28, height: 28, borderRadius: 8, flexShrink: 0,
|
||||
background: "rgba(139,92,246,0.15)",
|
||||
border: "1px solid rgba(139,92,246,0.3)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: 10, fontWeight: 800, color: "#a78bfa",
|
||||
}}>
|
||||
{activeBand ? getInitials(activeBand.name) : "?"}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span style={{ flex: 1, fontSize: 12, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{activeBand?.name ?? "Select a band"}
|
||||
</span>
|
||||
<span style={{ opacity: 0.3, flexShrink: 0, display: "flex" }}>
|
||||
<IconChevron />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{dropdownOpen && (
|
||||
<div style={{
|
||||
position: "absolute", top: "calc(100% - 2px)",
|
||||
left: 12, right: 12,
|
||||
background: "#1a1e30",
|
||||
border: "1px solid rgba(255,255,255,0.1)",
|
||||
borderRadius: 10, padding: 6, zIndex: 100,
|
||||
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: "7px 9px", marginBottom: 1,
|
||||
background: band.id === activeBandId ? "rgba(139,92,246,0.1)" : "transparent",
|
||||
border: "none", borderRadius: 6, cursor: "pointer",
|
||||
color: "#e8e9f0", textAlign: "left", fontFamily: "inherit",
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 22, height: 22, borderRadius: 6, background: "rgba(139,92,246,0.15)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 9, fontWeight: 700, color: "#a78bfa", flexShrink: 0 }}>
|
||||
{getInitials(band.name)}
|
||||
</div>
|
||||
<span style={{ flex: 1, fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "rgba(232,233,240,0.7)" }}>
|
||||
{band.name}
|
||||
</span>
|
||||
{band.id === activeBandId && <span style={{ fontSize: 10, color: "#a78bfa", flexShrink: 0 }}>✓</span>}
|
||||
</button>
|
||||
))}
|
||||
<div style={{ borderTop: "1px solid rgba(255,255,255,0.06)", marginTop: 4, paddingTop: 4 }}>
|
||||
<button
|
||||
onClick={() => { navigate("/"); setDropdownOpen(false); }}
|
||||
style={{ width: "100%", display: "flex", alignItems: "center", gap: 8, padding: "7px 9px", background: "transparent", border: "none", borderRadius: 6, cursor: "pointer", color: "rgba(232,233,240,0.35)", fontSize: 12, textAlign: "left", fontFamily: "inherit" }}
|
||||
>
|
||||
<span style={{ fontSize: 14, opacity: 0.5 }}>+</span>
|
||||
Create new band
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav style={{ flex: 1, padding: "10px 12px", overflowY: "auto", overflowX: "hidden", display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{activeBand && (
|
||||
{activeBandId && (
|
||||
<>
|
||||
<NavItem icon={<IconLibrary />} label="Library" active={isLibrary} onClick={() => navigate(`/bands/${activeBand.id}`)} collapsed={collapsed} />
|
||||
<NavItem icon={<IconLibrary />} label="Library" active={isLibrary} onClick={() => navigate(`/bands/${activeBandId}`)} collapsed={collapsed} />
|
||||
<NavItem
|
||||
icon={<IconPlay />}
|
||||
label="Now Playing"
|
||||
@@ -358,15 +227,6 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeBand && (
|
||||
<>
|
||||
<div style={{ height: 1, background: border, margin: "10px 0", flexShrink: 0 }} />
|
||||
<NavItem icon={<IconMembers />} label="Members" active={isBandSettings && bandSettingsPanel === "members"} onClick={() => navigate(`/bands/${activeBand.id}/settings/members`)} collapsed={collapsed} />
|
||||
<NavItem icon={<IconStorage />} label="Storage" active={isBandSettings && bandSettingsPanel === "storage"} onClick={() => navigate(`/bands/${activeBand.id}/settings/storage`)} collapsed={collapsed} />
|
||||
<NavItem icon={<IconSettings />} label="Band Settings" active={isBandSettings && bandSettingsPanel === "band"} onClick={() => navigate(`/bands/${activeBand.id}/settings/band`)} collapsed={collapsed} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ height: 1, background: border, margin: "10px 0", flexShrink: 0 }} />
|
||||
<NavItem icon={<IconSettings />} label="Settings" active={isSettings} onClick={() => navigate("/settings")} collapsed={collapsed} />
|
||||
</nav>
|
||||
@@ -409,8 +269,11 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
</aside>
|
||||
|
||||
{/* ── Main content ── */}
|
||||
<main style={{ flex: 1, overflow: "auto", display: "flex", flexDirection: "column", background: "#0c0e1a", minWidth: 0 }}>
|
||||
{children}
|
||||
<main style={{ flex: 1, overflow: "hidden", display: "grid", gridTemplateRows: "44px 1fr", background: "#0c0e1a", minWidth: 0 }}>
|
||||
<TopBandBar />
|
||||
<div style={{ overflow: "auto", display: "flex", flexDirection: "column" }}>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user