- 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:
+160
-56
@@ -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,52 +357,13 @@ export default function App() {
|
|||||||
{ id: 'wiki', label: 'Loop Wiki', icon: BookOpen },
|
{ id: 'wiki', label: 'Loop Wiki', icon: BookOpen },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
const handleNavClick = (id) => {
|
||||||
<div className="flex h-screen bg-slate-950 text-slate-100 font-sans overflow-hidden selection:bg-blue-500/30">
|
setActiveView(id);
|
||||||
|
setSidebarOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
{/* Sidebar Layout */}
|
const AudioPlayer = () => (
|
||||||
<div className="w-72 border-r border-slate-800 flex flex-col bg-slate-900 shadow-2xl relative z-20">
|
activeAudio ? (
|
||||||
|
|
||||||
{/* Brand */}
|
|
||||||
<div className="p-6">
|
|
||||||
<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}/>
|
|
||||||
</div>
|
|
||||||
<span className="font-bold text-xl tracking-wide text-white uppercase">FALAH</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] uppercase font-bold tracking-[0.2em] text-slate-500 ml-11">Coach Portal</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<nav className="flex-1 px-4 mt-6 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;
|
|
||||||
const isActive = activeView === item.id;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => setActiveView(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'
|
|
||||||
: 'hover:bg-slate-800 text-slate-400 hover:text-slate-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Icon size={18} className={isActive ? 'text-white' : 'text-slate-500 group-hover:text-blue-400 transition-colors'}/>
|
|
||||||
<span className={`font-medium ${isActive ? 'text-white' : ''}`}>{item.label}</span>
|
|
||||||
</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="animate-fade-in-up">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<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-[10px] font-bold text-emerald-400 uppercase tracking-wider">Now Playing</span>
|
||||||
@@ -409,18 +373,17 @@ export default function App() {
|
|||||||
{activeAudio.title}
|
{activeAudio.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-400 mb-3 truncate">{activeAudio.author}</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">
|
<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>
|
<button aria-label="Previous track" className="text-slate-400 hover:text-white transition-colors"><SkipBack size={18} /></button>
|
||||||
<button
|
<button
|
||||||
onClick={togglePlay}
|
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"
|
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" />}
|
{isPlaying ? <PauseCircle size={24} className="text-slate-950" /> : <PlayCircle size={24} className="text-slate-950" />}
|
||||||
</button>
|
</button>
|
||||||
<button className="text-slate-400 hover:text-white transition-colors"><SkipForward size={18} /></button>
|
<button aria-label="Next track" className="text-slate-400 hover:text-white transition-colors"><SkipForward size={18} /></button>
|
||||||
</div>
|
</div>
|
||||||
{/* Fake progress bar */}
|
|
||||||
<div className="w-full bg-slate-800 h-1 mt-4 rounded-full overflow-hidden">
|
<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 className="bg-emerald-500 h-full w-1/4 rounded-full transition-all duration-1000 ease-linear"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -430,23 +393,164 @@ export default function App() {
|
|||||||
<Headphones className="mx-auto text-slate-700 mb-2" size={24} />
|
<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>
|
<p className="text-xs text-slate-500 font-medium">Select an audio brief to begin</p>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-slate-950 text-slate-100 font-sans overflow-hidden selection:bg-blue-500/30">
|
||||||
|
|
||||||
|
{/* 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 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} />
|
||||||
|
</div>
|
||||||
|
<span className="font-bold text-xl tracking-wide text-white uppercase">FALAH</span>
|
||||||
|
</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-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;
|
||||||
|
const isActive = activeView === item.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={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'
|
||||||
|
: 'hover:bg-slate-800 text-slate-400 hover:text-slate-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Icon size={18} className={isActive ? 'text-white' : 'text-slate-500 group-hover:text-blue-400 transition-colors'} />
|
||||||
|
<span className={`font-medium ${isActive ? 'text-white' : ''}`}>{item.label}</span>
|
||||||
|
</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">
|
||||||
|
<AudioPlayer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main content column */}
|
||||||
<main className="flex-1 overflow-y-auto p-8 relative scroll-smooth overflow-x-hidden">
|
<div className="flex-1 flex flex-col min-w-0 overflow-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>
|
|
||||||
|
|
||||||
|
{/* 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}
|
||||||
|
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={18} className="text-slate-950" /> : <PlayCircle size={18} className="text-slate-950" />}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="w-8" />
|
||||||
|
)}
|
||||||
|
</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">
|
<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 />}
|
||||||
{activeView === 'audio' && <AudioLibraryView setActiveAudio={setActiveAudio}/>}
|
{activeView === 'audio' && <AudioLibraryView setActiveAudio={setActiveAudio} />}
|
||||||
{activeView === 'wiki' && <MSLoopWikiView />}
|
{activeView === 'wiki' && <MSLoopWikiView />}
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user