Spaces:
Running
Running
| import React, { useState } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { | |
| LayoutDashboard, | |
| BookOpen, | |
| Bookmark, | |
| Settings, | |
| LogOut, | |
| Search, | |
| Bell, | |
| Menu, | |
| X, | |
| ChevronRight, | |
| TrendingUp, | |
| User, | |
| Activity, | |
| Award | |
| } from 'lucide-react'; | |
| import { Link, useLocation, useNavigate } from 'react-router-dom'; | |
| import { useAppContext } from '../../context/AppContext'; | |
| interface DashboardLayoutProps { | |
| children: React.ReactNode; | |
| } | |
| const sidebarItems = [ | |
| { icon: LayoutDashboard, label: 'Dashboard', path: '/dashboard' }, | |
| { icon: BookOpen, label: 'Navigator', path: '/navigator' }, | |
| { icon: Bookmark, label: 'Resources', path: '/resources' }, | |
| { icon: Settings, label: 'Settings', path: '#' }, | |
| ]; | |
| export const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children }) => { | |
| const [isSidebarOpen, setIsSidebarOpen] = useState(true); | |
| const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); | |
| const location = useLocation(); | |
| const navigate = useNavigate(); | |
| const [isSettingsOpen, setIsSettingsOpen] = useState(false); | |
| const [isProfileOpen, setIsProfileOpen] = useState(false); | |
| const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); | |
| const { agent, levelUpMessage, setLevelUpMessage, notifications, markNotificationsAsRead } = useAppContext(); | |
| const progressPercent = agent.totalReward % 100; | |
| const unreadCount = notifications.filter(n => !n.read).length; | |
| const handleSignOut = () => { | |
| // Basic sign out | |
| navigate('/login'); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-[#F0F4F8] text-slate-800 font-['Poppins'] overflow-hidden flex transition-colors duration-500"> | |
| {/* Background Blobs - Subtle & Airy */} | |
| <div className="fixed top-[-10%] left-[-10%] w-[40%] h-[40%] bg-brand/5 rounded-full blur-[120px] pointer-events-none" /> | |
| <div className="fixed bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-brand-light/5 rounded-full blur-[120px] pointer-events-none" /> | |
| {/* Sidebar - Desktop */} | |
| <motion.aside | |
| initial={false} | |
| animate={{ width: isSidebarOpen ? 280 : 88 }} | |
| className="hidden lg:flex flex-col bg-white/60 backdrop-blur-xl border-r border-slate-200 relative z-30 h-screen transition-all duration-300 shadow-xl shadow-slate-200/50" | |
| > | |
| <div className="p-6 flex items-center gap-3"> | |
| <AnimatePresence> | |
| {isSidebarOpen && ( | |
| <motion.span | |
| initial={{ opacity: 0, x: -10 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0, x: -10 }} | |
| className="text-xl font-extrabold whitespace-nowrap bg-clip-text text-transparent bg-gradient-to-r from-brand to-brand-dark uppercase tracking-tighter" | |
| > | |
| Navigated Learning | |
| </motion.span> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| <nav className="flex-1 px-4 py-10 space-y-3"> | |
| {sidebarItems.map((item) => { | |
| const isActive = location.pathname === item.path; | |
| return ( | |
| <Link | |
| key={item.path} | |
| to={item.path} | |
| onClick={(e) => { | |
| if (item.label === 'Settings') { | |
| e.preventDefault(); | |
| setIsSettingsOpen(true); | |
| } | |
| }} | |
| className={` | |
| flex items-center gap-4 px-4 py-3.5 rounded-2xl transition-all duration-300 group relative | |
| ${isActive ? 'bg-white text-brand shadow-lg shadow-brand/10' : 'text-slate-500 hover:bg-white/40 hover:text-brand'} | |
| `} | |
| > | |
| {isActive && ( | |
| <motion.div layoutId="activePill" className="absolute left-0 w-1.5 h-6 bg-brand rounded-full" /> | |
| )} | |
| <item.icon size={22} className={`${isActive ? 'text-brand' : 'group-hover:scale-110 transition-transform'}`} /> | |
| {isSidebarOpen && <span className="font-bold">{item.label}</span>} | |
| {isActive && isSidebarOpen && ( | |
| <motion.div layoutId="activeTab" className="ml-auto"> | |
| <ChevronRight size={16} /> | |
| </motion.div> | |
| )} | |
| </Link> | |
| ); | |
| })} | |
| </nav> | |
| <div className="p-6 border-t border-slate-100"> | |
| <button | |
| onClick={handleSignOut} | |
| className="flex items-center gap-4 px-4 py-3 w-full rounded-2xl text-slate-400 hover:bg-red-50 hover:text-red-500 transition-all duration-300 font-bold"> | |
| <LogOut size={22} /> | |
| {isSidebarOpen && <span>Sign Out</span>} | |
| </button> | |
| </div> | |
| {/* Toggle Button */} | |
| <button | |
| onClick={() => setIsSidebarOpen(!isSidebarOpen)} | |
| className="absolute -right-3 top-20 w-7 h-7 bg-white rounded-full flex items-center justify-center border border-slate-200 shadow-xl hover:scale-110 hover:border-brand transition-all group" | |
| > | |
| <ChevronRight size={14} className={`text-slate-400 group-hover:text-brand transition-transform duration-300 ${isSidebarOpen ? 'rotate-180' : ''}`} /> | |
| </button> | |
| </motion.aside> | |
| {/* Main Content Area */} | |
| <div className="flex-1 flex flex-col h-screen overflow-hidden"> | |
| {/* Navbar */} | |
| <header className="h-20 bg-white/40 backdrop-blur-md border-b border-white/80 flex items-center justify-between px-6 lg:px-12 shrink-0 z-20"> | |
| <div className="flex items-center gap-4 flex-1"> | |
| <button | |
| className="lg:hidden p-2 text-slate-500 hover:text-brand transition-colors" | |
| onClick={() => setIsMobileMenuOpen(true)} | |
| > | |
| <Menu size={24} /> | |
| </button> | |
| <div className="relative w-full max-w-md hidden sm:block"> | |
| <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" size={18} /> | |
| <input | |
| type="text" | |
| placeholder="Find a module or project..." | |
| className="w-full bg-white/80 border border-slate-200 rounded-2xl py-3 pl-12 pr-4 outline-none focus:border-brand focus:ring-4 focus:ring-brand/10 transition-all text-sm shadow-sm" | |
| /> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-4 lg:gap-8"> | |
| <button | |
| onClick={() => { | |
| setIsNotificationsOpen(!isNotificationsOpen); | |
| if (!isNotificationsOpen) markNotificationsAsRead(); | |
| }} | |
| className="relative p-2.5 bg-white border border-slate-100 rounded-xl text-slate-500 hover:text-brand hover:border-brand/20 transition-all group shadow-sm" | |
| > | |
| <Bell size={20} /> | |
| {unreadCount > 0 && ( | |
| <span className="absolute top-2 right-2 w-2.5 h-2.5 bg-brand rounded-full ring-4 ring-white" /> | |
| )} | |
| </button> | |
| <div className="h-8 w-px bg-slate-200 hidden sm:block" /> | |
| <div className="flex items-center gap-4 pl-2 group"> | |
| <div className="text-right hidden sm:block"> | |
| <p className="text-sm font-extrabold text-slate-900 group-hover:text-brand transition-colors">Learner</p> | |
| <div className="flex items-center gap-2 justify-end mt-0.5"> | |
| <div className="w-16 h-1 bg-slate-100 rounded-full overflow-hidden"> | |
| <motion.div | |
| initial={{ width: 0 }} | |
| animate={{ width: `${progressPercent}%` }} | |
| className="h-full bg-brand" | |
| /> | |
| </div> | |
| <p className="text-[9px] text-slate-400 font-bold uppercase tracking-widest">{progressPercent}% SYNC</p> | |
| </div> | |
| </div> | |
| <motion.div | |
| whileHover={{ scale: 1.05 }} | |
| onClick={() => setIsProfileOpen(true)} | |
| className="w-11 h-11 rounded-2xl bg-gradient-to-tr from-brand to-brand-light p-0.5 shadow-lg shadow-brand/20 cursor-pointer" | |
| > | |
| <div className="w-full h-full rounded-[14px] bg-white flex items-center justify-center overflow-hidden"> | |
| <img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix" alt="Avatar" className="w-full h-full object-cover" /> | |
| </div> | |
| </motion.div> | |
| </div> | |
| </div> | |
| </header> | |
| {/* Scrollable Content */} | |
| <main className="flex-1 overflow-y-auto custom-scrollbar p-6 lg:p-12 relative"> | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.5 }} | |
| > | |
| {children} | |
| </motion.div> | |
| </main> | |
| </div> | |
| {/* Mobile Menu Overlay */} | |
| <AnimatePresence> | |
| {isMobileMenuOpen && ( | |
| <> | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| onClick={() => setIsMobileMenuOpen(false)} | |
| className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 lg:hidden" | |
| /> | |
| <motion.aside | |
| initial={{ x: '-100%' }} | |
| animate={{ x: 0 }} | |
| exit={{ x: '-100%' }} | |
| className="fixed inset-y-0 left-0 w-72 bg-white border-r border-slate-200 z-50 lg:hidden p-6 shadow-2xl" | |
| > | |
| <div className="flex items-center justify-between mb-10"> | |
| <div className="flex items-center gap-3"> | |
| <span className="text-lg font-black text-brand uppercase tracking-tighter">Navigated Learning</span> | |
| </div> | |
| <button onClick={() => setIsMobileMenuOpen(false)} className="p-2 text-slate-400 hover:text-brand transition-colors"> | |
| <X size={24} /> | |
| </button> | |
| </div> | |
| <nav className="space-y-3"> | |
| {sidebarItems.map((item) => { | |
| const isActive = location.pathname === item.path; | |
| return ( | |
| <Link | |
| key={item.path} | |
| to={item.path} | |
| onClick={() => setIsMobileMenuOpen(false)} | |
| className={`flex items-center gap-4 px-4 py-3.5 rounded-2xl transition-all ${isActive ? 'bg-brand/10 text-brand font-bold' : 'text-slate-500 hover:bg-slate-50'}`} | |
| > | |
| <item.icon size={20} className={isActive ? 'text-brand' : ''} /> | |
| <span>{item.label}</span> | |
| </Link> | |
| ); | |
| })} | |
| </nav> | |
| </motion.aside> | |
| </> | |
| )} | |
| </AnimatePresence> | |
| <style dangerouslySetInnerHTML={{ __html: ` | |
| .custom-scrollbar::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 10px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| `}} /> | |
| {/* Notifications Dropdown */} | |
| <AnimatePresence> | |
| {isNotificationsOpen && ( | |
| <> | |
| <div className="fixed inset-0 z-[140]" onClick={() => setIsNotificationsOpen(false)} /> | |
| <motion.div | |
| initial={{ opacity: 0, y: 10, scale: 0.95 }} | |
| animate={{ opacity: 1, y: 0, scale: 1 }} | |
| exit={{ opacity: 0, y: 10, scale: 0.95 }} | |
| className="fixed top-24 right-12 w-80 bg-white rounded-3xl shadow-2xl border border-slate-100 z-[150] overflow-hidden" | |
| > | |
| <div className="p-5 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center"> | |
| <h3 className="font-bold text-slate-800">Intelligence Briefing</h3> | |
| <span className="text-[10px] font-black text-brand uppercase tracking-widest">{notifications.length} Events</span> | |
| </div> | |
| <div className="max-h-[400px] overflow-y-auto"> | |
| {notifications.length > 0 ? ( | |
| notifications.map((n) => ( | |
| <div key={n.id} className="p-5 border-b border-slate-50 last:border-0 hover:bg-slate-50 transition-colors"> | |
| <div className="flex gap-4"> | |
| <div className={`w-2 h-2 rounded-full mt-1.5 shrink-0 ${n.type === 'success' ? 'bg-green-500' : 'bg-brand'}`} /> | |
| <div> | |
| <p className="text-xs font-medium text-slate-700 leading-relaxed">{n.message}</p> | |
| <p className="text-[9px] text-slate-400 font-bold uppercase mt-2">{new Date(n.timestamp).toLocaleTimeString()}</p> | |
| </div> | |
| </div> | |
| </div> | |
| )) | |
| ) : ( | |
| <div className="p-10 text-center text-slate-400"> | |
| <p className="text-xs font-bold uppercase tracking-widest">Awaiting Intel...</p> | |
| </div> | |
| )} | |
| </div> | |
| </motion.div> | |
| </> | |
| )} | |
| </AnimatePresence> | |
| {/* Profile Modal */} | |
| <AnimatePresence> | |
| {isProfileOpen && ( | |
| <div className="fixed inset-0 bg-slate-900/60 backdrop-blur-md z-[100] flex items-center justify-center p-4"> | |
| <motion.div | |
| initial={{ scale: 0.9, opacity: 0, y: 20 }} | |
| animate={{ scale: 1, opacity: 1, y: 0 }} | |
| exit={{ scale: 0.9, opacity: 0, y: 20 }} | |
| className="bg-white rounded-[3rem] shadow-2xl w-full max-w-lg overflow-hidden relative border border-white/20" | |
| > | |
| {/* Profile Background Header */} | |
| <div className="h-40 bg-gradient-to-br from-brand to-brand-dark relative"> | |
| <button | |
| onClick={() => setIsProfileOpen(false)} | |
| className="absolute top-6 right-6 p-2 bg-white/20 hover:bg-white/40 text-white rounded-full transition-all" | |
| > | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| <div className="px-10 pb-10 -mt-16 relative"> | |
| <div className="flex items-end justify-between mb-8"> | |
| <div className="w-32 h-32 rounded-[2.5rem] bg-white p-1.5 shadow-2xl border border-slate-100"> | |
| <div className="w-full h-full rounded-[2rem] overflow-hidden bg-slate-50"> | |
| <img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix" alt="Profile" className="w-full h-full object-cover" /> | |
| </div> | |
| </div> | |
| <div className="flex flex-col items-end pb-2"> | |
| <span className="px-4 py-1.5 bg-brand text-white text-[10px] font-black uppercase tracking-widest rounded-xl shadow-lg shadow-brand/20">Stage {agent.level} Explorer</span> | |
| <p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-2">ID: Learner_NL_01</p> | |
| </div> | |
| </div> | |
| <div className="mb-8"> | |
| <h2 className="text-3xl font-black text-slate-900 uppercase tracking-tighter">Learner</h2> | |
| <p className="text-slate-500 font-medium mt-1">Neural Architect & NLP Specialist</p> | |
| </div> | |
| <div className="grid grid-cols-2 gap-4 mb-8"> | |
| <div className="bg-slate-50 p-5 rounded-3xl border border-slate-100"> | |
| <div className="flex items-center gap-2 text-brand mb-2"> | |
| <Activity size={16} /> | |
| <span className="text-[9px] font-black uppercase tracking-widest">Total XP</span> | |
| </div> | |
| <p className="text-2xl font-black text-slate-900 tracking-tight">{agent.totalReward}</p> | |
| </div> | |
| <div className="bg-slate-50 p-5 rounded-3xl border border-slate-100"> | |
| <div className="flex items-center gap-2 text-green-500 mb-2"> | |
| <Award size={16} /> | |
| <span className="text-[9px] font-black uppercase tracking-widest">Matrix Clear</span> | |
| </div> | |
| <p className="text-2xl font-black text-slate-900 tracking-tight">{agent.visitedResources.length}/18</p> | |
| </div> | |
| </div> | |
| <div className="space-y-3"> | |
| <div className="flex justify-between items-end"> | |
| <span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Current Stage Synchronization</span> | |
| <span className="text-lg font-black text-brand tracking-tighter">{progressPercent}%</span> | |
| </div> | |
| <div className="h-3 bg-slate-100 rounded-full overflow-hidden border border-slate-200"> | |
| <motion.div | |
| initial={{ width: 0 }} | |
| animate={{ width: `${progressPercent}%` }} | |
| className="h-full bg-brand" | |
| /> | |
| </div> | |
| </div> | |
| <div className="mt-10 flex gap-4"> | |
| <button className="flex-1 py-4 bg-slate-900 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest shadow-xl shadow-slate-900/20 hover:scale-[1.02] active:scale-95 transition-all flex items-center justify-center gap-3"> | |
| Edit Profile <TrendingUp size={18} /> | |
| </button> | |
| <button | |
| onClick={handleSignOut} | |
| className="p-4 bg-slate-100 text-slate-500 rounded-2xl hover:bg-red-50 hover:text-red-500 transition-all active:scale-95" | |
| > | |
| <LogOut size={20} /> | |
| </button> | |
| </div> | |
| </div> | |
| </motion.div> | |
| </div> | |
| )} | |
| </AnimatePresence> | |
| {/* Settings Modal */} | |
| <AnimatePresence> | |
| {isSettingsOpen && ( | |
| <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[100] flex items-center justify-center p-4"> | |
| <motion.div | |
| initial={{ scale: 0.95, opacity: 0 }} | |
| animate={{ scale: 1, opacity: 1 }} | |
| exit={{ scale: 0.95, opacity: 0 }} | |
| className="bg-white rounded-[2rem] shadow-2xl w-full max-w-md overflow-hidden relative" | |
| > | |
| <div className="p-6 border-b border-slate-100 flex items-center justify-between bg-slate-50/50"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 bg-slate-200/50 rounded-xl text-slate-600"> | |
| <Settings size={20} /> | |
| </div> | |
| <h3 className="font-bold text-lg text-slate-800">Preferences</h3> | |
| </div> | |
| <button onClick={() => setIsSettingsOpen(false)} className="p-2 text-slate-400 hover:bg-slate-200/50 hover:text-slate-600 rounded-full transition-colors"> | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| <div className="p-6 space-y-6 bg-white"> | |
| <div> | |
| <h4 className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4">Neural Settings</h4> | |
| <div className="space-y-3"> | |
| <div className="flex items-center justify-between p-3 rounded-2xl border border-slate-100 bg-slate-50/50"> | |
| <div> | |
| <p className="text-sm font-semibold text-slate-700">High-Performance Engine</p> | |
| <p className="text-[10px] text-slate-400 font-medium mt-0.5">Accelerate matrix polyline generation</p> | |
| </div> | |
| <div className="w-10 h-6 bg-brand rounded-full relative cursor-pointer shadow-inner"> | |
| <div className="absolute right-1 top-1 w-4 h-4 bg-white rounded-full shadow-sm" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <h4 className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4">Danger Zone</h4> | |
| <button | |
| onClick={async () => { | |
| try { | |
| const res = await fetch('http://localhost:5000/api/reset', { method: 'POST' }); | |
| if (res.ok) window.location.reload(); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| }} | |
| className="w-full flex items-center justify-between p-3 rounded-2xl border border-red-100 bg-red-50 hover:bg-red-100 transition-colors group" | |
| > | |
| <div className="text-left"> | |
| <p className="text-sm font-semibold text-red-600 group-hover:text-red-700">Wipe Database Memory</p> | |
| <p className="text-[10px] text-red-400 mt-0.5">Permanently resets all learning progress & histories</p> | |
| </div> | |
| <LogOut size={18} className="text-red-400 group-hover:text-red-600" /> | |
| </button> | |
| </div> | |
| </div> | |
| </motion.div> | |
| </div> | |
| )} | |
| </AnimatePresence> | |
| {/* Global Level Up Toast Notification */} | |
| <AnimatePresence> | |
| {levelUpMessage && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: -50, scale: 0.9 }} | |
| animate={{ opacity: 1, y: 0, scale: 1 }} | |
| exit={{ opacity: 0, y: -20, scale: 0.9 }} | |
| className="fixed top-8 left-1/2 -translate-x-1/2 z-[200] bg-[#0f111a] text-white px-8 py-5 rounded-[2rem] shadow-[0_0_80px_-15px_rgba(79,70,229,0.5)] border border-white/10 flex items-center gap-6" | |
| > | |
| <div className="w-14 h-14 rounded-2xl bg-gradient-to-tr from-brand via-purple-500 to-pink-500 flex items-center justify-center shadow-inner relative overflow-hidden"> | |
| <div className="absolute inset-0 bg-[linear-gradient(45deg,transparent_25%,rgba(255,255,255,0.3)_50%,transparent_75%,transparent_100%)] bg-[length:200%_200%] animate-[shine_2s_infinite]" /> | |
| <span className="text-xl font-black drop-shadow-md">{agent.level}</span> | |
| </div> | |
| <div> | |
| <p className="text-[10px] font-bold text-brand uppercase tracking-[0.3em] mb-1">Rank Up</p> | |
| <h3 className="text-xl font-bold tracking-tight">{levelUpMessage}</h3> | |
| </div> | |
| <button onClick={() => setLevelUpMessage(null)} className="ml-4 p-2 text-white/50 hover:text-white transition-colors"> | |
| <X size={20} /> | |
| </button> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ); | |
| }; | |