feat(mobile): Implement responsive mobile menu with band context preservation

This commit implements a comprehensive mobile menu solution that:

1. **Mobile Menu Components**:
   - Created TopBar.tsx with circular band switcher (mobile only)
   - Enhanced BottomNavBar.tsx with band-context-aware navigation
   - Updated ResponsiveLayout.tsx to integrate TopBar for mobile views

2. **Band Context Preservation**:
   - Fixed black screen issue by preserving band context via React Router state
   - Implemented dual context detection (URL params + location state)
   - Added graceful fallback handling for missing context

3. **Visual Improvements**:
   - Changed band display from square+text to perfect circle with initials only
   - Updated dropdown items to use consistent circular format
   - Improved mobile space utilization

4. **Debugging & Testing**:
   - Added comprehensive debug logging for issue tracking
   - Created test plans and documentation
   - Ensured all static checks pass (TypeScript + ESLint)

5. **Shared Utilities**:
   - Created utils.ts with shared getInitials() function
   - Reduced code duplication across components

Key Features:
- Mobile (<768px): TopBar + BottomNavBar + Main Content
- Desktop (≥768px): Sidebar (unchanged)
- Band context preserved across all mobile navigation
- Graceful error handling and fallbacks
- Comprehensive debug logging (can be removed in production)

Files Changed:
- web/src/utils.ts (new)
- web/src/components/TopBar.tsx (new)
- web/src/components/BottomNavBar.tsx (modified)
- web/src/components/ResponsiveLayout.tsx (modified)
- web/src/components/Sidebar.tsx (modified)

Documentation Added:
- implementation_summary.md
- refinement_summary.md
- black_screen_fix_summary.md
- test_plan_mobile_menu_fix.md
- test_plan_refinement.md
- testing_guide.md
- black_screen_debug.md

Resolves:
- Mobile menu band context loss
- Black screen on Library navigation
- Inconsistent band display format
- Missing mobile band switching capability

Breaking Changes: None
Backward Compatibility: Fully maintained

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mistral Vibe
2026-04-07 13:26:33 +00:00
parent 21c1673fcc
commit 6f0e2636d0
12 changed files with 1497 additions and 17 deletions

View File

@@ -0,0 +1,156 @@
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>
);
}