refactor(layouts): extract MainLayout component and improve sidebar mobile UX

- Extract MainLayout component from ConditionalLayout to reduce nesting and improve code organization
- Add useIsMobile hook to MainLayout for responsive behavior and prevent layout reflow on mobile
- Implement collapsed sidebar state for mobile devices showing only icons (70px width)
- Add MenuExpandIcon component to sidebar icons for collapsed state UI
- Update sidebar to display favicon instead of full logo when collapsed on mobile
- Remove menu toggle button and logo from header, consolidate in sidebar for better UX
- Improve sidebar styling with conditional padding and alignment for collapsed state
- Update sidebar navigation items to hide text labels when collapsed, showing only icons
- Refactor header component to remove mobile-specific logic now handled in MainLayout
- Enhance mobile navigation by maintaining sidebar space reservation to avoid layout reflow
This commit is contained in:
AmirReza Jamali
2025-12-08 17:07:54 +03:30
parent 13d736e9f0
commit 4cc7684b1f
5 changed files with 165 additions and 88 deletions
+105 -49
View File
@@ -2,11 +2,12 @@
import { Logo } from "@/components/logo";
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { NAV_DATA, NavSubItem } from "./data";
import { ArrowLeftIcon, ChevronUp } from "./icons";
import { ArrowLeftIcon, ChevronUp, MenuExpandIcon } from "./icons";
import { MenuItem } from "./menu-item";
import { useSidebarContext } from "./sidebar-context";
@@ -43,6 +44,9 @@ export function Sidebar() {
setExpandedItems(newExpandedItems);
}, [pathname]);
// Collapsed state: show only icons (for mobile)
const isCollapsed = isMobile && !isOpen;
return (
<>
{isMobile && isOpen && (
@@ -55,25 +59,41 @@ export function Sidebar() {
<aside
className={cn(
"max-w-[290px] overflow-hidden border-r border-gray-200 bg-white transition-[width] duration-200 ease-linear dark:border-gray-800 dark:bg-gray-dark",
"overflow-hidden border-r border-gray-200 bg-white transition-[width] duration-200 ease-linear dark:border-gray-800 dark:bg-gray-dark",
isMobile ? "fixed bottom-0 top-0 z-50" : "sticky top-0 h-screen",
isOpen ? "w-full" : "w-0",
isMobile
? isOpen
? "w-[290px]"
: "w-[70px]"
: "w-full max-w-[290px]",
)}
aria-label="Main navigation"
aria-hidden={!isOpen}
inert={!isOpen}
>
<div className="flex h-full flex-col py-10 pl-[25px] pr-[7px]">
<div className="relative pr-4.5">
<div
className={cn(
"flex h-full flex-col py-10",
isCollapsed ? "items-center px-2" : "pl-[25px] pr-[7px]",
)}
>
<div className={cn("relative", isCollapsed ? "px-0" : "pr-4.5")}>
<Link
href={"/"}
onClick={() => isMobile && toggleSidebar()}
onClick={() => isMobile && isOpen && toggleSidebar()}
className="px-0 py-2.5 min-[850px]:py-0"
>
<Logo />
{isCollapsed ? (
<Image
src="/images/fav-icon.svg"
width={32}
height={32}
alt="Logo"
/>
) : (
<Logo />
)}
</Link>
{isMobile && (
{isMobile && isOpen && (
<button
onClick={toggleSidebar}
className="absolute left-3/4 right-4.5 top-1/2 -translate-y-1/2 text-right"
@@ -85,15 +105,22 @@ export function Sidebar() {
)}
</div>
<div className="custom-scrollbar mt-6 flex-1 overflow-y-auto pr-3 min-[850px]:mt-10">
<div
className={cn(
"custom-scrollbar mt-6 flex-1 overflow-y-auto min-[850px]:mt-10",
isCollapsed ? "w-full px-1" : "pr-3",
)}
>
{NAV_DATA.map((section) => (
<div key={section.label} className="mb-6">
<h2 className="mb-5 text-sm font-medium text-dark-4 dark:text-dark-6">
{section.label}
</h2>
{!isCollapsed && (
<h2 className="mb-5 text-sm font-medium text-dark-4 dark:text-dark-6">
{section.label}
</h2>
)}
<nav role="navigation" aria-label={section.label}>
<ul className="space-y-2">
<ul className={cn("space-y-2", isCollapsed && "space-y-1")}>
{section.items.map((item) => (
<li key={item.title}>
{item.items.length ? (
@@ -110,47 +137,60 @@ export function Sidebar() {
pathname.startsWith(url) && url !== "/",
))
}
onClick={() => toggleExpanded(item.title)}
onClick={() => {
if (isCollapsed) {
toggleSidebar();
} else {
toggleExpanded(item.title);
}
}}
isCollapsed={isCollapsed}
title={item.title}
>
<item.icon
className="size-6 shrink-0"
aria-hidden="true"
/>
<span>{item.title}</span>
{!isCollapsed && (
<>
<span>{item.title}</span>
<ChevronUp
className={cn(
"ml-auto rotate-180 transition-transform duration-200",
expandedItems.includes(item.title) &&
"rotate-0",
)}
aria-hidden="true"
/>
<ChevronUp
className={cn(
"ml-auto rotate-180 transition-transform duration-200",
expandedItems.includes(item.title) &&
"rotate-0",
)}
aria-hidden="true"
/>
</>
)}
</MenuItem>
{expandedItems.includes(item.title) && (
<ul
className="ml-9 mr-0 space-y-1.5 pb-[15px] pr-0 pt-2"
role="menu"
>
{item.items.map((subItem: NavSubItem) => (
<li key={subItem.title} role="none">
<MenuItem
as="link"
href={subItem.url}
isActive={
pathname === subItem.url ||
(pathname.startsWith(subItem.url) &&
subItem.url !== "/")
}
>
<span>{subItem.title}</span>
</MenuItem>
</li>
))}
</ul>
)}
{!isCollapsed &&
expandedItems.includes(item.title) && (
<ul
className="ml-9 mr-0 space-y-1.5 pb-[15px] pr-0 pt-2"
role="menu"
>
{item.items.map((subItem: NavSubItem) => (
<li key={subItem.title} role="none">
<MenuItem
as="link"
href={subItem.url}
isActive={
pathname === subItem.url ||
(pathname.startsWith(subItem.url) &&
subItem.url !== "/")
}
>
<span>{subItem.title}</span>
</MenuItem>
</li>
))}
</ul>
)}
</div>
) : (
(() => {
@@ -167,17 +207,22 @@ export function Sidebar() {
return (
<MenuItem
className="flex items-center gap-3 py-3"
className={cn(
"flex items-center gap-3 py-3",
isCollapsed && "justify-center px-2",
)}
as="link"
href={href}
isActive={isActive}
isCollapsed={isCollapsed}
title={item.title}
>
<item.icon
className="size-6 shrink-0"
aria-hidden="true"
/>
<span>{item.title}</span>
{!isCollapsed && <span>{item.title}</span>}
</MenuItem>
);
})()
@@ -189,6 +234,17 @@ export function Sidebar() {
</div>
))}
</div>
{/* Toggle button at bottom for collapsed state */}
{isCollapsed && (
<button
onClick={toggleSidebar}
className="mt-4 rounded-lg p-2 text-dark-4 transition-colors hover:bg-gray-100 dark:text-dark-6 dark:hover:bg-[#FFFFFF1A]"
aria-label="Expand sidebar"
>
<MenuExpandIcon className="size-6" />
</button>
)}
</div>
</aside>
</>