feat(layout): add mobile sidebar drawer navigation

This commit is contained in:
2026-04-25 17:28:00 +03:30
parent 2d903de97b
commit 056ff31ef8
3 changed files with 148 additions and 67 deletions

View File

@@ -3,14 +3,18 @@ import { useNavigate } from "react-router-dom"
import { useTranslation } from "../hooks/useTranslation"
import { Button } from "./ui/button"
import { SettingsMenu } from "./SettingsMenu"
import { LogOut, User, Moon, Sun, Globe, Command } from "lucide-react"
import { LogOut, User, Moon, Sun, Globe, Command, Menu } from "lucide-react"
import { logoutUser, getUserProfile } from "../api/users"
import { WorkspaceSelector } from "./WorkspaceSelector"
import { toast } from "sonner"
import { NotificationBell } from "./notifications/NotificationBell"
import { clearSessionTokens, getAccessToken, getRefreshToken } from "../lib/session"
export function Navbar() {
type NavbarProps = {
onOpenSidebar?: () => void
}
export function Navbar({ onOpenSidebar }: NavbarProps) {
const { t, lang, setLanguage } = useTranslation()
const navigate = useNavigate()
const [showLogoutModal, setShowLogoutModal] = useState(false)
@@ -106,12 +110,22 @@ export function Navbar() {
return (
<>
<header className="sticky top-0 z-50 flex items-center justify-between border-b border-slate-200/80 bg-white/70 px-8 py-6 backdrop-blur-md transition-colors dark:border-slate-800/80 dark:bg-slate-900/70">
<div className="flex cursor-pointer items-center gap-2" onClick={() => navigate("/")}>
<div className="flex items-center gap-3">
<button
type="button"
onClick={onOpenSidebar}
className="inline-flex h-11 w-11 items-center justify-center rounded-xl border border-slate-200 text-slate-600 transition-colors hover:bg-slate-100 md:hidden dark:border-slate-800 dark:text-slate-300 dark:hover:bg-slate-800"
title="Open menu"
>
<Menu className="h-5 w-5" />
</button>
<div className="flex cursor-pointer items-center gap-2" onClick={() => navigate("/")}>
<span className="relative z-20 flex items-center gap-2 text-xl font-bold tracking-tight text-slate-900 dark:text-slate-50">
<Command className="h-7 w-7" />
{t.title || "Qlockify"}
</span>
</div>
</div>
<div className="flex items-center gap-4">
{user && <WorkspaceSelector />}

View File

@@ -1,8 +1,9 @@
import { useState } from 'react';
import { NavLink } from 'react-router-dom';
import { useState } from 'react';
import { NavLink } from 'react-router-dom';
import {
Users,
LayoutDashboard,
X,
PanelLeftClose,
PanelLeftOpen,
PanelRightClose,
@@ -11,12 +12,17 @@ import {
Clock3,
Tags,
} from 'lucide-react';
import { useTranslation } from '../hooks/useTranslation';
export const Sidebar = () => {
const [isCollapsed, setIsCollapsed] = useState(false);
const { t, lang } = useTranslation();
import { useTranslation } from '../hooks/useTranslation';
type SidebarProps = {
mobileOpen?: boolean;
onMobileClose?: () => void;
};
export const Sidebar = ({ mobileOpen = false, onMobileClose }: SidebarProps) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const { t, lang } = useTranslation();
const isRtl = lang === 'fa';
const ToggleIcon = isRtl
@@ -49,57 +55,112 @@ export const Sidebar = () => {
icon: Briefcase,
label: t.sidebar?.projects || 'Projects'
},
];
return (
<aside
className={`border-e border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950 hidden md:flex flex-col h-screen sticky top-0 shrink-0 transition-all duration-300 ease-in-out ${
isCollapsed ? 'w-20' : 'w-64'
}`}
>
<div className={`h-18.25 flex items-center border-b border-slate-200 dark:border-slate-800 shrink-0 transition-all ${
isCollapsed ? 'justify-center px-0' : 'justify-between px-6'
}`}>
{!isCollapsed && (
<h2 className="text-xl font-bold text-slate-800 dark:text-white truncate">
{t.title || "Qlockify"}
</h2>
)}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="p-2 rounded-lg text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800 transition-colors"
title={isCollapsed ? (t.sidebar?.expand || 'Expand') : (t.sidebar?.collapse || 'Collapse')}
>
<ToggleIcon size={20} />
</button>
</div>
<nav className="flex-1 p-4 space-y-1 overflow-y-auto overflow-x-hidden">
{navItems.map((item) => {
const Icon = item.icon;
return (
<NavLink
key={item.path}
to={item.path}
title={isCollapsed ? item.label : undefined}
className={({ isActive }) =>
`flex items-center py-2.5 rounded-lg text-sm font-medium transition-colors ${
isCollapsed ? 'justify-center px-0' : 'gap-3 px-3'
} ${
isActive
? 'bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-900/50'
}`
}
>
<Icon size={isCollapsed ? 22 : 18} className="shrink-0" />
{!isCollapsed && (
<span className="truncate whitespace-nowrap">{item.label}</span>
)}
</NavLink>
);
})}
</nav>
</aside>
);
};
];
const renderNavItems = (mobile = false) =>
navItems.map((item) => {
const Icon = item.icon;
return (
<NavLink
key={`${mobile ? 'mobile' : 'desktop'}-${item.path}`}
to={item.path}
title={!mobile && isCollapsed ? item.label : undefined}
onClick={() => {
if (mobile) onMobileClose?.();
}}
className={({ isActive }) =>
`flex items-center rounded-lg text-sm font-medium transition-colors ${
mobile
? 'gap-3 px-4 py-3'
: isCollapsed
? 'justify-center px-0 py-2.5'
: 'gap-3 px-3 py-2.5'
} ${
isActive
? 'bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400'
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-900/50'
}`
}
>
<Icon size={mobile ? 20 : isCollapsed ? 22 : 18} className="shrink-0" />
{(mobile || !isCollapsed) && (
<span className="truncate whitespace-nowrap">{item.label}</span>
)}
</NavLink>
);
});
return (
<>
<aside
className={`border-e border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950 hidden md:flex flex-col h-screen sticky top-0 shrink-0 transition-all duration-300 ease-in-out ${
isCollapsed ? 'w-20' : 'w-64'
}`}
>
<div className={`h-18.25 flex items-center border-b border-slate-200 dark:border-slate-800 shrink-0 transition-all ${
isCollapsed ? 'justify-center px-0' : 'justify-between px-6'
}`}>
{!isCollapsed && (
<h2 className="text-xl font-bold text-slate-800 dark:text-white truncate">
{t.title || "Qlockify"}
</h2>
)}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="p-2 rounded-lg text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800 transition-colors"
title={isCollapsed ? (t.sidebar?.expand || 'Expand') : (t.sidebar?.collapse || 'Collapse')}
>
<ToggleIcon size={20} />
</button>
</div>
<nav className="flex-1 p-4 space-y-1 overflow-y-auto overflow-x-hidden">
{renderNavItems(false)}
</nav>
</aside>
<div
className={`fixed inset-0 z-[70] md:hidden transition-all duration-200 ${
mobileOpen ? 'pointer-events-auto' : 'pointer-events-none'
}`}
aria-hidden={!mobileOpen}
>
<button
type="button"
className={`absolute inset-0 bg-slate-950/50 backdrop-blur-[1px] transition-opacity ${
mobileOpen ? 'opacity-100' : 'opacity-0'
}`}
onClick={onMobileClose}
/>
<aside
className={`absolute inset-y-0 flex w-[18.5rem] max-w-[86vw] flex-col bg-white shadow-2xl transition-transform dark:bg-slate-950 ${
isRtl
? `right-0 border-s border-slate-200 dark:border-slate-800 ${
mobileOpen ? 'translate-x-0' : 'translate-x-full'
}`
: `left-0 border-e border-slate-200 dark:border-slate-800 ${
mobileOpen ? 'translate-x-0' : '-translate-x-full'
}`
}`}
>
<div className="flex items-center justify-between border-b border-slate-200 px-5 py-5 dark:border-slate-800">
<h2 className="truncate text-lg font-bold text-slate-800 dark:text-white">
{t.title || "Qlockify"}
</h2>
<button
type="button"
onClick={onMobileClose}
className="rounded-lg p-2 text-slate-500 transition-colors hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800"
title="Close"
>
<X size={20} />
</button>
</div>
<nav className="flex-1 space-y-1 overflow-y-auto p-4">
{renderNavItems(true)}
</nav>
</aside>
</div>
</>
);
};