FlowWeb / src /components /Sidebar.jsx
danylokhodus's picture
feat: rebrand QuestboardAI -> TaskFlow everywhere (UI, locales, index.html)
abb5ee1
Raw
History Blame Contribute Delete
6.12 kB
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;