WIP: Mobile optimizations - responsive layout with bottom nav
This commit is contained in:
130
web/src/components/BottomNavBar.tsx
Normal file
130
web/src/components/BottomNavBar.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { useNavigate, useLocation, matchPath } from "react-router-dom";
|
||||
|
||||
// ── Icons (inline SVG) ──────────────────────────────────────────────────────
|
||||
function IconLibrary() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 14 14" fill="currentColor">
|
||||
<path d="M2 3.5h10v1.5H2zm0 3h10v1.5H2zm0 3h7v1.5H2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconPlay() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 14 14" fill="currentColor">
|
||||
<path d="M3 2l9 5-9 5V2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconSettings() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.3">
|
||||
<circle cx="7" cy="7" r="2" />
|
||||
<path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconMembers() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 14 14" fill="currentColor">
|
||||
<circle cx="5" cy="4.5" r="2" />
|
||||
<path d="M1 12c0-2.2 1.8-3.5 4-3.5s4 1.3 4 3.5H1z" />
|
||||
<circle cx="10.5" cy="4.5" r="1.5" opacity=".6" />
|
||||
<path d="M10.5 8.5c1.4 0 2.5 1 2.5 2.5H9.5" opacity=".6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ── NavItem ─────────────────────────────────────────────────────────────────
|
||||
interface NavItemProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function NavItem({ icon, label, active, onClick, disabled }: NavItemProps) {
|
||||
const color = active ? "#e8a22a" : "rgba(255,255,255,0.5)";
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
padding: "8px 4px",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
color,
|
||||
fontSize: 10,
|
||||
transition: "color 0.12s",
|
||||
fontFamily: "inherit",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<span style={{ fontSize: 10 }}>{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ── BottomNavBar ────────────────────────────────────────────────────────────
|
||||
export function BottomNavBar() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
|
||||
// Derive active states
|
||||
const isLibrary = !!matchPath("/bands/:bandId", location.pathname);
|
||||
const isPlayer = !!matchPath("/bands/:bandId/songs/:songId", location.pathname);
|
||||
const isSettings = location.pathname.startsWith("/settings");
|
||||
|
||||
return (
|
||||
<nav
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
background: "#0b0b0e",
|
||||
borderTop: "1px solid rgba(255,255,255,0.06)",
|
||||
zIndex: 1000,
|
||||
padding: "8px 16px",
|
||||
}}
|
||||
>
|
||||
<NavItem
|
||||
icon={<IconLibrary />}
|
||||
label="Library"
|
||||
active={isLibrary}
|
||||
onClick={() => navigate("/bands")}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<IconPlay />}
|
||||
label="Player"
|
||||
active={isPlayer}
|
||||
onClick={() => {}}
|
||||
disabled={!isPlayer}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<IconMembers />}
|
||||
label="Members"
|
||||
active={false}
|
||||
onClick={() => navigate("/settings")}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<IconSettings />}
|
||||
label="Settings"
|
||||
active={isSettings}
|
||||
onClick={() => navigate("/settings")}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user