fix(sidebar): add mobile sidebar
This commit is contained in:
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
|
||||
import { Montserrat, Roboto } from 'next/font/google';
|
||||
import '@core/styles/globals.scss';
|
||||
import '@core/styles/reset.scss';
|
||||
import { Footer, Header } from '@/widgets';
|
||||
import { Footer, Header, MobileCallback } from '@/widgets';
|
||||
import { ModalProvider } from '@core/providers/modal-provider';
|
||||
|
||||
const roboto = Roboto({
|
||||
@@ -34,6 +34,7 @@ export default function RootLayout({
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
<MobileCallback />
|
||||
</ModalProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.ContainerNavbar {
|
||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Navbar {
|
||||
margin: 0 auto;
|
||||
max-width: rem(1540px);
|
||||
padding: 0 10px;
|
||||
padding: 0 rem(10px);
|
||||
height: rem(100px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -27,15 +27,20 @@
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
.Invite{
|
||||
display: none;
|
||||
.Logotype {
|
||||
width: rem(250px);
|
||||
height: auto;
|
||||
|
||||
@include iflaptop{
|
||||
display: flex;
|
||||
width: rem(250px);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@include ifdesktop{
|
||||
width: rem(300px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.Nav {
|
||||
margin-left: auto;
|
||||
|
||||
@@ -85,7 +90,7 @@
|
||||
&:hover > ul {
|
||||
display: block;
|
||||
position: absolute;
|
||||
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +124,7 @@
|
||||
padding: 0px;
|
||||
min-width: 12em;
|
||||
font-family: $font-montseratt;
|
||||
font-weight: $font-medium;
|
||||
font-weight: 500;
|
||||
font-size: rem(16px);
|
||||
line-height: 100%;
|
||||
color: $color-text;
|
||||
@@ -152,4 +157,20 @@
|
||||
transform: rotateZ(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.Invite {
|
||||
display: none;
|
||||
|
||||
@include iflaptop {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.MobileNav{
|
||||
display: block;
|
||||
margin-right: rem(10px);
|
||||
@include iflaptop{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import s from './styles.module.scss';
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { ROUTES } from '@shared/const/route';
|
||||
import { Icons } from '@shared/ui/icon';
|
||||
import { baseMenu } from '@shared/const/menu';
|
||||
import { TMenu } from '@shared/types/menu';
|
||||
import { baseMenu, TMenuItem } from '@shared/const/menu';
|
||||
import { BeautyButton } from '@/entities';
|
||||
import dtrLogo from '@public/images/dtr-logo-eagle.png';
|
||||
import { Burger } from '@shared/ui';
|
||||
import { Sidebar } from '@/widgets';
|
||||
|
||||
function BaseMenu() {
|
||||
const phone = '+7 (900) 241-34-34';
|
||||
|
||||
const Menu = ({ list }: { list: TMenu }) => {
|
||||
const nodeList = (list: TMenu) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
const Menu = ({ list }: { list: TMenuItem[] }) => {
|
||||
const nodeList = (list: TMenuItem[]) => {
|
||||
return list.map((item, index) => {
|
||||
if (item.children && item.children.length) {
|
||||
return (
|
||||
<li key={index} className={s.Parent}>
|
||||
<div className={s.Row}>
|
||||
{item.link ? (
|
||||
<Link href={item.link}>{item.name}</Link>
|
||||
<Link href={item.link}>{item.title}</Link>
|
||||
) : (
|
||||
item.name
|
||||
item.title
|
||||
)}
|
||||
<Icons.MenuArrow className={s.Expand} />
|
||||
</div>
|
||||
@@ -32,7 +40,7 @@ function BaseMenu() {
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<Link href={item.link!}>{item.name}</Link>
|
||||
<Link href={item.link!}>{item.title}</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
@@ -48,13 +56,23 @@ function BaseMenu() {
|
||||
<section className={s.ContainerNavbar}>
|
||||
<div className={s.Navbar}>
|
||||
<Link href={ROUTES.HOME}>
|
||||
<Image src={dtrLogo} alt={'ДиТрасо'} quality={75} priority />
|
||||
<Image
|
||||
className={s.Logotype}
|
||||
src={dtrLogo}
|
||||
alt={'ДиТрасо'}
|
||||
quality={75}
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
<Menu list={baseMenu} />
|
||||
<div className={s.Invite}>
|
||||
<BeautyButton>{phone}</BeautyButton>
|
||||
</div>
|
||||
<div className={s.MobileNav}>
|
||||
<Burger isOpen={open} onToggle={setOpen} />
|
||||
</div>
|
||||
</div>
|
||||
<Sidebar isOpen={open} onClose={handleClose} list={baseMenu} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,267 +1,272 @@
|
||||
import { TMenu } from '@shared/types/menu';
|
||||
import { ROUTES } from './route';
|
||||
|
||||
export const baseMenu: TMenu = [
|
||||
export type TMenuItem = {
|
||||
title: string;
|
||||
link?: string;
|
||||
children?: TMenuItem[];
|
||||
};
|
||||
|
||||
export const baseMenu: TMenuItem[] = [
|
||||
{
|
||||
name: 'Экспертиза',
|
||||
title: 'Экспертиза',
|
||||
link: ROUTES.EXPERTIZA,
|
||||
children: [
|
||||
{
|
||||
name: 'Автотехническая',
|
||||
title: 'Автотехническая',
|
||||
link: ROUTES.EXPERTIZA_AUTOTECH,
|
||||
},
|
||||
{
|
||||
name: 'Трасологическая',
|
||||
title: 'Трасологическая',
|
||||
link: ROUTES.EXPERTIZA_TRASOLOGIA,
|
||||
},
|
||||
{
|
||||
name: 'Пожарно-техническая',
|
||||
title: 'Пожарно-техническая',
|
||||
link: ROUTES.EXPERTIZA_POZHAR,
|
||||
},
|
||||
{
|
||||
name: 'Товароведческая',
|
||||
title: 'Товароведческая',
|
||||
link: ROUTES.EXPERTIZA_TOVAR,
|
||||
},
|
||||
{
|
||||
name: 'Рецензирование и проверка экспертизы',
|
||||
title: 'Рецензирование и проверка экспертизы',
|
||||
link: ROUTES.EXPERTIZA_RECENZII,
|
||||
},
|
||||
{
|
||||
name: 'Документарная',
|
||||
title: 'Документарная',
|
||||
link: ROUTES.EXPERTIZA_DOCUMENT,
|
||||
},
|
||||
{
|
||||
name: 'Бухгалтерская',
|
||||
title: 'Бухгалтерская',
|
||||
link: ROUTES.EXPERTIZA_BUHGALTER,
|
||||
},
|
||||
{
|
||||
name: 'Финансово-экономическая',
|
||||
title: 'Финансово-экономическая',
|
||||
link: ROUTES.EXPERTIZA_FINANS,
|
||||
},
|
||||
{
|
||||
name: 'Земле-устроительная',
|
||||
title: 'Земле-устроительная',
|
||||
link: ROUTES.EXPERTIZA_ZEM_STROY,
|
||||
},
|
||||
{
|
||||
name: 'Кадастровая',
|
||||
title: 'Кадастровая',
|
||||
link: ROUTES.EXPERTIZA_KADASTR,
|
||||
},
|
||||
{
|
||||
name: 'Строительно-техническая',
|
||||
title: 'Строительно-техническая',
|
||||
link: ROUTES.EXPERTIZA_STROIT,
|
||||
},
|
||||
{
|
||||
name: 'Компьютерно-техническая',
|
||||
title: 'Компьютерно-техническая',
|
||||
link: ROUTES.EXPERTIZA_COPMPUTER,
|
||||
},
|
||||
{
|
||||
name: 'Почерковедческая',
|
||||
title: 'Почерковедческая',
|
||||
link: ROUTES.EXPERTIZA_POCHERK,
|
||||
},
|
||||
{
|
||||
name: 'Технико-криминалистическая',
|
||||
title: 'Технико-криминалистическая',
|
||||
link: ROUTES.EXPERTIZA_TECH_CRIM,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Оценка',
|
||||
title: 'Оценка',
|
||||
link: ROUTES.OCENKA,
|
||||
children: [
|
||||
{
|
||||
name: 'По ситуации',
|
||||
title: 'По ситуации',
|
||||
children: [
|
||||
{
|
||||
name: 'Независимая оценка',
|
||||
title: 'Независимая оценка',
|
||||
link: ROUTES.OCENKA_NEZAVISIM,
|
||||
},
|
||||
{
|
||||
name: 'Оценка для опеки',
|
||||
title: 'Оценка для опеки',
|
||||
link: ROUTES.OCENKA_OPEKA,
|
||||
},
|
||||
{
|
||||
name: 'Оценка для нотариуса',
|
||||
title: 'Оценка для нотариуса',
|
||||
link: ROUTES.OCENKA_NOTARIUS,
|
||||
},
|
||||
{
|
||||
name: 'Оценка для вступления в наследство',
|
||||
title: 'Оценка для вступления в наследство',
|
||||
link: ROUTES.OCENKA_NASLEDSTVO,
|
||||
},
|
||||
{
|
||||
name: 'Оценка для страхования',
|
||||
title: 'Оценка для страхования',
|
||||
link: ROUTES.OCENKA_STRAHOVANIE,
|
||||
},
|
||||
{
|
||||
name: 'Оценка для определения стоимости ущерба',
|
||||
title: 'Оценка для определения стоимости ущерба',
|
||||
link: ROUTES.OCENKA_USCHERB,
|
||||
},
|
||||
{
|
||||
name: 'Оценка имущества при разводе',
|
||||
title: 'Оценка имущества при разводе',
|
||||
link: ROUTES.OCENKA_RAZVOD,
|
||||
},
|
||||
{
|
||||
name: 'Оценка недвижимости для суда',
|
||||
title: 'Оценка недвижимости для суда',
|
||||
link: ROUTES.OCENKA_SUD,
|
||||
},
|
||||
{
|
||||
name: 'Оценка для внесения в уставный капитал',
|
||||
title: 'Оценка для внесения в уставный капитал',
|
||||
link: ROUTES.OCENKA_USTAV_KAPITAL,
|
||||
},
|
||||
{
|
||||
name: 'Оценка для ипотеки',
|
||||
title: 'Оценка для ипотеки',
|
||||
link: ROUTES.OCENKA_IPOTEKA,
|
||||
},
|
||||
{
|
||||
name: 'Оценка для банка',
|
||||
title: 'Оценка для банка',
|
||||
link: ROUTES.OCENKA_BANK,
|
||||
},
|
||||
{
|
||||
name: 'Оценка имущества для банкротства',
|
||||
title: 'Оценка имущества для банкротства',
|
||||
link: ROUTES.OCENKA_BANKROTSTV,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Жилая недвижимость',
|
||||
title: 'Жилая недвижимость',
|
||||
children: [
|
||||
{
|
||||
name: 'Оценка квартиры',
|
||||
title: 'Оценка квартиры',
|
||||
link: ROUTES.OCENKA_KVARTIRA,
|
||||
},
|
||||
{
|
||||
name: 'Оценка жилого дома',
|
||||
title: 'Оценка жилого дома',
|
||||
link: ROUTES.OCENKA_DOM,
|
||||
},
|
||||
{
|
||||
name: 'Оценка земельного участка',
|
||||
title: 'Оценка земельного участка',
|
||||
link: ROUTES.OCENKA_UCHASTOK,
|
||||
},
|
||||
{
|
||||
name: 'Оценка гаража',
|
||||
title: 'Оценка гаража',
|
||||
link: ROUTES.OCENKA_GARAZH,
|
||||
},
|
||||
{
|
||||
name: 'Оценка ущерба недвижимости',
|
||||
title: 'Оценка ущерба недвижимости',
|
||||
link: ROUTES.OCENKA_USCHERB_NEDVIGI,
|
||||
},
|
||||
{
|
||||
name: 'Оценка ущерба от залива',
|
||||
title: 'Оценка ущерба от залива',
|
||||
link: ROUTES.OCENKA_ZATOPLENIE,
|
||||
},
|
||||
{
|
||||
name: 'Оценка ущерба от пожара',
|
||||
title: 'Оценка ущерба от пожара',
|
||||
link: ROUTES.OCENKA_POZHAR,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Коммерческая недвижимость',
|
||||
title: 'Коммерческая недвижимость',
|
||||
children: [
|
||||
{
|
||||
name: 'Оценка стоимости арендной ставки',
|
||||
title: 'Оценка стоимости арендной ставки',
|
||||
link: ROUTES.OCENKA_ARENDA,
|
||||
},
|
||||
{
|
||||
name: 'Оценка здания',
|
||||
title: 'Оценка здания',
|
||||
link: ROUTES.OCENKA_ZDANIE,
|
||||
},
|
||||
{
|
||||
name: 'Оценка земель',
|
||||
title: 'Оценка земель',
|
||||
link: ROUTES.OCENKA_ZEMLYA,
|
||||
},
|
||||
{
|
||||
name: 'Оценка нежилого помещения',
|
||||
title: 'Оценка нежилого помещения',
|
||||
link: ROUTES.OCENKA_POMESCHENIE,
|
||||
},
|
||||
{
|
||||
name: 'Оценка офиса',
|
||||
title: 'Оценка офиса',
|
||||
link: ROUTES.OCENKA_OFFICE,
|
||||
},
|
||||
{
|
||||
name: 'Оценка сооружений',
|
||||
title: 'Оценка сооружений',
|
||||
link: ROUTES.OCENKA_SOORUZHENIE,
|
||||
},
|
||||
{
|
||||
name: 'Оценка незавершенного строительства',
|
||||
title: 'Оценка незавершенного строительства',
|
||||
link: ROUTES.OCENKA_NEZAV_STROIT,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Имущество',
|
||||
title: 'Имущество',
|
||||
children: [
|
||||
{
|
||||
name: 'Оценка недвижимости',
|
||||
title: 'Оценка недвижимости',
|
||||
link: ROUTES.OCENKA_NEDVIGA,
|
||||
},
|
||||
{
|
||||
name: 'Оценка машин и оборудования',
|
||||
title: 'Оценка машин и оборудования',
|
||||
link: ROUTES.OCENKA_MASHINES,
|
||||
},
|
||||
{
|
||||
name: 'Оценка спецтехники',
|
||||
title: 'Оценка спецтехники',
|
||||
link: ROUTES.OCENKA_SPECTECHNIKA,
|
||||
},
|
||||
{
|
||||
name: 'Оценка нематериальных активов',
|
||||
title: 'Оценка нематериальных активов',
|
||||
link: ROUTES.OCENKA_ACTIVES,
|
||||
},
|
||||
{
|
||||
name: 'Оценка стоимости предприятия',
|
||||
title: 'Оценка стоимости предприятия',
|
||||
link: ROUTES.OCENKA_BUSINESS,
|
||||
},
|
||||
{
|
||||
name: 'Оценка ценных бумаг',
|
||||
title: 'Оценка ценных бумаг',
|
||||
link: ROUTES.OCENKA_CENN_BUMAGI,
|
||||
},
|
||||
{
|
||||
name: 'Экспертиза и рецензирование отчета об оценке',
|
||||
title: 'Экспертиза и рецензирование отчета об оценке',
|
||||
link: ROUTES.OCENKA_RECINSIA_OTCHETA,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Ипотека',
|
||||
title: 'Ипотека',
|
||||
children: [
|
||||
{
|
||||
name: 'Оценка для ипотеки в Сбербанке',
|
||||
title: 'Оценка для ипотеки в Сбербанке',
|
||||
link: ROUTES.OCENKA_IPOTEKA_SBER,
|
||||
},
|
||||
{
|
||||
name: 'Оценка для ипотеки в банке ВТБ',
|
||||
title: 'Оценка для ипотеки в банке ВТБ',
|
||||
link: ROUTES.OCENKA_IPOTEKA_VTB,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Транспорт',
|
||||
title: 'Транспорт',
|
||||
children: [
|
||||
{
|
||||
name: 'Оценка ущерба в ДТП',
|
||||
title: 'Оценка ущерба в ДТП',
|
||||
link: ROUTES.OCENKA_DTP,
|
||||
},
|
||||
{
|
||||
name: 'Оценка УТС – утраты товарной стоимости',
|
||||
title: 'Оценка УТС – утраты товарной стоимости',
|
||||
link: ROUTES.OCENKA_UTS,
|
||||
},
|
||||
{
|
||||
name: 'Оценка рыночной стоимости автомобиля',
|
||||
title: 'Оценка рыночной стоимости автомобиля',
|
||||
link: ROUTES.OCENKA_RYNOCHNAYA,
|
||||
},
|
||||
{
|
||||
name: 'Оценка автомобиля для суда',
|
||||
title: 'Оценка автомобиля для суда',
|
||||
link: ROUTES.OCENKA_AUTO_SUD,
|
||||
},
|
||||
{
|
||||
name: 'Оценка мотоциклов и мототехники',
|
||||
title: 'Оценка мотоциклов и мототехники',
|
||||
link: ROUTES.OCENKA_MOTO,
|
||||
},
|
||||
{
|
||||
name: 'Оценка морских и речных судов',
|
||||
title: 'Оценка морских и речных судов',
|
||||
link: ROUTES.OCENKA_PLAVSREDSTV,
|
||||
},
|
||||
{
|
||||
name: 'Оценка воздушных судов и летательных аппаратов',
|
||||
title: 'Оценка воздушных судов и летательных аппаратов',
|
||||
link: ROUTES.OCENKA_VOZDUSHNYH_SUDOV,
|
||||
},
|
||||
],
|
||||
@@ -269,66 +274,66 @@ export const baseMenu: TMenu = [
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Юрист',
|
||||
title: 'Юрист',
|
||||
link: ROUTES.JURIST,
|
||||
children: [
|
||||
{
|
||||
name: 'Решение споров',
|
||||
title: 'Решение споров',
|
||||
children: [
|
||||
{
|
||||
name: 'Страховые споры',
|
||||
title: 'Страховые споры',
|
||||
link: ROUTES.JURIST_STRAHOVKA,
|
||||
},
|
||||
{
|
||||
name: 'Земельные и имущественные',
|
||||
title: 'Земельные и имущественные',
|
||||
link: ROUTES.JURIST_ZEMLYA_IMUSHESTVO,
|
||||
},
|
||||
{
|
||||
name: 'Семейные и наследственные',
|
||||
title: 'Семейные и наследственные',
|
||||
link: ROUTES.JURIST_NASLEDSTVO,
|
||||
},
|
||||
{
|
||||
name: 'Трудовые споры',
|
||||
title: 'Трудовые споры',
|
||||
link: ROUTES.JURIST_TRUD,
|
||||
},
|
||||
{
|
||||
name: 'Споры по ДТП',
|
||||
title: 'Споры по ДТП',
|
||||
link: ROUTES.JURIST_DTP,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Сопровождение сделок',
|
||||
title: 'Сопровождение сделок',
|
||||
link: ROUTES.JURIST_SDELKI_DOGOVORA,
|
||||
},
|
||||
{
|
||||
name: 'Представительство в суде',
|
||||
title: 'Представительство в суде',
|
||||
link: ROUTES.JURIST_PREDSTAVITELSTVO,
|
||||
},
|
||||
{
|
||||
name: 'Банкротство физических лиц',
|
||||
title: 'Банкротство физических лиц',
|
||||
link: ROUTES.JURIST_BANKROTSTVO,
|
||||
},
|
||||
{
|
||||
name: 'Взыскание задолженности',
|
||||
title: 'Взыскание задолженности',
|
||||
link: ROUTES.JURIST_DOLGI,
|
||||
},
|
||||
{
|
||||
name: 'Помощь должникам',
|
||||
title: 'Помощь должникам',
|
||||
link: ROUTES.JURIST_DOLZHNIKAM,
|
||||
},
|
||||
{
|
||||
name: 'Возврат страховки и комиссии банков',
|
||||
title: 'Возврат страховки и комиссии банков',
|
||||
link: ROUTES.JURIST_BANKI,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Эксперты',
|
||||
title: 'Эксперты',
|
||||
link: ROUTES.EXPERTS,
|
||||
},
|
||||
{
|
||||
name: 'Контакты',
|
||||
title: 'Контакты',
|
||||
link: ROUTES.CONTACTS,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export type TMenu = {
|
||||
name: string;
|
||||
link?: string;
|
||||
children?: TMenu;
|
||||
}[];
|
||||
1
src/shared/ui/burger/index.ts
Normal file
1
src/shared/ui/burger/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './ui';
|
||||
36
src/shared/ui/burger/styles.module.scss
Normal file
36
src/shared/ui/burger/styles.module.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
.Burger {
|
||||
position: relative;
|
||||
width: rem(32px);
|
||||
height: rem(24px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
z-index: 110;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
height: rem(4px);
|
||||
width: 100%;
|
||||
background: #000000;
|
||||
border-radius: 2px;
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
&_open {
|
||||
span:nth-child(1) {
|
||||
transform: translateY(10px) rotate(45deg);
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
span:nth-child(3) {
|
||||
transform: translateY(-10px) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/shared/ui/burger/ui.tsx
Normal file
23
src/shared/ui/burger/ui.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import s from './styles.module.scss';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type BurgerProps = {
|
||||
isOpen: boolean;
|
||||
onToggle: (value: boolean) => void;
|
||||
};
|
||||
|
||||
function Burger({ isOpen, onToggle }: BurgerProps) {
|
||||
return (
|
||||
<button
|
||||
className={clsx(s.Burger, isOpen && s.Burger_open)}
|
||||
onClick={() => onToggle(!isOpen)}
|
||||
aria-label='Menu'
|
||||
>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export { Burger };
|
||||
@@ -310,6 +310,38 @@ const MapOutline = (props: SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
//RiContactsBookLine
|
||||
const MobileContact = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={96}
|
||||
height={96}
|
||||
viewBox='0 0 24 24'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M3 2h16.005C20.107 2 21 2.898 21 3.99v16.02c0 1.099-.893 1.99-1.995 1.99H3zm4 2H5v16h2zm2 16h10V4H9zm2-4a3 3 0 1 1 6 0zm3-4a2 2 0 1 1 0-4a2 2 0 0 1 0 4m8-6h2v4h-2zm0 6h2v4h-2z'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
//lets-icons:phone-fill
|
||||
const MobilePhone = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width={96}
|
||||
height={96}
|
||||
viewBox='0 0 24 24'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='m6.68 3.32l.613-.613a1 1 0 0 1 1.414 0l2.586 2.586a1 1 0 0 1 0 1.414L9.5 8.5a.98.98 0 0 0-.183 1.133a11.3 11.3 0 0 0 5.05 5.05a.98.98 0 0 0 1.133-.184l1.793-1.792a1 1 0 0 1 1.414 0l2.586 2.586a1 1 0 0 1 0 1.414l-.613.613a6 6 0 0 1-7.843.558l-1.208-.907a23 23 0 0 1-4.6-4.6l-.907-1.208A6 6 0 0 1 6.68 3.32'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export {
|
||||
Map,
|
||||
Envelope,
|
||||
@@ -328,4 +360,6 @@ export {
|
||||
MailBulk,
|
||||
Consultation,
|
||||
MapOutline,
|
||||
MobileContact,
|
||||
MobilePhone,
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
MailBulk,
|
||||
Consultation,
|
||||
MapOutline,
|
||||
MobileContact,
|
||||
MobilePhone,
|
||||
} from './base';
|
||||
|
||||
import {
|
||||
@@ -69,6 +71,8 @@ const Icons = Object.assign(
|
||||
MailBulk,
|
||||
Consultation,
|
||||
MapOutline,
|
||||
MobileContact,
|
||||
MobilePhone,
|
||||
},
|
||||
{
|
||||
GridBook,
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './modal';
|
||||
export * from './text-area';
|
||||
export * from './partners-slider';
|
||||
export * from './icon';
|
||||
export * from './burger';
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
.Footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: rem(60px);
|
||||
|
||||
@include iflaptop {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.Container {
|
||||
@@ -151,6 +156,11 @@
|
||||
@include iflaptop {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: rem(10px);
|
||||
}
|
||||
|
||||
@include ifdesktop{
|
||||
gap: rem(24px);
|
||||
}
|
||||
|
||||
.IconBox {
|
||||
@@ -276,7 +286,7 @@
|
||||
%title {
|
||||
font-family: $font-roboto;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
font-size: rem(20px);
|
||||
line-height: 100%;
|
||||
color: $color-white;
|
||||
margin-bottom: rem(20px);
|
||||
@@ -285,7 +295,7 @@
|
||||
%list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: rem(10px);
|
||||
list-style: disc;
|
||||
margin-left: rem(40px);
|
||||
}
|
||||
@@ -307,4 +317,12 @@
|
||||
line-height: 100%;
|
||||
color: $color-white;
|
||||
margin-bottom: rem(20px);
|
||||
|
||||
@include iflaptop{
|
||||
font-size: rem(14px);
|
||||
}
|
||||
|
||||
@include ifdesktop{
|
||||
font-size: rem(18px);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './header';
|
||||
export * from './footer';
|
||||
export * from './breadcrumbs';
|
||||
export * from './mobile-callback';
|
||||
export * from './sidebar';
|
||||
|
||||
1
src/widgets/mobile-callback/index.ts
Normal file
1
src/widgets/mobile-callback/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './ui';
|
||||
47
src/widgets/mobile-callback/styles.module.scss
Normal file
47
src/widgets/mobile-callback/styles.module.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
.Root {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
z-index: 120;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: rem(60px);
|
||||
width: 100%;
|
||||
background: $color-green;
|
||||
|
||||
@include iflaptop{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.Button {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: rem(8px);
|
||||
border: 1px solid $color-white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
width: rem(20px);
|
||||
|
||||
@include iftablet{
|
||||
width: rem(40px);
|
||||
}
|
||||
|
||||
path {
|
||||
fill: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.Text {
|
||||
font-family: $font-roboto;
|
||||
font-weight: 500;
|
||||
font-size: rem(16px);
|
||||
line-height: 100%;
|
||||
color: $color-white;
|
||||
@include iftablet{
|
||||
font-size: rem(20px);
|
||||
}
|
||||
}
|
||||
19
src/widgets/mobile-callback/ui.tsx
Normal file
19
src/widgets/mobile-callback/ui.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import s from './styles.module.scss';
|
||||
import { Icons } from '@shared/ui';
|
||||
|
||||
function MobileCallback() {
|
||||
return (
|
||||
<div className={s.Root}>
|
||||
<button className={s.Button}>
|
||||
<Icons.MobileContact className={s.Icon} />
|
||||
<span className={s.Text}>Записаться</span>
|
||||
</button>
|
||||
<button className={s.Button}>
|
||||
<Icons.MobilePhone className={s.Icon} />
|
||||
<span className={s.Text}>+7 (900) 241-34-34</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MobileCallback };
|
||||
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