Начало вёрстки

This commit is contained in:
neizbejnoezlo 2026-04-27 16:24:05 +07:00
parent 1a4c62c968
commit 7edf6d74be
35 changed files with 2896 additions and 148 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>mdm-front</title> <title>MDM</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

File diff suppressed because it is too large Load Diff

View File

@ -11,14 +11,20 @@
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^4.1.9", "@apollo/client": "^4.1.9",
"@heroui/react": "^3.0.3",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@internationalized/date": "^3.12.1",
"@radix-ui/react-accordion": "^1.2.12",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"date-fns": "^4.1.0",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"echarts-for-react": "^3.0.6", "echarts-for-react": "^3.0.6",
"framer-motion": "^12.38.0",
"graphql": "^16.13.2", "graphql": "^16.13.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^1.9.0", "lucide-react": "^1.9.0",
"react": "^19.2.5", "react": "^19.2.5",
"react-datepicker": "^9.1.0",
"react-dom": "^19.2.5", "react-dom": "^19.2.5",
"react-hook-form": "^7.73.1", "react-hook-form": "^7.73.1",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
@ -32,6 +38,7 @@
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@types/node": "^24.12.2", "@types/node": "^24.12.2",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-datepicker": "^6.2.0",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.1", "eslint": "^10.2.1",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,116 +1,16 @@
import { useState } from 'react' import { useState } from 'react'
import reactLogo from './assets/react.svg' //import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg' //import viteLogo from './assets/vite.svg'
import heroImg from './assets/hero.png' //import heroImg from './assets/hero.png'
import './App.css' import './App.scss'
function App() { function App() {
const [count, setCount] = useState(0) //const [count, setCount] = useState(0)
return ( return (
<> <>
<section id="center"> <section id="center">
<div className="hero">
<img src={heroImg} className="base" width="170" height="179" alt="" />
<img src={reactLogo} className="framework" alt="React logo" />
<img src={viteLogo} className="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
</p>
</div>
<button
type="button"
className="counter"
onClick={() => setCount((count) => count + 1)}
>
Count is {count}
</button>
</section>
<div className="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img className="logo" src={viteLogo} alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://react.dev/" target="_blank">
<img className="button-icon" src={reactLogo} alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section> </section>
<div className="ticks"></div> <div className="ticks"></div>

View File

@ -0,0 +1,12 @@
.app-layout {
display: flex;
min-height: 100vh;
background: #EFF2F7;
}
.app-layout__content {
display: flex;
flex-direction: column;
flex: 1;
padding: 20px 36px 36px 0;
}

View File

@ -0,0 +1,16 @@
import { Outlet } from 'react-router-dom'
import { Sidebar } from '../../widgets/Sidebar/Sidebar'
import './AppLayout.scss'
import { Navbar } from '../../widgets/Navbar/Navbar'
export function AppLayout() {
return (
<div className="app-layout">
<Sidebar />
<main className="app-layout__content">
<Navbar />
<Outlet />
</main>
</div>
)
}

View File

@ -0,0 +1,37 @@
// src/app/router/router.tsx
import { createBrowserRouter, Navigate } from 'react-router-dom'
import { AppLayout } from '../layouts/AppLayout'
import { DevicesPage } from '../../pages/DevicesPage/DevicesPage'
//import { DevicePage } from '../../pages/DevicePage/DevicePage'
import { MapPage } from '../../pages/MapPage/MapPage'
import { EmployeesPage } from '../../pages/EmployeesPage/EmployeesPage'
export const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true,
element: <Navigate to="/devices" replace />,
},
{
path: 'devices',
element: <DevicesPage />,
},/*
{
path: 'devices/:deviceId',
element: <DevicePage />,
}, */
{
path: 'map',
element: <MapPage />,
},
{
path: 'employees',
element: <EmployeesPage />,
},
],
},
])

View File

@ -0,0 +1,26 @@
<svg width="171" height="32" viewBox="0 0 171 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1457_3609" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="32">
<path d="M31.7347 0.457031H0.546509V31.64H31.7347V0.457031Z" fill="white"/>
</mask>
<g mask="url(#mask0_1457_3609)">
<path d="M16.0099 0.451556C12.7558 0.506041 10.1047 3.16577 10.0593 6.42146C10.0122 9.80658 12.7407 12.565 16.1138 12.565C16.2045 12.565 16.2953 12.5632 16.3854 12.5578L16.3841 12.5706C16.9236 12.5952 17.4436 12.6927 17.9347 12.8542C20.2658 13.6194 21.9489 15.8134 21.9489 18.4019C21.9489 19.6456 21.5588 20.7998 20.8941 21.7471C20.8471 21.8166 20.7956 21.8862 20.7443 21.9534C20.6964 22.0162 20.6462 22.0774 20.5966 22.1385C19.5269 23.4221 17.9166 24.2381 16.1138 24.2381C14.313 24.2381 12.7056 23.4233 11.6341 22.1428C11.6341 22.1428 11.6328 22.1403 11.6316 22.1385C11.5301 22.0162 11.4338 21.8904 11.3432 21.7598C11.339 21.7555 11.336 21.7514 11.3335 21.7471C10.2232 20.4078 8.54645 19.5555 6.67117 19.5555C3.29986 19.5555 0.571986 22.3133 0.618506 25.6978C0.662605 28.9232 3.26719 31.571 6.49032 31.6654C8.48714 31.7234 10.2735 30.8141 11.417 29.3733C11.4182 29.3716 11.42 29.3691 11.4212 29.3673C11.4502 29.3285 11.4782 29.2911 11.5077 29.2548C11.5131 29.2474 11.5174 29.2408 11.524 29.2336C12.5913 27.8737 14.2501 27.0002 16.1138 27.0002C17.9776 27.0002 19.6363 27.8737 20.7037 29.2336C20.7103 29.2408 20.7146 29.2474 20.7207 29.2548C20.7478 29.2928 20.7768 29.3303 20.8059 29.366L20.8065 29.3673C21.9156 30.7694 23.6299 31.6677 25.5565 31.6677C28.9007 31.6677 31.6116 28.9565 31.6116 25.6126C31.6116 25.5569 31.6109 25.5018 31.6092 25.4474C31.5862 24.586 31.36 23.7421 30.9786 22.969L30.7257 22.4561L21.6786 4.11676L21.4028 3.55658L21.4016 3.55358C20.3651 1.70221 18.3857 0.450684 16.1136 0.450684C16.0791 0.450684 16.0444 0.450973 16.0099 0.451556Z" fill="url(#paint0_linear_1457_3609)"/>
<path d="M19.2184 18.3998C19.2184 19.0615 19.011 19.675 18.6577 20.1788C18.6325 20.2159 18.6051 20.2529 18.5776 20.2884C18.5526 20.3218 18.5258 20.3544 18.4992 20.387C17.9303 21.0694 17.074 21.5035 16.1155 21.5035C15.1579 21.5035 14.3031 21.0702 13.7335 20.3892L13.732 20.387C13.678 20.3218 13.6268 20.2552 13.5787 20.1854C13.5765 20.1833 13.5749 20.1809 13.5735 20.1788C13.2201 19.675 13.0128 19.0615 13.0128 18.3998C13.0128 16.6852 14.4017 15.2959 16.1155 15.2959C16.1637 15.2959 16.2119 15.2967 16.2593 15.2989C17.9066 15.3744 19.2184 16.7334 19.2184 18.3998Z" fill="#DF5D13"/>
<path d="M30.9785 22.9678C28.9451 19.9999 22.562 13.6726 18.5855 12.9247C18.1655 12.8458 17.7434 12.7773 17.3251 12.689C17.0197 12.6246 16.7054 12.5841 16.3842 12.5692L16.3853 12.5565C16.2953 12.562 16.2046 12.5638 16.1139 12.5638C12.7408 12.5638 10.0122 9.80538 10.0594 6.42023C10.1048 3.16454 12.7558 0.504814 16.0098 0.450329C18.3253 0.411644 20.3493 1.67287 21.4015 3.55235L21.4028 3.55536L21.6785 4.11553L30.7258 22.4548L30.9785 22.9678Z" fill="#315B96"/>
<path d="M20.8059 29.366C20.7769 29.3303 20.7478 29.2928 20.7206 29.2547C20.7146 29.2474 20.7103 29.2407 20.7037 29.2336C19.6363 27.8737 17.9776 27.0001 16.1138 27.0001C14.2501 27.0001 12.5913 27.8737 11.524 29.2336C11.5174 29.2407 11.5131 29.2474 11.5077 29.2547C11.4781 29.2909 11.4502 29.3285 11.4213 29.3672C11.42 29.369 11.4182 29.3715 11.417 29.3733C10.2734 30.8141 8.48714 31.7234 6.49032 31.6652C3.26719 31.571 0.662607 28.9232 0.618509 25.6978C0.571891 22.3133 3.29986 19.5554 6.67117 19.5554C8.54636 19.5554 10.2232 20.4078 11.3335 21.7471C11.336 21.7514 11.339 21.7555 11.3432 21.7598C11.4339 21.8904 11.5301 22.0162 11.6316 22.1385C11.6316 22.1385 12.5352 24.3445 16.115 25.8272C19.6388 27.2869 20.7713 29.3031 20.8059 29.366Z" fill="#DF5D13"/>
</g>
<mask id="mask1_1457_3609" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="44" y="6" width="127" height="20">
<path d="M170.633 6.61426H44.2109V25.4801H170.633V6.61426Z" fill="white"/>
</mask>
<g mask="url(#mask1_1457_3609)">
<path d="M50.9662 7.59399H53.6169L60.5187 25.3656H57.568L55.6925 20.5348H48.8407L46.9902 25.3656H44.0144L50.9662 7.59399ZM55.1424 18.4572L52.2916 10.6727L49.3408 18.4572H55.1424Z" fill="#21477A"/>
<path d="M62.7145 25.3656V7.59399H70.1664C70.9666 7.59399 71.7002 7.76086 72.367 8.0946C73.0338 8.42834 73.6173 8.87055 74.1174 9.42118C74.6176 9.97185 75.0011 10.5893 75.2678 11.2734C75.5345 11.9577 75.6679 12.6585 75.6679 13.376C75.6679 14.3606 75.4428 15.3034 74.9927 16.2044C74.5593 17.0889 73.9341 17.8065 73.1172 18.3571C72.317 18.9078 71.3834 19.1831 70.3165 19.1831H65.5152V25.3656H62.7145ZM65.5152 16.7051H70.1415C70.6749 16.7051 71.1417 16.5632 71.5418 16.2796C71.9419 15.9792 72.2503 15.5787 72.4671 15.0781C72.7004 14.5775 72.8171 14.0102 72.8171 13.376C72.8171 12.7252 72.6838 12.1495 72.417 11.6489C72.1503 11.1484 71.8002 10.7645 71.3667 10.4976C70.95 10.2139 70.4915 10.072 69.9914 10.072H65.5152V16.7051Z" fill="#21477A"/>
<path d="M93.2233 25.3656V12.7002L87.9974 22.3119H86.3469L81.0955 12.7002V25.3656H78.2948V7.59399H81.2955L87.1721 18.4572L93.0486 7.59399H96.0498V25.3656H93.2233Z" fill="#21477A"/>
<path d="M105.18 7.59399H107.831L114.733 25.3656H111.782L109.906 20.5348H103.054L101.204 25.3656H98.228L105.18 7.59399ZM109.356 18.4572L106.506 10.6727L103.555 18.4572H109.356Z" fill="#21477A"/>
</g>
<defs>
<linearGradient id="paint0_linear_1457_3609" x1="22.2884" y1="14.2309" x2="14.1936" y2="24.442" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B4071"/>
<stop offset="1" stop-color="#315B96"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,17 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_1457_3610" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="32">
<path d="M31.1882 0H0V31.183H31.1882V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1457_3610)">
<path d="M15.4633 -0.00547545C12.2093 0.0490096 9.55818 2.70874 9.51282 5.96443C9.46573 9.34955 12.1942 12.1079 15.5673 12.1079C15.658 12.1079 15.7487 12.1061 15.8389 12.1007L15.8376 12.1135C16.3771 12.1382 16.8971 12.2357 17.3882 12.3971C19.7193 13.1624 21.4024 15.3564 21.4024 17.9448C21.4024 19.1885 21.0123 20.3427 20.3476 21.29C20.3005 21.3596 20.2491 21.4292 20.1978 21.4964C20.1498 21.5592 20.0997 21.6204 20.0501 21.6814C18.9804 22.9651 17.37 23.7811 15.5673 23.7811C13.7665 23.7811 12.1591 22.9663 11.0876 21.6857C11.0876 21.6857 11.0863 21.6833 11.0851 21.6814C10.9836 21.5592 10.8873 21.4334 10.7967 21.3027C10.7925 21.2984 10.7895 21.2943 10.787 21.29C9.67669 19.9508 7.99994 19.0985 6.12466 19.0985C2.75335 19.0985 0.0254772 21.8562 0.0719974 25.2407C0.116096 28.4662 2.72069 31.1139 5.94382 31.2084C7.94063 31.2663 9.72698 30.3571 10.8705 28.9163C10.8717 28.9145 10.8735 28.9121 10.8747 28.9102C10.9037 28.8715 10.9317 28.8341 10.9612 28.7978C10.9666 28.7904 10.9709 28.7838 10.9775 28.7766C12.0448 27.4167 13.7036 26.5431 15.5673 26.5431C17.4311 26.5431 19.0898 27.4167 20.1572 28.7766C20.1638 28.7838 20.168 28.7904 20.1742 28.7978C20.2013 28.8358 20.2303 28.8733 20.2594 28.909L20.2599 28.9102C21.3691 30.3124 23.0834 31.2107 25.01 31.2107C28.3542 31.2107 31.0651 28.4994 31.0651 25.1555C31.0651 25.0999 31.0644 25.0448 31.0627 24.9903C31.0397 24.129 30.8135 23.2851 30.4321 22.512L30.1792 21.9991L21.132 3.65973L20.8562 3.09954L20.8551 3.09655C19.8185 1.24518 17.8392 -0.00634766 15.567 -0.00634766C15.5326 -0.00634766 15.4979 -0.00605785 15.4633 -0.00547545Z" fill="url(#paint0_linear_1457_3610)"/>
<path d="M18.6719 17.9428C18.6719 18.6044 18.4645 19.2179 18.1112 19.7218C18.086 19.7588 18.0586 19.7958 18.0311 19.8314C18.0061 19.8648 17.9793 19.8973 17.9527 19.93C17.3838 20.6124 16.5275 21.0465 15.569 21.0465C14.6114 21.0465 13.7566 20.6131 13.187 19.9322L13.1855 19.93C13.1315 19.8648 13.0803 19.7981 13.0322 19.7284C13.03 19.7263 13.0284 19.7239 13.027 19.7218C12.6736 19.2179 12.4663 18.6044 12.4663 17.9428C12.4663 16.2282 13.8552 14.8389 15.569 14.8389C15.6172 14.8389 15.6654 14.8397 15.7127 14.8419C17.3601 14.9174 18.6719 16.2764 18.6719 17.9428Z" fill="#DF5D13"/>
<path d="M30.432 22.5108C28.3986 19.5428 22.0155 13.2156 18.039 12.4677C17.619 12.3887 17.1969 12.3202 16.7786 12.2319C16.4732 12.1676 16.1589 12.127 15.8377 12.1121L15.8388 12.0994C15.7488 12.105 15.6581 12.1068 15.5674 12.1068C12.1943 12.1068 9.46566 9.34835 9.51293 5.9632C9.55829 2.70751 12.2093 0.0477823 15.4633 -0.00670269C17.7788 -0.045387 19.8028 1.21584 20.855 3.09532L20.8563 3.09832L21.132 3.6585L30.1792 21.9978L30.432 22.5108Z" fill="#315B96"/>
<path d="M20.2594 28.9089C20.2304 28.8733 20.2013 28.8358 20.1741 28.7977C20.168 28.7904 20.1638 28.7837 20.1572 28.7766C19.0898 27.4167 17.4311 26.5431 15.5673 26.5431C13.7036 26.5431 12.0448 27.4167 10.9775 28.7766C10.9709 28.7837 10.9666 28.7904 10.9612 28.7977C10.9316 28.8339 10.9037 28.8715 10.8748 28.9101C10.8735 28.912 10.8717 28.9144 10.8705 28.9163C9.72689 30.3571 7.94063 31.2663 5.94382 31.2082C2.72068 31.1139 0.116098 28.4662 0.0719999 25.2407C0.0253818 21.8562 2.75335 19.0984 6.12466 19.0984C7.99985 19.0984 9.67669 19.9508 10.787 21.29C10.7895 21.2943 10.7925 21.2984 10.7967 21.3027C10.8874 21.4334 10.9836 21.5592 11.0851 21.6814C11.0851 21.6814 11.9887 23.8875 15.5685 25.3702C19.0923 26.8299 20.2248 28.846 20.2594 28.9089Z" fill="#DF5D13"/>
</g>
<defs>
<linearGradient id="paint0_linear_1457_3610" x1="21.7419" y1="13.7739" x2="13.6471" y2="23.985" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B4071"/>
<stop offset="1" stop-color="#315B96"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -1,3 +1,13 @@
@use './shared/styles/variables' as *;
@font-face {
font-family: 'Montserrat';
src: url('../assets/fonts/montserrat/Montserrat-VariableFont_wght.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
:root { :root {
--text: #6b6375; --text: #6b6375;
--text-h: #08060d; --text-h: #08060d;
@ -11,9 +21,9 @@
--shadow: --shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif; --sans: 'Montserrat', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif; --heading: 'Montserrat', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;;
--mono: ui-monospace, Consolas, monospace; --mono: 'Montserrat', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;;
font: 18px/145% var(--sans); font: 18px/145% var(--sans);
letter-spacing: 0.18px; letter-spacing: 0.18px;
@ -51,7 +61,7 @@
} }
#root { #root {
width: 1126px; //width: 1126px;
max-width: 100%; max-width: 100%;
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
@ -70,13 +80,12 @@ h1,
h2 { h2 {
font-family: var(--heading); font-family: var(--heading);
font-weight: 500; font-weight: 500;
color: var(--text-h); color: black;
} }
h1 { h1 {
font-size: 56px; font-size: 32px;
letter-spacing: -1.68px; margin: 0;
margin: 32px 0;
@media (max-width: 1024px) { @media (max-width: 1024px) {
font-size: 36px; font-size: 36px;
margin: 20px 0; margin: 20px 0;

View File

@ -1,10 +1,12 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.scss'
import App from './App.tsx' import { RouterProvider } from 'react-router-dom'
import { router } from './app/router/router.tsx'
import 'react-datepicker/dist/react-datepicker.css'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <RouterProvider router={router} />
</StrictMode>, </StrictMode>,
) )

View File

@ -0,0 +1,242 @@
@use '../../shared/styles/variables' as *;
.devices-page {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
}
.devices-table-container {
display: flex;
flex: 1;
flex-direction: column;
//gap: 20px;
}
.devices-table-filter-container {
display: flex;
flex: 1;
}
.devices-table-card {
flex: 1;
overflow: auto;
border-radius: 20px;
background: #ffffff;
}
.devices-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
th {
//height: 36px;
padding: 14px 20px;
line-height: 1;
border-bottom: 1px solid #e3e8f0;
color: $gray50;
font-size: 16px;
font-weight: 450;
text-align: left;
}
td {
height: 68px;
padding: 14px 20px;
border-bottom: 1px solid $gray20;
vertical-align: middle;
text-align: left;
color: #151a24;
font-size: 18px;
font-weight: 400;
}
th:nth-child(1),
td:nth-child(1) {
width: 52px;
}
th:nth-child(2),
td:nth-child(2) {
width: 31%;
}
th:nth-child(3),
td:nth-child(3) {
width: 23%;
}
th:nth-child(4),
td:nth-child(4) {
width: 23%;
}
th:nth-child(5),
td:nth-child(5) {
width: 146px;
}
th:nth-child(6),
td:nth-child(6) {
width: 146px;
}
}
.devices-table__id {
color: #1d2533;
text-align: left;
}
.device-info {
display: flex;
flex-direction: column;
line-height: 1.3;
}
.device-info__number {
color: #111827;
font-size: 18px;
font-weight: 600;
}
.device-info__imei,
.device-info__employee {
color: #738098;
font-size: 18px;
font-weight: 400;
}
.device-info__work {
display: flex;
align-items: center;
gap: 5px;
color: #738098;
font-size: 18px;
font-weight: 400;
}
.devices-status {
display: inline-flex;
align-items: center;
gap: 8px;
color: #111827;
font-size: 18px;
font-weight: 400;
}
.devices-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex: 0 0 7px;
}
.devices-dot--green {
background: $green;
}
.devices-dot--red {
background: $red;
}
.devices-dot--gray {
background: $gray50;
}
.device-icons {
display: grid;
grid-template-columns: repeat(4, 16px);
gap: 4px 8px;
align-items: center;
color: $gray50;
svg {
width: 20px;
height: auto;
color: $gray50;
stroke-width: 2;
}
.is-active {
color: $blue;
}
.is-danger {
color: $red;
}
}
.devices-map-btn {
padding: 12px;
border: none;
border-radius: 12px;
background: $color-bg;
display: inline-flex;
align-items: center;
gap: 8px;
color: #151a24;
font-size: 18px;
font-weight: 500;
cursor: pointer;
transition: .2s ease;
&:hover {
background-color: $gray20;
color: $blue;
}
svg {
height: 18px;
width: auto;
}
}
.devices-pagination {
padding: 12px 14px 0;
display: flex;
align-items: center;
justify-content: space-between;
color: #738098;
font-size: 13px;
}
.devices-pagination__controls {
display: flex;
align-items: center;
gap: 6px;
button {
min-width: 30px;
height: 30px;
padding: 0 12px;
border: none;
border-radius: 10px;
background: #e9edf5;
color: #738098;
font-size: 14px;
cursor: pointer;
&.is-active {
background: #031d9a;
color: #ffffff;
}
&:disabled {
opacity: 0.5;
cursor: default;
}
}
}

View File

@ -0,0 +1,180 @@
import { useState } from 'react'
import {
Bluetooth,
Camera,
Map,
MapPin,
SlidersHorizontal,
Volume2,
Wifi,
Lock,
Store,
} from 'lucide-react'
import devices from './devices.mock.json'
import './DevicesPage.scss'
import { DevicesTabs } from './components/DevicesTabs/DevicesTabs'
import { DevicesToolbar } from './components/DevicesToolbar/DevicesToolbar'
import { DevicesFiltersPanel } from './components/DevicesFiltersPanel/DevicesFiltersPanel'
type DeviceCondition = 'ok' | 'inspection'
type DeviceConnection = 'online' | 'offline' | 'offlineDanger'
type Device = {
id: number
factoryNumber: string
imei: string
workTime: string | null
employee: string | null
condition: DeviceCondition
connection: DeviceConnection
connectionText: string
statusIcons: {
gps: boolean
wifi: boolean
bluetooth: boolean
lock: boolean
camera: boolean
sim: boolean
sound: boolean
kiosk: boolean
}
}
const typedDevices = devices as Device[]
const conditionText: Record<DeviceCondition, string> = {
ok: 'Исправно',
inspection: 'Требует осмотра',
}
function getDotClass(status: DeviceCondition | DeviceConnection) {
if (status === 'ok' || status === 'online') return 'devices-dot devices-dot--green'
if (status === 'inspection' || status === 'offlineDanger') return 'devices-dot devices-dot--red'
return 'devices-dot devices-dot--gray'
}
export function DevicesPage() {
const [isFiltersOpen, setIsFiltersOpen] = useState(true)
return (
<section className="devices-page">
<DevicesTabs />
<DevicesToolbar
isFiltersOpen={isFiltersOpen}
onToggleFilters={() => setIsFiltersOpen((prev) => !prev)}
/>
<div className='devices-table-filter-container'>
<div className='devices-table-container'>
<div className="devices-table-card">
<table className="devices-table">
<thead>
<tr>
<th>ID</th>
<th>Информация</th>
<th>Состояние</th>
<th>Связь</th>
<th>Статусы</th>
<th />
</tr>
</thead>
<tbody>
{typedDevices.map((device) => (
<tr key={device.id}>
<td className="devices-table__id">{device.id}</td>
<td>
<div className="device-info">
<div className="device-info__number">{device.factoryNumber}</div>
<div className="device-info__imei">{device.imei}</div>
{device.workTime && (
<div className="device-info__work">
<span className={getDotClass(device.condition)} />
В работе: {device.workTime}
</div>
)}
{device.employee && (
<div className="device-info__employee">{device.employee}</div>
)}
</div>
</td>
<td>
<div className="devices-status">
<span className={getDotClass(device.condition)} />
{conditionText[device.condition]}
</div>
</td>
<td>
<div className="devices-status">
<span className={getDotClass(device.connection)} />
{device.connectionText}
</div>
</td>
<td>
<div className="device-icons">
<MapPin className={device.statusIcons.gps ? 'is-active' : ''} size={16} />
<Wifi className={device.statusIcons.wifi ? 'is-active' : ''} size={16} />
<Bluetooth
className={device.statusIcons.bluetooth ? 'is-active' : ''}
size={16}
/>
<Lock
className={
device.statusIcons.lock ? 'is-danger is-active' : ''
}
size={16}
/>
<Camera className={device.statusIcons.camera ? 'is-active' : ''} size={16} />
<SlidersHorizontal className={device.statusIcons.sim ? 'is-active' : ''} size={16} />
<Volume2 className={device.statusIcons.sound ? 'is-active' : ''} size={16} />
<Store
className={
device.statusIcons.kiosk ? 'is-active' : ''
}
size={16}
/>
</div>
</td>
<td>
<button className="devices-map-btn" type="button">
<Map size={15} />
На карте
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="devices-pagination">
<span>1 из 10</span>
<div className="devices-pagination__controls">
<button type="button" disabled>
Назад
</button>
<button className="is-active" type="button">
1
</button>
<button type="button">2</button>
<button type="button">Вперед</button>
</div>
</div>
</div>
<DevicesFiltersPanel isOpen={isFiltersOpen} />
</div>
</section>
)
}

View File

@ -0,0 +1,43 @@
.react-datepicker {
border: none;
border-radius: 14px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.15);
font-family: inherit;
overflow: hidden;
}
.react-datepicker__header {
background: #f3f6fa;
border-bottom: 1px solid #e3e8f0;
}
.react-datepicker__current-month,
.react-datepicker-time__header,
.react-datepicker-year-header {
color: #30394b;
font-weight: 600;
}
.react-datepicker__day--selected,
.react-datepicker__day--keyboard-selected,
.react-datepicker__time-container
.react-datepicker__time
.react-datepicker__time-box
ul.react-datepicker__time-list
li.react-datepicker__time-list-item--selected {
background: #031d9a;
color: #ffffff;
}
.react-datepicker__day--in-range,
.react-datepicker__day--in-selecting-range {
background: #e8edff;
color: #031d9a;
}
.react-datepicker__day:hover,
.react-datepicker__month-text:hover,
.react-datepicker__quarter-text:hover,
.react-datepicker__year-text:hover {
background: #eef1f6;
}

View File

@ -0,0 +1,258 @@
@use '../../../../shared/styles/variables' as *;
.devices-filters {
width: 0;
flex: 0 0 0;
opacity: 0;
overflow: hidden;
pointer-events: none;
transform: translateX(16px);
transition:
width 0.25s ease,
flex-basis 0.25s ease,
opacity 0.2s ease,
transform 0.25s ease;
}
.devices-filters--open {
width: 292px;
flex-basis: 292px;
opacity: 1;
pointer-events: auto;
transform: translateX(0);
padding-left: 20px;
}
.devices-filters__accordion {
width: 292px;
display: flex;
flex-direction: column;
gap: 8px;
}
.devices-filter-item {
border-radius: 14px;
background: #ffffff;
overflow: hidden;
}
.devices-filter-item__header {
margin: 0;
}
.devices-filter-item__trigger {
width: 100%;
min-height: 44px;
padding: 0 14px;
border: none;
border-bottom: 1px solid transparent;
background: transparent;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: #30394b;
font-size: 15px;
font-weight: 500;
cursor: pointer;
&[data-state='open'] {
color: #031d9a;
border-bottom-color: #e3e8f0;
}
}
.devices-filter-item__chevron {
flex: 0 0 auto;
transition: transform 0.2s ease;
}
.devices-filter-item__trigger[data-state='open'] .devices-filter-item__chevron {
transform: rotate(180deg);
}
.devices-filter-item__content {
overflow: hidden;
&[data-state='open'] {
animation: filterSlideDown 0.2s ease;
}
&[data-state='closed'] {
animation: filterSlideUp 0.2s ease;
}
}
.devices-filter-item__inner {
padding: 12px 14px 10px;
}
.devices-period {
display: flex;
align-items: center;
gap: 6px;
}
.devices-period__divider {
width: 12px;
height: 1px;
background: #8c96aa;
flex: 0 0 12px;
}
.devices-period__input {
width: 126px;
min-height: 28px;
padding: 0 10px;
border: none;
outline: none;
border-radius: 999px;
background: #eef1f6;
color: #4f5b73;
font-size: 12px;
font-weight: 400;
cursor: pointer;
}
.devices-filter-reset {
margin: 8px 0 0 auto;
padding: 0;
display: block;
border: none;
background: transparent;
color: #031d9a;
font-size: 12px;
font-weight: 400;
text-decoration: underline;
cursor: pointer;
}
.devices-filter-row {
display: grid;
grid-template-columns: 1fr auto auto;
align-items: center;
gap: 12px;
min-height: 28px;
color: #30394b;
font-size: 13px;
& + & {
margin-top: 8px;
}
}
.devices-filter-row__label {
color: #30394b;
font-size: 13px;
font-weight: 400;
}
.devices-radio,
.devices-checkbox {
display: inline-flex;
align-items: center;
gap: 6px;
color: #30394b;
font-size: 13px;
cursor: pointer;
user-select: none;
input {
display: none;
}
}
.devices-radio__control {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid #dfe5ef;
background: #f3f6fa;
position: relative;
flex: 0 0 18px;
}
.devices-radio input:checked + .devices-radio__control {
border-color: #031d9a;
background: #031d9a;
}
.devices-radio input:checked + .devices-radio__control::after {
content: '';
position: absolute;
inset: 5px;
border-radius: 50%;
background: #ffffff;
}
.devices-radio__label {
white-space: nowrap;
}
.devices-checkbox {
min-height: 28px;
& + & {
margin-top: 8px;
}
}
.devices-checkbox__control {
width: 18px;
height: 18px;
border-radius: 5px;
border: 2px solid #dfe5ef;
background: #f3f6fa;
position: relative;
flex: 0 0 18px;
}
.devices-checkbox input:checked + .devices-checkbox__control {
border-color: #031d9a;
background: #031d9a;
}
.devices-checkbox input:checked + .devices-checkbox__control::after {
content: '';
position: absolute;
left: 5px;
top: 2px;
width: 4px;
height: 8px;
border: solid #ffffff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
@keyframes filterSlideDown {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes filterSlideUp {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}

View File

@ -0,0 +1,234 @@
import { useState } from 'react'
import DatePicker, { registerLocale } from 'react-datepicker'
import { ru } from 'date-fns/locale/ru'
import * as Accordion from '@radix-ui/react-accordion'
import { ChevronDown } from 'lucide-react'
import './Datepicker.scss'
import './DevicesFiltersPanel.scss'
registerLocale('ru', ru)
type DevicesFiltersPanelProps = {
isOpen: boolean
}
export function DevicesFiltersPanel({ isOpen }: DevicesFiltersPanelProps) {
const [startDate, setStartDate] = useState<Date | null>(
new Date(2026, 3, 20, 7, 0),
)
const [endDate, setEndDate] = useState<Date | null>(
new Date(2026, 3, 22, 16, 0),
)
return (
<aside className={`devices-filters ${isOpen ? 'devices-filters--open' : ''}`}>
<Accordion.Root
className="devices-filters__accordion"
type="multiple"
defaultValue={['work-period', 'statuses']}
>
<Accordion.Item className="devices-filter-item" value="work-period">
<Accordion.Header className="devices-filter-item__header">
<Accordion.Trigger className="devices-filter-item__trigger">
<span>Период работы</span>
<ChevronDown className="devices-filter-item__chevron" size={16} />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className="devices-filter-item__content">
<div className="devices-filter-item__inner">
<div className="devices-period">
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
selectsStart
startDate={startDate}
endDate={endDate}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="dd.MM.yyyy, HH:mm"
locale="ru"
placeholderText="Дата начала"
className="devices-period__input"
/>
<span className="devices-period__divider" />
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
selectsEnd
startDate={startDate}
endDate={endDate}
minDate={startDate ?? undefined}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="dd.MM.yyyy, HH:mm"
locale="ru"
placeholderText="Дата окончания"
className="devices-period__input"
/>
</div>
<button
className="devices-filter-reset"
type="button"
onClick={() => {
setStartDate(null)
setEndDate(null)
}}
>
Сбросить
</button>
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className="devices-filter-item" value="statuses">
<Accordion.Header className="devices-filter-item__header">
<Accordion.Trigger className="devices-filter-item__trigger">
<span>Статусы</span>
<ChevronDown className="devices-filter-item__chevron" size={16} />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className="devices-filter-item__content">
<div className="devices-filter-item__inner">
<div className="devices-filter-row">
<span className="devices-filter-row__label">GPS</span>
<label className="devices-radio">
<input type="radio" name="gps" />
<span className="devices-radio__control" />
<span className="devices-radio__label">Вкл</span>
</label>
<label className="devices-radio">
<input type="radio" name="gps" defaultChecked />
<span className="devices-radio__control" />
<span className="devices-radio__label">Выкл</span>
</label>
</div>
<div className="devices-filter-row">
<span className="devices-filter-row__label">Wi-Fi</span>
<label className="devices-radio">
<input type="radio" name="wifi" defaultChecked />
<span className="devices-radio__control" />
<span className="devices-radio__label">Вкл</span>
</label>
<label className="devices-radio">
<input type="radio" name="wifi" />
<span className="devices-radio__control" />
<span className="devices-radio__label">Выкл</span>
</label>
</div>
<div className="devices-filter-row">
<span className="devices-filter-row__label">Камера</span>
<label className="devices-radio">
<input type="radio" name="camera" />
<span className="devices-radio__control" />
<span className="devices-radio__label">Вкл</span>
</label>
<label className="devices-radio">
<input type="radio" name="camera" />
<span className="devices-radio__control" />
<span className="devices-radio__label">Выкл</span>
</label>
</div>
<button className="devices-filter-reset" type="button">
Сбросить
</button>
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className="devices-filter-item" value="network">
<Accordion.Header className="devices-filter-item__header">
<Accordion.Trigger className="devices-filter-item__trigger">
<span>Сеть</span>
<ChevronDown className="devices-filter-item__chevron" size={16} />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className="devices-filter-item__content">
<div className="devices-filter-item__inner">
<label className="devices-checkbox">
<input type="checkbox" defaultChecked />
<span className="devices-checkbox__control" />
<span>В сети</span>
</label>
<label className="devices-checkbox">
<input type="checkbox" />
<span className="devices-checkbox__control" />
<span>Не в сети</span>
</label>
<label className="devices-checkbox">
<input type="checkbox" />
<span className="devices-checkbox__control" />
<span>Долго не в сети</span>
</label>
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className="devices-filter-item" value="condition">
<Accordion.Header className="devices-filter-item__header">
<Accordion.Trigger className="devices-filter-item__trigger">
<span>Состояние</span>
<ChevronDown className="devices-filter-item__chevron" size={16} />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className="devices-filter-item__content">
<div className="devices-filter-item__inner">
<label className="devices-checkbox">
<input type="checkbox" defaultChecked />
<span className="devices-checkbox__control" />
<span>Исправно</span>
</label>
<label className="devices-checkbox">
<input type="checkbox" />
<span className="devices-checkbox__control" />
<span>Требует осмотра</span>
</label>
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className="devices-filter-item" value="storage">
<Accordion.Header className="devices-filter-item__header">
<Accordion.Trigger className="devices-filter-item__trigger">
<span>Хранилище</span>
<ChevronDown className="devices-filter-item__chevron" size={16} />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className="devices-filter-item__content">
<div className="devices-filter-item__inner">
<label className="devices-checkbox">
<input type="checkbox" />
<span className="devices-checkbox__control" />
<span>Заполнено больше 80%</span>
</label>
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</aside>
)
}

View File

@ -0,0 +1,39 @@
@use '../../../../shared/styles/variables' as *;
.devices-tabs {
display: flex;
align-items: center;
gap: 10px;
}
.devices-tabs__item {
position: relative;
min-height: 36px;
padding: 8px 18px;
border: none;
border-radius: 10px;
background: #ffffff;
color: #4f5b73;
font-size: 16px;
font-weight: 400;
cursor: pointer;
b {
font-weight: 700;
color: #1d2533;
}
}
.devices-tabs__item--active {
color: #4f5b73;
}
.devices-tabs__alert {
position: absolute;
top: -5px;
right: -5px;
width: 11px;
height: 11px;
border-radius: 50%;
background: $red;
}

View File

@ -0,0 +1,21 @@
import './DevicesTabs.scss'
export function DevicesTabs() {
return (
<div className="devices-tabs">
<button className="devices-tabs__item devices-tabs__item--active" type="button">
В работе: <b>15 из 77</b>
</button>
<button className="devices-tabs__item" type="button">
Долгое время не в сети: <b>8</b>
<span className="devices-tabs__alert" />
</button>
<button className="devices-tabs__item" type="button">
Требуют обслуживания: <b>9</b>
<span className="devices-tabs__alert" />
</button>
</div>
)
}

View File

@ -0,0 +1,99 @@
@use '../../../../shared/styles/variables' as *;
.devices-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
background-color: white;
border-radius: 100px;
padding: 4px;
}
.devices-search {
width: 250px;
padding: 10px 12px;
border-radius: 20px;
background: $color-bg;
display: flex;
align-items: center;
gap: 8px;
color: $gray50;
svg {
height: 16px;
width: auto;
}
input {
width: 100%;
border: none;
outline: none;
background: transparent;
color: black;
font-size: 16px;
&::placeholder {
color: $gray50;
}
}
}
.devices-toolbar__right {
display: flex;
align-items: center;
gap: 6px;
button {
padding: 12px;
font-size: 16px;
background-color: $color-bg;
border-radius: 20px;
border: none;
}
.add-device {
background-color: $blue;
color: white;
}
}
.devices-sort {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
background: #ffffff;
color: $gray50;
font-size: 16px;
cursor: pointer;
}
.devices-filter {
position: relative;
border: none;
border-radius: 20px;
color: $blue;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
svg {
height: 16px;
width: auto;
}
}
.devices-filter__alert {
position: absolute;
top: -2px;
right: -1px;
width: 8px;
height: 8px;
border-radius: 50%;
background: $blue;
}

View File

@ -0,0 +1,38 @@
import { Search, SlidersHorizontal } from 'lucide-react'
import './DevicesToolbar.scss'
type DevicesToolbarProps = {
isFiltersOpen: boolean
onToggleFilters: () => void
}
export function DevicesToolbar({ isFiltersOpen, onToggleFilters }: DevicesToolbarProps) {
return (
<div className="devices-toolbar">
<label className="devices-search">
<Search size={16} />
<input type="text" placeholder="Поиск" />
</label>
<div className="devices-toolbar__right">
<button className='add-device'>Добавить устройство</button>
<button className="devices-sort" type="button">
По умолчанию
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.4249 4.0835L7.17063 8.33775L2.91638 4.0835L2.09212 4.90862L7.17063 9.98712L12.25 4.90862L11.4249 4.0835Z" fill="currentColor" />
</svg>
</button>
<button
className={`devices-filter ${isFiltersOpen ? 'devices-filter--active' : ''}`}
type="button"
aria-label="Фильтр"
onClick={onToggleFilters}
>
<SlidersHorizontal size={16} />
<span className="devices-filter__alert" />
</button>
</div>
</div>
)
}

View File

@ -0,0 +1,102 @@
[
{
"id": 1,
"factoryNumber": "Заводской номер",
"imei": "IMEI устройства",
"workTime": "4 ч 15 мин",
"employee": "Иванов Иван Иванович",
"condition": "ok",
"connection": "online",
"connectionText": "В сети",
"statusIcons": {
"gps": true,
"wifi": true,
"bluetooth": true,
"lock": false,
"camera": true,
"sim": true,
"sound": false,
"kiosk": false
}
},
{
"id": 2,
"factoryNumber": "Заводской номер",
"imei": "IMEI устройства",
"workTime": null,
"employee": null,
"condition": "inspection",
"connection": "offline",
"connectionText": "Не в сети",
"statusIcons": {
"gps": true,
"wifi": true,
"bluetooth": true,
"lock": false,
"camera": true,
"sim": true,
"sound": false,
"kiosk": false
}
},
{
"id": 3,
"factoryNumber": "Заводской номер",
"imei": "IMEI устройства",
"workTime": "14 ч 15 мин",
"employee": "Иванов Иван Иванович",
"condition": "inspection",
"connection": "offlineDanger",
"connectionText": "Не в сети 10 ч 5 мин",
"statusIcons": {
"gps": true,
"wifi": true,
"bluetooth": true,
"lock": false,
"camera": true,
"sim": true,
"sound": false,
"kiosk": false
}
},
{
"id": 4,
"factoryNumber": "Заводской номер",
"imei": "IMEI устройства",
"workTime": null,
"employee": null,
"condition": "ok",
"connection": "online",
"connectionText": "В сети",
"statusIcons": {
"gps": true,
"wifi": true,
"bluetooth": true,
"lock": false,
"camera": true,
"sim": true,
"sound": false,
"kiosk": false
}
},
{
"id": 5,
"factoryNumber": "Заводской номер",
"imei": "IMEI устройства",
"workTime": null,
"employee": null,
"condition": "ok",
"connection": "offline",
"connectionText": "Не в сети",
"statusIcons": {
"gps": true,
"wifi": false,
"bluetooth": false,
"lock": true,
"camera": false,
"sim": true,
"sound": false,
"kiosk": true
}
}
]

View File

@ -0,0 +1,3 @@
export function EmployeesPage() {
return <h1>Сотрудники</h1>
}

View File

@ -0,0 +1,3 @@
export function MapPage() {
return <h1>Карта</h1>
}

View File

@ -0,0 +1,11 @@
// colors light
$color-bg: #EFF2F7;
$gray50: #6A768E;
$gray30: hsla(219, 32%, 76%, 0.2);
$gray20: #E2E7F0;
$color-text: #2F3747;
$red: #DD0000;
$red20: hsla(0, 100%, 43%, 0.2);
$green: #33B343;
$blue: #031D9A;
$blue20: hsla(230, 96%, 31%, 0.2);

View File

@ -0,0 +1,9 @@
@use '../../shared/styles/variables' as *;
.navbar{
display: flex;
flex-direction: row;
gap: 100px;
justify-content: space-between;
padding-bottom: 26px;
}

View File

@ -0,0 +1,25 @@
import { useLocation } from 'react-router-dom'
import './Navbar.scss'
const pageTitles: Record<string, string> = {
'/devices': 'Устройства',
'/employees': 'Сотрудники',
'/map': 'Карта',
}
export function Navbar() {
const location = useLocation()
const title = pageTitles[location.pathname] ?? ''
return (
<header className="navbar">
<div>
<h1>{title}</h1>
</div>
<div className='profile'>
<span>Администратор</span>
</div>
</header>
)
}

View File

@ -0,0 +1,191 @@
@use '../../shared/styles/variables' as *;
.sidebar {
width: 171px;
min-height: calc(100vh - 56px);
max-height: 100vh;
padding: 20px 20px 36px 20px;
background: transparent;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 20px;
overflow: hidden;
transition:
width 0.25s ease,
padding 0.25s ease;
.wrap-btn {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 8px;
border-radius: 12px;
padding: 12px;
font-size: 20px;
line-height: 1;
cursor: pointer;
background-color: transparent;
color: $gray50;
font-weight: 500;
border: none;
transition:
background-color 0.2s ease,
color 0.2s ease;
&:hover {
background-color: $gray30;
}
svg {
height: 20px;
width: 20px;
flex: 0 0 20px;
}
}
}
.sidebar__top {
display: flex;
flex-direction: column;
}
.sidebar__logo {
margin-bottom: 20px;
display: flex;
align-items: center;
min-height: 32px;
img {
height: 32px;
width: auto;
display: block;
}
}
.sidebar__nav {
display: flex;
flex-direction: column;
gap: 4px;
}
.sidebar__link {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 8px;
padding: 12px;
border-radius: 12px;
color: #6a768e;
text-decoration: none;
font-size: 20px;
transition:
background-color 0.2s ease,
color 0.2s ease;
font-weight: 500;
line-height: 1;
svg {
height: 20px;
width: 20px;
flex: 0 0 20px;
}
&:hover {
background-color: $gray30;
}
}
.sidebar__icon {
width: 20px;
height: 20px;
flex: 0 0 20px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.sidebar__label {
white-space: nowrap;
overflow: hidden;
transition:
width 0.2s ease,
opacity 0.2s ease;
}
.sidebar__link--active {
background: white;
color: #031d9a;
&:hover {
background-color: white;
}
}
.wrap-btn__arrow {
transition: transform 0.25s ease;
}
.wrap-btn__text {
white-space: nowrap;
overflow: hidden;
transition:
width 0.2s ease,
opacity 0.2s ease;
}
/* Свернутое состояние */
.sidebar--collapsed {
width: 44px;
padding: 20px 20px 36px 20px;
.sidebar__logo {
justify-content: center;
}
.sidebar__nav {
//align-items: center;
}
.sidebar__link {
//width: 44px;
//height: 44px;
justify-content: center;
padding: 12px;
gap: 0;
}
.sidebar__label {
width: 0;
opacity: 0;
}
.wrap-btn {
width: 44px;
height: 44px;
justify-content: center;
padding: 12px;
gap: 0;
}
.wrap-btn__text {
width: 0;
opacity: 0;
}
.wrap-btn__arrow {
transform: rotate(180deg);
}
}

View File

@ -0,0 +1,75 @@
import { useState } from 'react'
import { NavLink } from 'react-router-dom'
import './Sidebar.scss'
import Logo from '../../assets/Logo.svg'
import LogoCompact from '../../assets/LogoIcon.svg'
const links = [
{
to: '/devices',
label: 'Устройства',
icon:
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33337 8.33341C3.33337 5.19091 3.33337 3.61925 4.31004 2.64341C5.28671 1.66758 6.85754 1.66675 10 1.66675C13.1425 1.66675 14.7142 1.66675 15.69 2.64341C16.6659 3.62008 16.6667 5.19091 16.6667 8.33341V11.6667C16.6667 14.8092 16.6667 16.3809 15.69 17.3567C14.7134 18.3326 13.1425 18.3334 10 18.3334C6.85754 18.3334 5.28587 18.3334 4.31004 17.3567C3.33421 16.3801 3.33337 14.8092 3.33337 11.6667V8.33341Z" stroke="currentColor" stroke-width="1.5" />
<path d="M12.5 15.8335H7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
},
{
to: '/employees',
label: 'Сотрудники',
icon:
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 12.1668C20 12.6668 19 13.0002 17.8333 13.1668C17.0833 11.7502 15.5833 10.6668 13.8333 9.91683C14 9.66683 14.1667 9.50016 14.3333 9.25016H15C17.5833 9.16683 20 10.7502 20 12.1668ZM5.66667 9.16683H5C2.41667 9.16683 0 10.7502 0 12.1668C0 12.6668 1 13.0002 2.16667 13.1668C2.91667 11.7502 4.41667 10.6668 6.16667 9.91683L5.66667 9.16683ZM10 10.0002C11.8333 10.0002 13.3333 8.50016 13.3333 6.66683C13.3333 4.8335 11.8333 3.3335 10 3.3335C8.16667 3.3335 6.66667 4.8335 6.66667 6.66683C6.66667 8.50016 8.16667 10.0002 10 10.0002ZM10 10.8335C6.58333 10.8335 3.33333 13.0002 3.33333 15.0002C3.33333 16.6668 10 16.6668 10 16.6668C10 16.6668 16.6667 16.6668 16.6667 15.0002C16.6667 13.0002 13.4167 10.8335 10 10.8335ZM14.75 8.3335H15C16.4167 8.3335 17.5 7.25016 17.5 5.8335C17.5 4.41683 16.4167 3.3335 15 3.3335C14.5833 3.3335 14.25 3.41683 13.9167 3.5835C14.5833 4.41683 15 5.50016 15 6.66683C15 7.25016 14.9167 7.8335 14.75 8.3335ZM5 8.3335H5.25C5.08333 7.8335 5 7.25016 5 6.66683C5 5.50016 5.41667 4.41683 6.08333 3.5835C5.75 3.41683 5.41667 3.3335 5 3.3335C3.58333 3.3335 2.5 4.41683 2.5 5.8335C2.5 7.25016 3.58333 8.3335 5 8.3335Z" fill="currentColor" />
</svg>
},
{
to: '/map',
label: 'Карта',
icon:
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3333 8.74981V8.09731C18.3333 6.48064 18.3333 5.67314 17.845 5.17064C17.3566 4.66898 16.5716 4.66898 15 4.66898H13.2675C12.5033 4.66898 12.4966 4.66731 11.8091 4.32314L9.03329 2.93398C7.87413 2.35398 7.29413 2.06398 6.67663 2.08398C6.05913 2.10398 5.49996 2.43148 4.37746 3.08648L3.35496 3.68314C2.53079 4.16398 2.11913 4.40481 1.89329 4.80481C1.66663 5.20481 1.66663 5.69148 1.66663 6.66564V13.5131C1.66663 14.7923 1.66663 15.4323 1.95163 15.7881C2.14163 16.0256 2.40746 16.1848 2.70163 16.2381C3.14329 16.3173 3.68496 16.0015 4.76663 15.3698C5.50163 14.9406 6.20829 14.4948 7.08746 14.6156C7.82413 14.7173 8.50829 15.1823 9.16663 15.5115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M6.66663 2.08301V14.583" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
<path d="M12.5 4.58301V7.91634" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M14.5833 10C16.6124 10 18.3333 11.6808 18.3333 13.7192C18.3333 15.79 16.5849 17.2442 14.9699 18.2317C14.8521 18.2984 14.7191 18.3335 14.5837 18.3335C14.4483 18.3335 14.3152 18.2984 14.1974 18.2317C12.5849 17.2342 10.8333 15.7975 10.8333 13.7192C10.8333 11.68 12.5541 10 14.5833 10Z" stroke="currentColor" stroke-width="1.5" />
<path d="M14.5833 13.75H14.5908" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
},
]
export function Sidebar() {
const [collapsed, setCollapsed] = useState(false)
return (
<aside className={`sidebar ${collapsed ? 'sidebar--collapsed' : ''}`}>
<div>
<div className="sidebar__logo">
<img src={collapsed ? LogoCompact : Logo} alt='Лого' />
</div>
<nav className="sidebar__nav">
{links.map((link) => (
<NavLink
key={link.to}
to={link.to}
title={collapsed ? link.label : ''}
className={({ isActive }) =>
isActive ? 'sidebar__link sidebar__link--active' : 'sidebar__link'
}
>
<span className="sidebar__icon">{link.icon}</span>
<span className="sidebar__label">{link.label}</span>
</NavLink>
))}
</nav>
</div>
<button className='wrap-btn' type='button' onClick={() => setCollapsed((prev) => !prev)}>
<svg className={`wrap-btn__arrow ${collapsed ? 'wrap-btn__arrow--collapsed' : ''}`} width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.91637 11.4249L5.66212 7.17063L9.91637 2.91638L9.09125 2.09212L4.01275 7.17063L9.09125 12.25L9.91637 11.4249Z" fill="currentColor"/>
</svg>
{collapsed ? '' :
<span className="wrap-btn__text">Свернуть</span>
}
</button>
</aside>
)
}