Spaces:
Running
Running
| import React, { useState, useEffect } from 'react'; | |
| import axios from 'axios'; | |
| import { motion } from 'framer-motion'; | |
| import { useNavigate, useLocation } from 'react-router-dom'; | |
| import { useAuth } from '../context/AuthContext'; | |
| import { useTranslation } from 'react-i18next'; | |
| import { | |
| LayoutDashboard, | |
| PenTool, | |
| Calendar, | |
| Trophy, | |
| Settings, | |
| LogOut, | |
| User | |
| } from 'lucide-react'; | |
| const SidebarItem = ({ icon: Icon, label, active, onClick, badge }) => ( | |
| <motion.button | |
| whileHover={{ x: 5 }} | |
| whileTap={{ scale: 0.95 }} | |
| onClick={onClick} | |
| className={`w-full flex items-center justify-between px-6 py-4 rounded-xl transition-all group ${ | |
| active | |
| ? 'bg-primary text-on-primary rough-border shadow-[4px_4px_0px_0px_rgba(0,0,0,0.1)]' | |
| : 'text-on-surface-variant hover:bg-surface-variant hover:text-primary' | |
| }`} | |
| > | |
| <div className="flex items-center gap-4"> | |
| <Icon size={24} className={active ? 'text-secondary' : 'group-hover:text-primary'} /> | |
| <span className="text-xl font-bold font-display-lg tracking-wide">{label}</span> | |
| </div> | |
| {badge && ( | |
| <span className={`px-2 py-1 rounded-full text-xs font-bold ${active ? 'bg-secondary text-primary' : 'bg-primary/10 text-primary'}`}> | |
| {badge} | |
| </span> | |
| )} | |
| </motion.button> | |
| ); | |
| let cachedProjectCount = null; | |
| const Sidebar = () => { | |
| const navigate = useNavigate(); | |
| const location = useLocation(); | |
| const { logout, user } = useAuth(); | |
| const { t } = useTranslation(); | |
| const [projectCount, setProjectCount] = useState(() => { | |
| if (cachedProjectCount !== null) return cachedProjectCount; | |
| const stored = localStorage.getItem('tf_project_count'); | |
| return stored ? parseInt(stored, 10) : 0; | |
| }); | |
| useEffect(() => { | |
| if (!user) return; | |
| const fetchProjectCount = async () => { | |
| try { | |
| const API_URL = import.meta.env.VITE_API_URL; | |
| const res = await axios.get(`${API_URL}/graphs/user/${user.id}`); | |
| if (Array.isArray(res.data)) { | |
| const count = res.data.length; | |
| setProjectCount(count); | |
| cachedProjectCount = count; | |
| localStorage.setItem('tf_project_count', count.toString()); | |
| } | |
| } catch (err) { | |
| console.error('[Sidebar] Failed to fetch project count:', err); | |
| } | |
| }; | |
| fetchProjectCount(); | |
| const interval = setInterval(fetchProjectCount, 10000); | |
| return () => clearInterval(interval); | |
| }, [user]); | |
| const handleLogout = () => { | |
| logout(); | |
| navigate('/auth'); | |
| }; | |
| const menuItems = [ | |
| { icon: LayoutDashboard, labelKey: 'projects', path: '/dashboard' }, | |
| { icon: PenTool, labelKey: 'canvas', path: '/canvas' }, | |
| { icon: Calendar, labelKey: 'habits', path: '/habits' }, | |
| { icon: User, labelKey: 'profile', path: '/profile' }, | |
| { icon: Settings, labelKey: 'settings', path: '/settings' }, | |
| ]; | |
| return ( | |
| <aside className="w-80 border-r-4 border-primary bg-surface-container-lowest p-6 flex flex-col gap-10 h-screen sticky top-0"> | |
| <div className="flex items-center gap-3 px-4 cursor-pointer" onClick={() => navigate('/')}> | |
| <div className="w-12 h-12 bg-primary rounded-xl flex items-center justify-center text-on-primary"> | |
| <LayoutDashboard size={28} /> | |
| </div> | |
| <span className="text-3xl font-display-lg text-primary">TaskFlow</span> | |
| </div> | |
| <nav className="flex flex-col gap-2 flex-grow"> | |
| <div className="px-4 mb-2 text-xs font-bold uppercase tracking-[0.2em] text-on-surface-variant/50"> | |
| {t('sidebar.main_menu', 'Main Menu')} | |
| </div> | |
| {menuItems.map((item) => ( | |
| <SidebarItem | |
| key={item.labelKey} | |
| icon={item.icon} | |
| label={t(`sidebar.${item.labelKey}`)} | |
| active={location.pathname === item.path} | |
| onClick={() => navigate(item.path)} | |
| badge={item.labelKey === 'projects' ? projectCount.toString() : null} | |
| /> | |
| ))} | |
| </nav> | |
| <div className="flex flex-col gap-4"> | |
| <div className="p-4 bg-secondary/5 rough-border border-dashed border-secondary/30 flex flex-col gap-4"> | |
| <div className="flex flex-col gap-1"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-accent-note text-2xl text-secondary"> | |
| {user?.subscriptionTier === 'Ultra' ? 'Ultra' : user?.subscriptionTier === 'Pro' ? 'Pro' : t('sidebar.free_plan')} | |
| </span> | |
| {(!user?.subscriptionTier || user?.subscriptionTier === 'Free') && ( | |
| <span className="text-xs font-bold text-primary">{projectCount}/3 Slots</span> | |
| )} | |
| </div> | |
| <p className="text-[10px] text-on-surface-variant/70 font-semibold tracking-wide uppercase mt-1"> | |
| {user?.subscriptionTier === 'Ultra' | |
| ? t('sidebar.ultra_active', 'All Limits Unlocked') | |
| : user?.subscriptionTier === 'Pro' | |
| ? t('sidebar.pro_active', 'AI: {{count}}/200 daily', { count: user?.dailyAiGenerationsCount || 0 }) | |
| : t('sidebar.free_active', 'AI: {{count}}/3 daily', { count: user?.dailyAiGenerationsCount || 0 })} | |
| </p> | |
| </div> | |
| {user?.subscriptionTier !== 'Ultra' && ( | |
| <button | |
| onClick={() => navigate('/subscription')} | |
| className="w-full py-2 bg-primary text-on-primary text-sm font-bold rounded-lg hover:bg-secondary hover:text-primary transition-all cursor-pointer" | |
| > | |
| {user?.subscriptionTier === 'Pro' ? t('sidebar.go_ultra', 'Go Ultra') : t('sidebar.upgrade_now')} | |
| </button> | |
| )} | |
| </div> | |
| <button | |
| onClick={handleLogout} | |
| className="w-full flex items-center gap-4 px-6 py-4 text-on-surface-variant hover:text-error transition-colors font-bold cursor-pointer" | |
| > | |
| <LogOut size={24} /> | |
| <span className="font-display-lg text-xl">{t('sidebar.logout')}</span> | |
| </button> | |
| </div> | |
| </aside> | |
| ); | |
| }; | |
| export default Sidebar; | |