- 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:
+142
-38
@@ -15,7 +15,9 @@ import {
|
||||
TrendingUp,
|
||||
FileText,
|
||||
ChevronRight,
|
||||
FolderOpen
|
||||
FolderOpen,
|
||||
Menu,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -344,6 +346,7 @@ export default function App() {
|
||||
const [activeView, setActiveView] = useState('missionControl');
|
||||
const [activeAudio, setActiveAudio] = useState(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const togglePlay = () => setIsPlaying(!isPlaying);
|
||||
|
||||
@@ -354,14 +357,66 @@ export default function App() {
|
||||
{ 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 (
|
||||
<div className="flex h-screen bg-slate-950 text-slate-100 font-sans overflow-hidden selection:bg-blue-500/30">
|
||||
|
||||
{/* Sidebar Layout */}
|
||||
<div className="w-72 border-r border-slate-800 flex flex-col bg-slate-900 shadow-2xl relative z-20">
|
||||
{/* Mobile overlay backdrop */}
|
||||
{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 */}
|
||||
<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="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
|
||||
<TrendingUp className="text-slate-950" size={20} strokeWidth={2.5} />
|
||||
@@ -370,9 +425,17 @@ export default function App() {
|
||||
</div>
|
||||
<p className="text-[10px] uppercase font-bold tracking-[0.2em] text-slate-500 ml-11">Coach Portal</p>
|
||||
</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 */}
|
||||
<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>
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
@@ -380,7 +443,7 @@ export default function App() {
|
||||
return (
|
||||
<button
|
||||
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 ${
|
||||
isActive
|
||||
? 'bg-blue-600 shadow-md shadow-blue-500/20'
|
||||
@@ -393,52 +456,51 @@ export default function App() {
|
||||
</div>
|
||||
{isActive && <ChevronRight size={16} className="text-blue-200" />}
|
||||
</button>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Global Audio Player */}
|
||||
<div className="mt-auto border-t border-slate-800 p-4 bg-slate-950/50 backdrop-blur-md">
|
||||
{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>
|
||||
<AudioPlayer />
|
||||
</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">
|
||||
<button className="text-slate-400 hover:text-white transition-colors"><SkipBack size={18} /></button>
|
||||
{/* Main content column */}
|
||||
<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
|
||||
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 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">
|
||||
<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 className="w-8" />
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
</header>
|
||||
|
||||
{/* 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">
|
||||
{activeView === 'missionControl' && <MissionControlView setActiveView={setActiveView} setActiveAudio={setActiveAudio} />}
|
||||
{activeView === 'simulator' && <AICoPilotView />}
|
||||
@@ -447,6 +509,48 @@ export default function App() {
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user