fix: mobile responsive layout
Deploy to Netlify / build-and-deploy (push) Failing after 15m14s

- Sidebar hidden on mobile, slides in from left as overlay (hamburger)
- Mobile top bar with hamburger, brand, and mini play/pause button
- Mobile bottom navigation bar (icon + label tabs for all 4 views)
- Mobile mini audio strip appears above bottom nav when track is active
- Desktop layout unchanged
- Added aria-labels to all icon-only buttons
This commit is contained in:
wmj
2026-08-23 12:31:10 +08:00
parent 0c2da241aa
commit a701c58ded
+142 -38
View File
@@ -15,7 +15,9 @@ import {
TrendingUp, TrendingUp,
FileText, FileText,
ChevronRight, ChevronRight,
FolderOpen FolderOpen,
Menu,
X,
} from 'lucide-react'; } from 'lucide-react';
/* ========================================================================== /* ==========================================================================
@@ -344,6 +346,7 @@ export default function App() {
const [activeView, setActiveView] = useState('missionControl'); const [activeView, setActiveView] = useState('missionControl');
const [activeAudio, setActiveAudio] = useState(null); const [activeAudio, setActiveAudio] = useState(null);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(false);
const togglePlay = () => setIsPlaying(!isPlaying); const togglePlay = () => setIsPlaying(!isPlaying);
@@ -354,14 +357,66 @@ export default function App() {
{ id: 'wiki', label: 'Loop Wiki', icon: BookOpen }, { id: 'wiki', label: 'Loop Wiki', icon: BookOpen },
]; ];
const handleNavClick = (id) => {
setActiveView(id);
setSidebarOpen(false);
};
const AudioPlayer = () => (
activeAudio ? (
<div className="animate-fade-in-up">
<div className="flex justify-between items-center mb-2">
<span className="text-[10px] font-bold text-emerald-400 uppercase tracking-wider">Now Playing</span>
<span className="text-xs text-slate-500 font-mono">{isPlaying ? '02:14' : '00:00'} / {activeAudio.duration}</span>
</div>
<p className="font-semibold text-sm text-white truncate mb-1" title={activeAudio.title}>
{activeAudio.title}
</p>
<p className="text-xs text-slate-400 mb-3 truncate">{activeAudio.author}</p>
<div className="flex items-center justify-between mt-3 px-2">
<button aria-label="Previous track" className="text-slate-400 hover:text-white transition-colors"><SkipBack size={18} /></button>
<button
onClick={togglePlay}
aria-label={isPlaying ? 'Pause' : 'Play'}
className="text-white bg-emerald-500 hover:bg-emerald-400 rounded-full w-10 h-10 flex items-center justify-center transition-all shadow-lg shadow-emerald-500/20"
>
{isPlaying ? <PauseCircle size={24} className="text-slate-950" /> : <PlayCircle size={24} className="text-slate-950" />}
</button>
<button aria-label="Next track" className="text-slate-400 hover:text-white transition-colors"><SkipForward size={18} /></button>
</div>
<div className="w-full bg-slate-800 h-1 mt-4 rounded-full overflow-hidden">
<div className="bg-emerald-500 h-full w-1/4 rounded-full transition-all duration-1000 ease-linear"></div>
</div>
</div>
) : (
<div className="py-4 text-center">
<Headphones className="mx-auto text-slate-700 mb-2" size={24} />
<p className="text-xs text-slate-500 font-medium">Select an audio brief to begin</p>
</div>
)
);
return ( return (
<div className="flex h-screen bg-slate-950 text-slate-100 font-sans overflow-hidden selection:bg-blue-500/30"> <div className="flex h-screen bg-slate-950 text-slate-100 font-sans overflow-hidden selection:bg-blue-500/30">
{/* Sidebar Layout */} {/* Mobile overlay backdrop */}
<div className="w-72 border-r border-slate-800 flex flex-col bg-slate-900 shadow-2xl relative z-20"> {sidebarOpen && (
<div
className="fixed inset-0 z-30 bg-black/60 md:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar — slide-over on mobile, fixed column on desktop */}
<div className={`
fixed inset-y-0 left-0 z-40 w-72 border-r border-slate-800 flex flex-col bg-slate-900 shadow-2xl
transform transition-transform duration-300 ease-in-out
md:relative md:translate-x-0
${sidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
`}>
{/* Brand */} {/* Brand */}
<div className="p-6"> <div className="p-6 flex items-start justify-between">
<div>
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center"> <div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
<TrendingUp className="text-slate-950" size={20} strokeWidth={2.5} /> <TrendingUp className="text-slate-950" size={20} strokeWidth={2.5} />
@@ -370,9 +425,17 @@ export default function App() {
</div> </div>
<p className="text-[10px] uppercase font-bold tracking-[0.2em] text-slate-500 ml-11">Coach Portal</p> <p className="text-[10px] uppercase font-bold tracking-[0.2em] text-slate-500 ml-11">Coach Portal</p>
</div> </div>
<button
onClick={() => setSidebarOpen(false)}
className="md:hidden text-slate-500 hover:text-white p-1 mt-1"
aria-label="Close menu"
>
<X size={20} />
</button>
</div>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 px-4 mt-6 space-y-2 overflow-y-auto"> <nav className="flex-1 px-4 mt-2 space-y-2 overflow-y-auto">
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 ml-3">Platform</div> <div className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 ml-3">Platform</div>
{navItems.map((item) => { {navItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
@@ -380,7 +443,7 @@ export default function App() {
return ( return (
<button <button
key={item.id} key={item.id}
onClick={() => setActiveView(item.id)} onClick={() => handleNavClick(item.id)}
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition-all duration-200 group ${ className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition-all duration-200 group ${
isActive isActive
? 'bg-blue-600 shadow-md shadow-blue-500/20' ? 'bg-blue-600 shadow-md shadow-blue-500/20'
@@ -393,52 +456,51 @@ export default function App() {
</div> </div>
{isActive && <ChevronRight size={16} className="text-blue-200" />} {isActive && <ChevronRight size={16} className="text-blue-200" />}
</button> </button>
) );
})} })}
</nav> </nav>
{/* Global Audio Player */} {/* Global Audio Player */}
<div className="mt-auto border-t border-slate-800 p-4 bg-slate-950/50 backdrop-blur-md"> <div className="mt-auto border-t border-slate-800 p-4 bg-slate-950/50 backdrop-blur-md">
{activeAudio ? ( <AudioPlayer />
<div className="animate-fade-in-up"> </div>
<div className="flex justify-between items-center mb-2">
<span className="text-[10px] font-bold text-emerald-400 uppercase tracking-wider">Now Playing</span>
<span className="text-xs text-slate-500 font-mono">{isPlaying ? '02:14' : '00:00'} / {activeAudio.duration}</span>
</div> </div>
<p className="font-semibold text-sm text-white truncate mb-1" title={activeAudio.title}>
{activeAudio.title}
</p>
<p className="text-xs text-slate-400 mb-3 truncate">{activeAudio.author}</p>
<div className="flex items-center justify-between mt-3 px-2"> {/* Main content column */}
<button className="text-slate-400 hover:text-white transition-colors"><SkipBack size={18} /></button> <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
{/* Mobile top bar */}
<header className="md:hidden flex items-center justify-between px-4 py-3 bg-slate-900 border-b border-slate-800 shrink-0 z-20">
<button
onClick={() => setSidebarOpen(true)}
aria-label="Open menu"
className="text-slate-400 hover:text-white p-1"
>
<Menu size={22} />
</button>
<div className="flex items-center gap-2">
<div className="w-6 h-6 bg-white rounded-md flex items-center justify-center">
<TrendingUp className="text-slate-950" size={13} strokeWidth={2.5} />
</div>
<span className="font-bold text-sm tracking-wide text-white uppercase">FALAH</span>
<span className="text-[9px] uppercase font-bold tracking-widest text-slate-500">Coach Portal</span>
</div>
{activeAudio ? (
<button <button
onClick={togglePlay} onClick={togglePlay}
className="text-white bg-emerald-500 hover:bg-emerald-400 rounded-full w-10 h-10 flex items-center justify-center transition-all shadow-lg shadow-emerald-500/20" aria-label={isPlaying ? 'Pause' : 'Play'}
className="w-8 h-8 bg-emerald-500 hover:bg-emerald-400 rounded-full flex items-center justify-center transition-all"
> >
{isPlaying ? <PauseCircle size={24} className="text-slate-950" /> : <PlayCircle size={24} className="text-slate-950" />} {isPlaying ? <PauseCircle size={18} className="text-slate-950" /> : <PlayCircle size={18} className="text-slate-950" />}
</button> </button>
<button className="text-slate-400 hover:text-white transition-colors"><SkipForward size={18} /></button>
</div>
{/* Fake progress bar */}
<div className="w-full bg-slate-800 h-1 mt-4 rounded-full overflow-hidden">
<div className="bg-emerald-500 h-full w-1/4 rounded-full transition-all duration-1000 ease-linear"></div>
</div>
</div>
) : ( ) : (
<div className="py-4 text-center"> <div className="w-8" />
<Headphones className="mx-auto text-slate-700 mb-2" size={24} />
<p className="text-xs text-slate-500 font-medium">Select an audio brief to begin</p>
</div>
)} )}
</div> </header>
</div>
{/* Main Content Area */}
<main className="flex-1 overflow-y-auto p-8 relative scroll-smooth overflow-x-hidden">
{/* Ambient background glow */}
<div className="absolute top-[-20%] left-[-10%] w-[50%] h-[50%] bg-blue-600/5 rounded-full blur-[120px] pointer-events-none"></div>
{/* Scrollable main content */}
<main className="flex-1 overflow-y-auto p-4 md:p-8 relative scroll-smooth overflow-x-hidden">
<div className="absolute top-[-20%] left-[-10%] w-[50%] h-[50%] bg-blue-600/5 rounded-full blur-[120px] pointer-events-none" />
<div className="max-w-6xl mx-auto h-full relative z-10"> <div className="max-w-6xl mx-auto h-full relative z-10">
{activeView === 'missionControl' && <MissionControlView setActiveView={setActiveView} setActiveAudio={setActiveAudio} />} {activeView === 'missionControl' && <MissionControlView setActiveView={setActiveView} setActiveAudio={setActiveAudio} />}
{activeView === 'simulator' && <AICoPilotView />} {activeView === 'simulator' && <AICoPilotView />}
@@ -447,6 +509,48 @@ export default function App() {
</div> </div>
</main> </main>
{/* Mobile mini audio strip */}
{activeAudio && (
<div className="md:hidden shrink-0 bg-slate-900 border-t border-emerald-500/30 px-4 py-2 flex items-center gap-3 z-20 animate-fade-in-up">
<div className="flex-1 min-w-0">
<p className="text-xs font-semibold text-white truncate">{activeAudio.title}</p>
<p className="text-[10px] text-emerald-400 font-bold uppercase tracking-wider">Now Playing</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button aria-label="Previous" className="text-slate-400"><SkipBack size={16} /></button>
<button
onClick={togglePlay}
aria-label={isPlaying ? 'Pause' : 'Play'}
className="w-8 h-8 bg-emerald-500 rounded-full flex items-center justify-center"
>
{isPlaying ? <PauseCircle size={18} className="text-slate-950" /> : <PlayCircle size={18} className="text-slate-950" />}
</button>
<button aria-label="Next" className="text-slate-400"><SkipForward size={16} /></button>
</div>
</div>
)}
{/* Mobile bottom nav bar */}
<nav className="md:hidden shrink-0 bg-slate-900 border-t border-slate-800 flex items-center justify-around px-2 py-2 z-20">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = activeView === item.id;
return (
<button
key={item.id}
onClick={() => handleNavClick(item.id)}
aria-label={item.label}
className={`flex flex-col items-center gap-1 px-3 py-1.5 rounded-lg transition-colors ${
isActive ? 'text-blue-400' : 'text-slate-500 hover:text-slate-300'
}`}
>
<Icon size={20} />
<span className="text-[10px] font-medium leading-none">{item.label.split(' ')[0]}</span>
</button>
);
})}
</nav>
</div>
</div> </div>
); );
} }