fix(sidebar): add mobile sidebar

This commit is contained in:
2025-12-03 15:30:46 +03:00
parent 18ebfa734a
commit 01de6f6e75
22 changed files with 613 additions and 109 deletions

View File

@@ -0,0 +1 @@
export * from './ui';

View File

@@ -0,0 +1,55 @@
import s from './styles.module.scss';
import { TMenuItem } from '@shared/const/menu';
import clsx from 'clsx';
import { MenuList } from '@widgets/sidebar/menu-list';
import Link from 'next/link';
type MenuItemProps = {
item: TMenuItem;
level: number;
isOpen: boolean;
onToggle: () => void;
onClose: () => void;
};
function MenuItem({ item, level, onToggle, onClose, isOpen }: MenuItemProps) {
const hasChildren = !!item.children?.length;
const onLinkClick = () => {
onClose();
};
return (
<li className={s.MenuItem}>
<div
className={clsx(s.MenuTitle, hasChildren && s.MenuTitle_hasChildren)}
onClick={hasChildren ? onToggle : undefined}
>
{hasChildren ? (
<>{item.title}</>
) : (
<Link href={item.link ?? '#'} onClick={onLinkClick}>
{item.title}
</Link>
)}
{hasChildren && (
<span className={clsx(s.Arrow, isOpen && s.Arrow_open)} />
)}
</div>
{hasChildren && (
<div className={clsx(s.Submenu, isOpen && s.Submenu_open)}>
<MenuList
items={item.children!}
level={level + 1}
onClose={onClose}
isOpen={isOpen}
/>
</div>
)}
</li>
);
}
export { MenuItem };

View File

@@ -0,0 +1,45 @@
'use client';
import s from './styles.module.scss';
import { TMenuItem } from '@shared/const/menu';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { MenuItem } from '@widgets/sidebar/menu-item';
type MenuListProps = {
items: TMenuItem[];
level: number;
onClose: () => void;
isOpen: boolean;
};
function MenuList({ items, level, onClose, isOpen }: MenuListProps) {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const toggleItem = (index: number): void => {
setActiveIndex((prev) => (prev === index ? null : index));
};
useEffect(() => {
if (!isOpen) {
setActiveIndex(null);
}
}, [isOpen]);
return (
<ul className={clsx(s.Menu, s[`Menu_level_${level}`])}>
{items.map((item, index) => (
<MenuItem
key={index}
item={item}
level={level}
isOpen={activeIndex === index}
onToggle={() => toggleItem(index)}
onClose={onClose}
/>
))}
</ul>
);
}
export { MenuList };

View File

@@ -0,0 +1,117 @@
.Overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
opacity: 0;
pointer-events: none;
transition: 0.3s ease;
z-index: 100;
&_show {
opacity: 1;
pointer-events: all;
}
}
.Sidebar {
position: fixed;
top: 0;
left: rem(-280px);
width: rem(280px);
height: 100%;
background: #fff;
padding: rem(40px) rem(20px);
box-shadow: rem(-4px) 0 rem(10px) rgba(0, 0, 0, 0.1);
transition: 0.35s ease;
z-index: 120;
display: flex;
flex-direction: column;
gap: rem(20px);
&_open {
left: 0;
}
nav {
display: flex;
flex-direction: column;
gap: 12px;
}
a {
text-decoration: none;
font-family: $font-roboto;
font-weight: 400;
font-size: rem(16px);
line-height: 100%;
color: #333333;
transition: 0.2s;
&:hover {
opacity: 0.6;
}
}
}
/* --- multi-level menu --- */
.Menu {
list-style: none;
padding: 0;
margin: 0;
&_level_1 {
padding-left: rem(15px);
}
&_level_2 {
padding-left: rem(30px);
}
}
.MenuItem {
margin-bottom: rem(8px);
.MenuTitle {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: rem(4px) 0;
font-family: $font-roboto;
font-weight: 400;
font-size: rem(16px);
line-height: 130%;
color: #333333;
transition: .2s;
&:hover {
opacity: 0.6;
}
}
.Arrow {
width: rem(8px);
height: rem(8px);
border-top: 2px solid #222;
border-right: 2px solid #222;
transform: rotate(45deg);
transition: 0.25s ease;
margin-left: rem(8px);
&_open {
transform: rotate(135deg);
}
}
/* submenu animation */
.Submenu {
max-height: 0;
overflow: hidden;
transition: max-height .3s ease;
&_open {
max-height: rem(500px); // достаточно, чтобы влезли элементы
}
}
}

View File

@@ -0,0 +1,40 @@
import s from './styles.module.scss';
import clsx from 'clsx';
import { TMenuItem } from '@shared/const/menu';
import { ROUTES } from '@shared/const/route';
import { MenuList } from '@widgets/sidebar/menu-list';
type SidebarProps = {
isOpen: boolean;
onClose: () => void;
list: TMenuItem[];
};
function Sidebar({ isOpen, onClose, list }: SidebarProps) {
const menuData = [
{
title: 'Главная',
link: ROUTES.HOME,
},
...list,
];
return (
<>
<div
className={clsx(s.Overlay, isOpen && s.Overlay_show)}
onClick={onClose}
/>
<aside className={clsx(s.Sidebar, isOpen && s.Sidebar_open)}>
<MenuList
items={menuData}
level={0}
onClose={onClose}
isOpen={isOpen}
/>
</aside>
</>
);
}
export { Sidebar };