fix(sidebar): add mobile sidebar
This commit is contained in:
1
src/widgets/sidebar/index.ts
Normal file
1
src/widgets/sidebar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './ui';
|
||||
55
src/widgets/sidebar/menu-item.tsx
Normal file
55
src/widgets/sidebar/menu-item.tsx
Normal 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 };
|
||||
45
src/widgets/sidebar/menu-list.tsx
Normal file
45
src/widgets/sidebar/menu-list.tsx
Normal 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 };
|
||||
117
src/widgets/sidebar/styles.module.scss
Normal file
117
src/widgets/sidebar/styles.module.scss
Normal 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); // достаточно, чтобы влезли элементы
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
src/widgets/sidebar/ui.tsx
Normal file
40
src/widgets/sidebar/ui.tsx
Normal 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 };
|
||||
Reference in New Issue
Block a user