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

28
.idea/workspace.xml generated
View File

@@ -5,9 +5,28 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="2a96f817-9dc2-4f3c-893a-c4974c750774" name="Changes" comment=""> <list default="true" id="2a96f817-9dc2-4f3c-893a-c4974c750774" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/src/shared/ui/burger/index.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/shared/ui/burger/styles.module.scss" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/shared/ui/burger/ui.tsx" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/widgets/mobile-callback/index.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/widgets/mobile-callback/styles.module.scss" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/widgets/mobile-callback/ui.tsx" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/widgets/sidebar/index.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/widgets/sidebar/menu-item.tsx" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/widgets/sidebar/menu-list.tsx" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/widgets/sidebar/styles.module.scss" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/widgets/sidebar/ui.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/app/layout.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/app/layout.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/entities/base-menu/styles.module.scss" beforeDir="false" afterPath="$PROJECT_DIR$/src/entities/base-menu/styles.module.scss" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/entities/base-menu/ui.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/entities/base-menu/ui.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/shared/const/menu.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/shared/const/menu.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/shared/types/menu.ts" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/shared/ui/icon/base.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/shared/ui/icon/base.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/shared/ui/icon/ui.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/shared/ui/icon/ui.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/shared/ui/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/shared/ui/index.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/widgets/footer/styles.module.scss" beforeDir="false" afterPath="$PROJECT_DIR$/src/widgets/footer/styles.module.scss" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/widgets/footer/styles.module.scss" beforeDir="false" afterPath="$PROJECT_DIR$/src/widgets/footer/styles.module.scss" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/widgets/footer/ui.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/widgets/footer/ui.tsx" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/widgets/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/widgets/index.ts" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -15,16 +34,16 @@
<option name="LAST_RESOLUTION" value="IGNORE" /> <option name="LAST_RESOLUTION" value="IGNORE" />
</component> </component>
<component name="DarkyenusTimeTracker"> <component name="DarkyenusTimeTracker">
<option name="totalTimeSeconds" value="181981" /> <option name="totalTimeSeconds" value="192744" />
<option name="gitIntegration" value="true" /> <option name="gitIntegration" value="true" />
<option name="naggedAbout" value="1" /> <option name="naggedAbout" value="1" />
</component> </component>
<component name="FileTemplateManagerImpl"> <component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES"> <option name="RECENT_TEMPLATES">
<list> <list>
<option value="SCSS File" />
<option value="TypeScript File" /> <option value="TypeScript File" />
<option value="TypeScript JSX File" /> <option value="TypeScript JSX File" />
<option value="SCSS File" />
</list> </list>
</option> </option>
</component> </component>
@@ -71,6 +90,7 @@
}</component> }</component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS"> <key name="MoveFile.RECENT_KEYS">
<recent name="C:\dev-personal\ocenka-web\src\widgets\sidebar" />
<recent name="C:\dev-personal\ocenka-web\src\views\home" /> <recent name="C:\dev-personal\ocenka-web\src\views\home" />
</key> </key>
</component> </component>
@@ -108,7 +128,7 @@
<workItem from="1764590282382" duration="1201000" /> <workItem from="1764590282382" duration="1201000" />
<workItem from="1764591867512" duration="4332000" /> <workItem from="1764591867512" duration="4332000" />
<workItem from="1764657017067" duration="21490000" /> <workItem from="1764657017067" duration="21490000" />
<workItem from="1764741053553" duration="1873000" /> <workItem from="1764741053553" duration="16724000" />
</task> </task>
<servers /> <servers />
</component> </component>

View File

@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
import { Montserrat, Roboto } from 'next/font/google'; import { Montserrat, Roboto } from 'next/font/google';
import '@core/styles/globals.scss'; import '@core/styles/globals.scss';
import '@core/styles/reset.scss'; import '@core/styles/reset.scss';
import { Footer, Header } from '@/widgets'; import { Footer, Header, MobileCallback } from '@/widgets';
import { ModalProvider } from '@core/providers/modal-provider'; import { ModalProvider } from '@core/providers/modal-provider';
const roboto = Roboto({ const roboto = Roboto({
@@ -34,6 +34,7 @@ export default function RootLayout({
<Header /> <Header />
<main>{children}</main> <main>{children}</main>
<Footer /> <Footer />
<MobileCallback />
</ModalProvider> </ModalProvider>
</body> </body>
</html> </html>

View File

@@ -1,11 +1,11 @@
.ContainerNavbar { .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 { .Navbar {
margin: 0 auto; margin: 0 auto;
max-width: rem(1540px); max-width: rem(1540px);
padding: 0 10px; padding: 0 rem(10px);
height: rem(100px); height: rem(100px);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -27,14 +27,19 @@
padding: 0 40px; padding: 0 40px;
} }
.Invite{ .Logotype {
display: none; width: rem(250px);
height: auto;
@include iflaptop{ @include iflaptop{
display: flex; width: rem(250px);
} height: auto;
} }
@include ifdesktop{
width: rem(300px);
}
}
.Nav { .Nav {
margin-left: auto; margin-left: auto;
@@ -85,7 +90,7 @@
&:hover > ul { &:hover > ul {
display: block; display: block;
position: absolute; 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; padding: 0px;
min-width: 12em; min-width: 12em;
font-family: $font-montseratt; font-family: $font-montseratt;
font-weight: $font-medium; font-weight: 500;
font-size: rem(16px); font-size: rem(16px);
line-height: 100%; line-height: 100%;
color: $color-text; color: $color-text;
@@ -152,4 +157,20 @@
transform: rotateZ(90deg); transform: rotateZ(90deg);
} }
} }
.Invite {
display: none;
@include iflaptop {
display: flex;
}
}
.MobileNav{
display: block;
margin-right: rem(10px);
@include iflaptop{
display: none;
}
}
} }

View File

@@ -1,27 +1,35 @@
'use client';
import s from './styles.module.scss'; import s from './styles.module.scss';
import { useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { ROUTES } from '@shared/const/route'; import { ROUTES } from '@shared/const/route';
import { Icons } from '@shared/ui/icon'; import { Icons } from '@shared/ui/icon';
import { baseMenu } from '@shared/const/menu'; import { baseMenu, TMenuItem } from '@shared/const/menu';
import { TMenu } from '@shared/types/menu';
import { BeautyButton } from '@/entities'; import { BeautyButton } from '@/entities';
import dtrLogo from '@public/images/dtr-logo-eagle.png'; import dtrLogo from '@public/images/dtr-logo-eagle.png';
import { Burger } from '@shared/ui';
import { Sidebar } from '@/widgets';
function BaseMenu() { function BaseMenu() {
const phone = '+7 (900) 241-34-34'; const phone = '+7 (900) 241-34-34';
const Menu = ({ list }: { list: TMenu }) => { const [open, setOpen] = useState(false);
const nodeList = (list: TMenu) => {
const handleClose = () => setOpen(false);
const Menu = ({ list }: { list: TMenuItem[] }) => {
const nodeList = (list: TMenuItem[]) => {
return list.map((item, index) => { return list.map((item, index) => {
if (item.children && item.children.length) { if (item.children && item.children.length) {
return ( return (
<li key={index} className={s.Parent}> <li key={index} className={s.Parent}>
<div className={s.Row}> <div className={s.Row}>
{item.link ? ( {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} /> <Icons.MenuArrow className={s.Expand} />
</div> </div>
@@ -32,7 +40,7 @@ function BaseMenu() {
return ( return (
<li key={index}> <li key={index}>
<Link href={item.link!}>{item.name}</Link> <Link href={item.link!}>{item.title}</Link>
</li> </li>
); );
}); });
@@ -48,13 +56,23 @@ function BaseMenu() {
<section className={s.ContainerNavbar}> <section className={s.ContainerNavbar}>
<div className={s.Navbar}> <div className={s.Navbar}>
<Link href={ROUTES.HOME}> <Link href={ROUTES.HOME}>
<Image src={dtrLogo} alt={'ДиТрасо'} quality={75} priority /> <Image
className={s.Logotype}
src={dtrLogo}
alt={'ДиТрасо'}
quality={75}
priority
/>
</Link> </Link>
<Menu list={baseMenu} /> <Menu list={baseMenu} />
<div className={s.Invite}> <div className={s.Invite}>
<BeautyButton>{phone}</BeautyButton> <BeautyButton>{phone}</BeautyButton>
</div> </div>
<div className={s.MobileNav}>
<Burger isOpen={open} onToggle={setOpen} />
</div> </div>
</div>
<Sidebar isOpen={open} onClose={handleClose} list={baseMenu} />
</section> </section>
); );
} }

View File

@@ -1,267 +1,272 @@
import { TMenu } from '@shared/types/menu';
import { ROUTES } from './route'; 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, link: ROUTES.EXPERTIZA,
children: [ children: [
{ {
name: 'Автотехническая', title: 'Автотехническая',
link: ROUTES.EXPERTIZA_AUTOTECH, link: ROUTES.EXPERTIZA_AUTOTECH,
}, },
{ {
name: 'Трасологическая', title: 'Трасологическая',
link: ROUTES.EXPERTIZA_TRASOLOGIA, link: ROUTES.EXPERTIZA_TRASOLOGIA,
}, },
{ {
name: 'Пожарно-техническая', title: 'Пожарно-техническая',
link: ROUTES.EXPERTIZA_POZHAR, link: ROUTES.EXPERTIZA_POZHAR,
}, },
{ {
name: 'Товароведческая', title: 'Товароведческая',
link: ROUTES.EXPERTIZA_TOVAR, link: ROUTES.EXPERTIZA_TOVAR,
}, },
{ {
name: 'Рецензирование и проверка экспертизы', title: 'Рецензирование и проверка экспертизы',
link: ROUTES.EXPERTIZA_RECENZII, link: ROUTES.EXPERTIZA_RECENZII,
}, },
{ {
name: 'Документарная', title: 'Документарная',
link: ROUTES.EXPERTIZA_DOCUMENT, link: ROUTES.EXPERTIZA_DOCUMENT,
}, },
{ {
name: 'Бухгалтерская', title: 'Бухгалтерская',
link: ROUTES.EXPERTIZA_BUHGALTER, link: ROUTES.EXPERTIZA_BUHGALTER,
}, },
{ {
name: 'Финансово-экономическая', title: 'Финансово-экономическая',
link: ROUTES.EXPERTIZA_FINANS, link: ROUTES.EXPERTIZA_FINANS,
}, },
{ {
name: 'Земле-устроительная', title: 'Земле-устроительная',
link: ROUTES.EXPERTIZA_ZEM_STROY, link: ROUTES.EXPERTIZA_ZEM_STROY,
}, },
{ {
name: 'Кадастровая', title: 'Кадастровая',
link: ROUTES.EXPERTIZA_KADASTR, link: ROUTES.EXPERTIZA_KADASTR,
}, },
{ {
name: 'Строительно-техническая', title: 'Строительно-техническая',
link: ROUTES.EXPERTIZA_STROIT, link: ROUTES.EXPERTIZA_STROIT,
}, },
{ {
name: 'Компьютерно-техническая', title: 'Компьютерно-техническая',
link: ROUTES.EXPERTIZA_COPMPUTER, link: ROUTES.EXPERTIZA_COPMPUTER,
}, },
{ {
name: 'Почерковедческая', title: 'Почерковедческая',
link: ROUTES.EXPERTIZA_POCHERK, link: ROUTES.EXPERTIZA_POCHERK,
}, },
{ {
name: 'Технико-криминалистическая', title: 'Технико-криминалистическая',
link: ROUTES.EXPERTIZA_TECH_CRIM, link: ROUTES.EXPERTIZA_TECH_CRIM,
}, },
], ],
}, },
{ {
name: 'Оценка', title: 'Оценка',
link: ROUTES.OCENKA, link: ROUTES.OCENKA,
children: [ children: [
{ {
name: 'По ситуации', title: 'По ситуации',
children: [ children: [
{ {
name: 'Независимая оценка', title: 'Независимая оценка',
link: ROUTES.OCENKA_NEZAVISIM, link: ROUTES.OCENKA_NEZAVISIM,
}, },
{ {
name: 'Оценка для опеки', title: 'Оценка для опеки',
link: ROUTES.OCENKA_OPEKA, link: ROUTES.OCENKA_OPEKA,
}, },
{ {
name: 'Оценка для нотариуса', title: 'Оценка для нотариуса',
link: ROUTES.OCENKA_NOTARIUS, link: ROUTES.OCENKA_NOTARIUS,
}, },
{ {
name: 'Оценка для вступления в наследство', title: 'Оценка для вступления в наследство',
link: ROUTES.OCENKA_NASLEDSTVO, link: ROUTES.OCENKA_NASLEDSTVO,
}, },
{ {
name: 'Оценка для страхования', title: 'Оценка для страхования',
link: ROUTES.OCENKA_STRAHOVANIE, link: ROUTES.OCENKA_STRAHOVANIE,
}, },
{ {
name: 'Оценка для определения стоимости ущерба', title: 'Оценка для определения стоимости ущерба',
link: ROUTES.OCENKA_USCHERB, link: ROUTES.OCENKA_USCHERB,
}, },
{ {
name: 'Оценка имущества при разводе', title: 'Оценка имущества при разводе',
link: ROUTES.OCENKA_RAZVOD, link: ROUTES.OCENKA_RAZVOD,
}, },
{ {
name: 'Оценка недвижимости для суда', title: 'Оценка недвижимости для суда',
link: ROUTES.OCENKA_SUD, link: ROUTES.OCENKA_SUD,
}, },
{ {
name: 'Оценка для внесения в уставный капитал', title: 'Оценка для внесения в уставный капитал',
link: ROUTES.OCENKA_USTAV_KAPITAL, link: ROUTES.OCENKA_USTAV_KAPITAL,
}, },
{ {
name: 'Оценка для ипотеки', title: 'Оценка для ипотеки',
link: ROUTES.OCENKA_IPOTEKA, link: ROUTES.OCENKA_IPOTEKA,
}, },
{ {
name: 'Оценка для банка', title: 'Оценка для банка',
link: ROUTES.OCENKA_BANK, link: ROUTES.OCENKA_BANK,
}, },
{ {
name: 'Оценка имущества для банкротства', title: 'Оценка имущества для банкротства',
link: ROUTES.OCENKA_BANKROTSTV, link: ROUTES.OCENKA_BANKROTSTV,
}, },
], ],
}, },
{ {
name: 'Жилая недвижимость', title: 'Жилая недвижимость',
children: [ children: [
{ {
name: 'Оценка квартиры', title: 'Оценка квартиры',
link: ROUTES.OCENKA_KVARTIRA, link: ROUTES.OCENKA_KVARTIRA,
}, },
{ {
name: 'Оценка жилого дома', title: 'Оценка жилого дома',
link: ROUTES.OCENKA_DOM, link: ROUTES.OCENKA_DOM,
}, },
{ {
name: 'Оценка земельного участка', title: 'Оценка земельного участка',
link: ROUTES.OCENKA_UCHASTOK, link: ROUTES.OCENKA_UCHASTOK,
}, },
{ {
name: 'Оценка гаража', title: 'Оценка гаража',
link: ROUTES.OCENKA_GARAZH, link: ROUTES.OCENKA_GARAZH,
}, },
{ {
name: 'Оценка ущерба недвижимости', title: 'Оценка ущерба недвижимости',
link: ROUTES.OCENKA_USCHERB_NEDVIGI, link: ROUTES.OCENKA_USCHERB_NEDVIGI,
}, },
{ {
name: 'Оценка ущерба от залива', title: 'Оценка ущерба от залива',
link: ROUTES.OCENKA_ZATOPLENIE, link: ROUTES.OCENKA_ZATOPLENIE,
}, },
{ {
name: 'Оценка ущерба от пожара', title: 'Оценка ущерба от пожара',
link: ROUTES.OCENKA_POZHAR, link: ROUTES.OCENKA_POZHAR,
}, },
], ],
}, },
{ {
name: 'Коммерческая недвижимость', title: 'Коммерческая недвижимость',
children: [ children: [
{ {
name: 'Оценка стоимости арендной ставки', title: 'Оценка стоимости арендной ставки',
link: ROUTES.OCENKA_ARENDA, link: ROUTES.OCENKA_ARENDA,
}, },
{ {
name: 'Оценка здания', title: 'Оценка здания',
link: ROUTES.OCENKA_ZDANIE, link: ROUTES.OCENKA_ZDANIE,
}, },
{ {
name: 'Оценка земель', title: 'Оценка земель',
link: ROUTES.OCENKA_ZEMLYA, link: ROUTES.OCENKA_ZEMLYA,
}, },
{ {
name: 'Оценка нежилого помещения', title: 'Оценка нежилого помещения',
link: ROUTES.OCENKA_POMESCHENIE, link: ROUTES.OCENKA_POMESCHENIE,
}, },
{ {
name: 'Оценка офиса', title: 'Оценка офиса',
link: ROUTES.OCENKA_OFFICE, link: ROUTES.OCENKA_OFFICE,
}, },
{ {
name: 'Оценка сооружений', title: 'Оценка сооружений',
link: ROUTES.OCENKA_SOORUZHENIE, link: ROUTES.OCENKA_SOORUZHENIE,
}, },
{ {
name: 'Оценка незавершенного строительства', title: 'Оценка незавершенного строительства',
link: ROUTES.OCENKA_NEZAV_STROIT, link: ROUTES.OCENKA_NEZAV_STROIT,
}, },
], ],
}, },
{ {
name: 'Имущество', title: 'Имущество',
children: [ children: [
{ {
name: 'Оценка недвижимости', title: 'Оценка недвижимости',
link: ROUTES.OCENKA_NEDVIGA, link: ROUTES.OCENKA_NEDVIGA,
}, },
{ {
name: 'Оценка машин и оборудования', title: 'Оценка машин и оборудования',
link: ROUTES.OCENKA_MASHINES, link: ROUTES.OCENKA_MASHINES,
}, },
{ {
name: 'Оценка спецтехники', title: 'Оценка спецтехники',
link: ROUTES.OCENKA_SPECTECHNIKA, link: ROUTES.OCENKA_SPECTECHNIKA,
}, },
{ {
name: 'Оценка нематериальных активов', title: 'Оценка нематериальных активов',
link: ROUTES.OCENKA_ACTIVES, link: ROUTES.OCENKA_ACTIVES,
}, },
{ {
name: 'Оценка стоимости предприятия', title: 'Оценка стоимости предприятия',
link: ROUTES.OCENKA_BUSINESS, link: ROUTES.OCENKA_BUSINESS,
}, },
{ {
name: 'Оценка ценных бумаг', title: 'Оценка ценных бумаг',
link: ROUTES.OCENKA_CENN_BUMAGI, link: ROUTES.OCENKA_CENN_BUMAGI,
}, },
{ {
name: 'Экспертиза и рецензирование отчета об оценке', title: 'Экспертиза и рецензирование отчета об оценке',
link: ROUTES.OCENKA_RECINSIA_OTCHETA, link: ROUTES.OCENKA_RECINSIA_OTCHETA,
}, },
], ],
}, },
{ {
name: 'Ипотека', title: 'Ипотека',
children: [ children: [
{ {
name: 'Оценка для ипотеки в Сбербанке', title: 'Оценка для ипотеки в Сбербанке',
link: ROUTES.OCENKA_IPOTEKA_SBER, link: ROUTES.OCENKA_IPOTEKA_SBER,
}, },
{ {
name: 'Оценка для ипотеки в банке ВТБ', title: 'Оценка для ипотеки в банке ВТБ',
link: ROUTES.OCENKA_IPOTEKA_VTB, link: ROUTES.OCENKA_IPOTEKA_VTB,
}, },
], ],
}, },
{ {
name: 'Транспорт', title: 'Транспорт',
children: [ children: [
{ {
name: 'Оценка ущерба в ДТП', title: 'Оценка ущерба в ДТП',
link: ROUTES.OCENKA_DTP, link: ROUTES.OCENKA_DTP,
}, },
{ {
name: 'Оценка УТС утраты товарной стоимости', title: 'Оценка УТС утраты товарной стоимости',
link: ROUTES.OCENKA_UTS, link: ROUTES.OCENKA_UTS,
}, },
{ {
name: 'Оценка рыночной стоимости автомобиля', title: 'Оценка рыночной стоимости автомобиля',
link: ROUTES.OCENKA_RYNOCHNAYA, link: ROUTES.OCENKA_RYNOCHNAYA,
}, },
{ {
name: 'Оценка автомобиля для суда', title: 'Оценка автомобиля для суда',
link: ROUTES.OCENKA_AUTO_SUD, link: ROUTES.OCENKA_AUTO_SUD,
}, },
{ {
name: 'Оценка мотоциклов и мототехники', title: 'Оценка мотоциклов и мототехники',
link: ROUTES.OCENKA_MOTO, link: ROUTES.OCENKA_MOTO,
}, },
{ {
name: 'Оценка морских и речных судов', title: 'Оценка морских и речных судов',
link: ROUTES.OCENKA_PLAVSREDSTV, link: ROUTES.OCENKA_PLAVSREDSTV,
}, },
{ {
name: 'Оценка воздушных судов и летательных аппаратов', title: 'Оценка воздушных судов и летательных аппаратов',
link: ROUTES.OCENKA_VOZDUSHNYH_SUDOV, link: ROUTES.OCENKA_VOZDUSHNYH_SUDOV,
}, },
], ],
@@ -269,66 +274,66 @@ export const baseMenu: TMenu = [
], ],
}, },
{ {
name: 'Юрист', title: 'Юрист',
link: ROUTES.JURIST, link: ROUTES.JURIST,
children: [ children: [
{ {
name: 'Решение споров', title: 'Решение споров',
children: [ children: [
{ {
name: 'Страховые споры', title: 'Страховые споры',
link: ROUTES.JURIST_STRAHOVKA, link: ROUTES.JURIST_STRAHOVKA,
}, },
{ {
name: 'Земельные и имущественные', title: 'Земельные и имущественные',
link: ROUTES.JURIST_ZEMLYA_IMUSHESTVO, link: ROUTES.JURIST_ZEMLYA_IMUSHESTVO,
}, },
{ {
name: 'Семейные и наследственные', title: 'Семейные и наследственные',
link: ROUTES.JURIST_NASLEDSTVO, link: ROUTES.JURIST_NASLEDSTVO,
}, },
{ {
name: 'Трудовые споры', title: 'Трудовые споры',
link: ROUTES.JURIST_TRUD, link: ROUTES.JURIST_TRUD,
}, },
{ {
name: 'Споры по ДТП', title: 'Споры по ДТП',
link: ROUTES.JURIST_DTP, link: ROUTES.JURIST_DTP,
}, },
], ],
}, },
{ {
name: 'Сопровождение сделок', title: 'Сопровождение сделок',
link: ROUTES.JURIST_SDELKI_DOGOVORA, link: ROUTES.JURIST_SDELKI_DOGOVORA,
}, },
{ {
name: 'Представительство в суде', title: 'Представительство в суде',
link: ROUTES.JURIST_PREDSTAVITELSTVO, link: ROUTES.JURIST_PREDSTAVITELSTVO,
}, },
{ {
name: 'Банкротство физических лиц', title: 'Банкротство физических лиц',
link: ROUTES.JURIST_BANKROTSTVO, link: ROUTES.JURIST_BANKROTSTVO,
}, },
{ {
name: 'Взыскание задолженности', title: 'Взыскание задолженности',
link: ROUTES.JURIST_DOLGI, link: ROUTES.JURIST_DOLGI,
}, },
{ {
name: 'Помощь должникам', title: 'Помощь должникам',
link: ROUTES.JURIST_DOLZHNIKAM, link: ROUTES.JURIST_DOLZHNIKAM,
}, },
{ {
name: 'Возврат страховки и комиссии банков', title: 'Возврат страховки и комиссии банков',
link: ROUTES.JURIST_BANKI, link: ROUTES.JURIST_BANKI,
}, },
], ],
}, },
{ {
name: 'Эксперты', title: 'Эксперты',
link: ROUTES.EXPERTS, link: ROUTES.EXPERTS,
}, },
{ {
name: 'Контакты', title: 'Контакты',
link: ROUTES.CONTACTS, link: ROUTES.CONTACTS,
}, },
]; ];

View File

@@ -1,5 +0,0 @@
export type TMenu = {
name: string;
link?: string;
children?: TMenu;
}[];

View File

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

View 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);
}
}
}

View 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 };

View File

@@ -310,6 +310,38 @@ const MapOutline = (props: SVGProps<SVGSVGElement>) => (
</svg> </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 { export {
Map, Map,
Envelope, Envelope,
@@ -328,4 +360,6 @@ export {
MailBulk, MailBulk,
Consultation, Consultation,
MapOutline, MapOutline,
MobileContact,
MobilePhone,
}; };

View File

@@ -20,6 +20,8 @@ import {
MailBulk, MailBulk,
Consultation, Consultation,
MapOutline, MapOutline,
MobileContact,
MobilePhone,
} from './base'; } from './base';
import { import {
@@ -69,6 +71,8 @@ const Icons = Object.assign(
MailBulk, MailBulk,
Consultation, Consultation,
MapOutline, MapOutline,
MobileContact,
MobilePhone,
}, },
{ {
GridBook, GridBook,

View File

@@ -5,3 +5,4 @@ export * from './modal';
export * from './text-area'; export * from './text-area';
export * from './partners-slider'; export * from './partners-slider';
export * from './icon'; export * from './icon';
export * from './burger';

View File

@@ -1,6 +1,11 @@
.Footer { .Footer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: rem(60px);
@include iflaptop {
margin-bottom: unset;
}
} }
.Container { .Container {
@@ -151,6 +156,11 @@
@include iflaptop { @include iflaptop {
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
gap: rem(10px);
}
@include ifdesktop{
gap: rem(24px);
} }
.IconBox { .IconBox {
@@ -276,7 +286,7 @@
%title { %title {
font-family: $font-roboto; font-family: $font-roboto;
font-weight: 400; font-weight: 400;
font-size: 20px; font-size: rem(20px);
line-height: 100%; line-height: 100%;
color: $color-white; color: $color-white;
margin-bottom: rem(20px); margin-bottom: rem(20px);
@@ -285,7 +295,7 @@
%list { %list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: rem(10px);
list-style: disc; list-style: disc;
margin-left: rem(40px); margin-left: rem(40px);
} }
@@ -307,4 +317,12 @@
line-height: 100%; line-height: 100%;
color: $color-white; color: $color-white;
margin-bottom: rem(20px); margin-bottom: rem(20px);
@include iflaptop{
font-size: rem(14px);
}
@include ifdesktop{
font-size: rem(18px);
}
} }

View File

@@ -1,3 +1,5 @@
export * from './header'; export * from './header';
export * from './footer'; export * from './footer';
export * from './breadcrumbs'; export * from './breadcrumbs';
export * from './mobile-callback';
export * from './sidebar';

View File

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

View 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);
}
}

View 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 };

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 };