added route preview
24
package.json
@ -6,37 +6,55 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.8.2",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@mui/icons-material": "^6.1.6",
|
||||
"@mui/lab": "^6.0.0-beta.14",
|
||||
"@mui/material": "^6.1.7",
|
||||
"@mui/x-data-grid": "^7.22.2",
|
||||
"@react-three/drei": "^10.0.6",
|
||||
"@react-three/fiber": "^9.1.2",
|
||||
"@refinedev/cli": "^2.16.21",
|
||||
"@refinedev/core": "^4.47.1",
|
||||
"@refinedev/core": "^4.57.9",
|
||||
"@refinedev/devtools": "^1.1.32",
|
||||
"@refinedev/kbar": "^1.3.6",
|
||||
"@refinedev/kbar": "^1.3.16",
|
||||
"@refinedev/mui": "^6.0.0",
|
||||
"@refinedev/react-hook-form": "^4.8.14",
|
||||
"@refinedev/react-router": "^1.0.0",
|
||||
"@refinedev/simple-rest": "^5.0.1",
|
||||
"@tanstack/react-query": "^5.74.3",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-simple-maps": "^3.0.6",
|
||||
"axios": "^1.7.9",
|
||||
"classnames": "^2.5.1",
|
||||
"d3-geo": "^3.1.1",
|
||||
"easymde": "^2.19.0",
|
||||
"i18next": "^24.2.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react": "^18.0.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hook-form": "^7.30.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-intl": "^7.1.10",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-sphere-viewer": "^6.2.2",
|
||||
"react-router": "^7.0.2",
|
||||
"react-simplemde-editor": "^5.2.0"
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"react-simplemde-editor": "^5.2.0",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"three": "^0.175.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3-geo": "^3.1.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^18.16.2",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/three": "^0.175.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
|
2664
pnpm-lock.yaml
758
src/App.tsx
@ -1,375 +1,427 @@
|
||||
import {Refine, Authenticated} from '@refinedev/core'
|
||||
import {DevtoolsPanel, DevtoolsProvider} from '@refinedev/devtools'
|
||||
import {RefineKbar, RefineKbarProvider} from '@refinedev/kbar'
|
||||
import { Refine, Authenticated } from "@refinedev/core";
|
||||
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
|
||||
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
|
||||
|
||||
import {ErrorComponent, useNotificationProvider, RefineSnackbarProvider, ThemedLayoutV2} from '@refinedev/mui'
|
||||
import {
|
||||
ErrorComponent,
|
||||
useNotificationProvider,
|
||||
RefineSnackbarProvider,
|
||||
ThemedLayoutV2,
|
||||
} from "@refinedev/mui";
|
||||
|
||||
import {customDataProvider} from './providers/data'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import GlobalStyles from '@mui/material/GlobalStyles'
|
||||
import {BrowserRouter, Route, Routes, Outlet} from 'react-router'
|
||||
import routerBindings, {NavigateToResource, CatchAllNavigate, UnsavedChangesNotifier, DocumentTitleHandler} from '@refinedev/react-router'
|
||||
import {ColorModeContextProvider} from './contexts/color-mode'
|
||||
import {Header} from './components/header'
|
||||
import {Login} from './pages/login'
|
||||
import {authProvider} from './authProvider'
|
||||
import {i18nProvider} from './i18nProvider'
|
||||
import { customDataProvider } from "./providers/data";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import GlobalStyles from "@mui/material/GlobalStyles";
|
||||
import { BrowserRouter, Route, Routes, Outlet } from "react-router";
|
||||
import routerBindings, {
|
||||
NavigateToResource,
|
||||
CatchAllNavigate,
|
||||
UnsavedChangesNotifier,
|
||||
DocumentTitleHandler,
|
||||
} from "@refinedev/react-router";
|
||||
import { ColorModeContextProvider } from "./contexts/color-mode";
|
||||
import { Header } from "./components/header";
|
||||
import { Login } from "./pages/login";
|
||||
import { authProvider } from "./authProvider";
|
||||
import { i18nProvider } from "./i18nProvider";
|
||||
|
||||
import {CountryList, CountryCreate, CountryEdit, CountryShow} from './pages/country'
|
||||
import {CityList, CityCreate, CityEdit, CityShow} from './pages/city'
|
||||
import {CarrierList, CarrierCreate, CarrierEdit, CarrierShow} from './pages/carrier'
|
||||
import {MediaList, MediaCreate, MediaEdit, MediaShow} from './pages/media'
|
||||
import {ArticleList, ArticleCreate, ArticleEdit, ArticleShow} from './pages/article'
|
||||
import {SightList, SightCreate, SightEdit, SightShow} from './pages/sight'
|
||||
import {StationList, StationCreate, StationEdit, StationShow} from './pages/station'
|
||||
import {VehicleList, VehicleCreate, VehicleEdit, VehicleShow} from './pages/vehicle'
|
||||
import {RouteList, RouteCreate, RouteEdit, RouteShow} from './pages/route'
|
||||
import {UserList, UserCreate, UserEdit, UserShow} from './pages/user'
|
||||
import {
|
||||
CountryList,
|
||||
CountryCreate,
|
||||
CountryEdit,
|
||||
CountryShow,
|
||||
} from "./pages/country";
|
||||
import { CityList, CityCreate, CityEdit, CityShow } from "./pages/city";
|
||||
import {
|
||||
CarrierList,
|
||||
CarrierCreate,
|
||||
CarrierEdit,
|
||||
CarrierShow,
|
||||
} from "./pages/carrier";
|
||||
import { MediaList, MediaCreate, MediaEdit, MediaShow } from "./pages/media";
|
||||
import {
|
||||
ArticleList,
|
||||
ArticleCreate,
|
||||
ArticleEdit,
|
||||
ArticleShow,
|
||||
} from "./pages/article";
|
||||
import { SightList, SightCreate, SightEdit, SightShow } from "./pages/sight";
|
||||
import {
|
||||
StationList,
|
||||
StationCreate,
|
||||
StationEdit,
|
||||
StationShow,
|
||||
} from "./pages/station";
|
||||
import {
|
||||
VehicleList,
|
||||
VehicleCreate,
|
||||
VehicleEdit,
|
||||
VehicleShow,
|
||||
} from "./pages/vehicle";
|
||||
import { RouteList, RouteCreate, RouteEdit, RouteShow } from "./pages/route";
|
||||
import { UserList, UserCreate, UserEdit, UserShow } from "./pages/user";
|
||||
|
||||
import {CountryIcon, CityIcon, CarrierIcon, MediaIcon, ArticleIcon, SightIcon, StationIcon, VehicleIcon, RouteIcon, UsersIcon} from './components/ui/Icons'
|
||||
import SidebarTitle from './components/ui/SidebarTitle'
|
||||
import {AdminOnly} from './components/AdminOnly'
|
||||
import {
|
||||
CountryIcon,
|
||||
CityIcon,
|
||||
CarrierIcon,
|
||||
MediaIcon,
|
||||
ArticleIcon,
|
||||
SightIcon,
|
||||
StationIcon,
|
||||
VehicleIcon,
|
||||
RouteIcon,
|
||||
UsersIcon,
|
||||
} from "./components/ui/Icons";
|
||||
import SidebarTitle from "./components/ui/SidebarTitle";
|
||||
import { AdminOnly } from "./components/AdminOnly";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<RefineKbarProvider>
|
||||
<ColorModeContextProvider>
|
||||
<CssBaseline />
|
||||
<GlobalStyles styles={{html: {WebkitFontSmoothing: 'auto'}}} />
|
||||
<RefineSnackbarProvider>
|
||||
<DevtoolsProvider>
|
||||
<Refine
|
||||
dataProvider={customDataProvider}
|
||||
notificationProvider={useNotificationProvider}
|
||||
routerProvider={routerBindings}
|
||||
authProvider={authProvider}
|
||||
i18nProvider={i18nProvider}
|
||||
resources={[
|
||||
{
|
||||
name: 'country',
|
||||
list: '/country',
|
||||
create: '/country/create',
|
||||
edit: '/country/edit/:id',
|
||||
show: '/country/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Страны',
|
||||
icon: <CountryIcon />,
|
||||
},
|
||||
<ColorModeContextProvider>
|
||||
<CssBaseline />
|
||||
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
|
||||
<RefineSnackbarProvider>
|
||||
<DevtoolsProvider>
|
||||
<Refine
|
||||
dataProvider={customDataProvider}
|
||||
notificationProvider={useNotificationProvider}
|
||||
routerProvider={routerBindings}
|
||||
authProvider={authProvider}
|
||||
i18nProvider={i18nProvider}
|
||||
resources={[
|
||||
{
|
||||
name: "country",
|
||||
list: "/country",
|
||||
create: "/country/create",
|
||||
edit: "/country/edit/:id",
|
||||
show: "/country/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Страны",
|
||||
icon: <CountryIcon />,
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
list: '/city',
|
||||
create: '/city/create',
|
||||
edit: '/city/edit/:id',
|
||||
show: '/city/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Города',
|
||||
icon: <CityIcon />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "city",
|
||||
list: "/city",
|
||||
create: "/city/create",
|
||||
edit: "/city/edit/:id",
|
||||
show: "/city/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Города",
|
||||
icon: <CityIcon />,
|
||||
},
|
||||
{
|
||||
name: 'carrier',
|
||||
list: '/carrier',
|
||||
create: '/carrier/create',
|
||||
edit: '/carrier/edit/:id',
|
||||
show: '/carrier/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Перевозчики',
|
||||
icon: <CarrierIcon />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "carrier",
|
||||
list: "/carrier",
|
||||
create: "/carrier/create",
|
||||
edit: "/carrier/edit/:id",
|
||||
show: "/carrier/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Перевозчики",
|
||||
icon: <CarrierIcon />,
|
||||
},
|
||||
{
|
||||
name: 'media',
|
||||
list: '/media',
|
||||
create: '/media/create',
|
||||
edit: '/media/edit/:id',
|
||||
show: '/media/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Медиа',
|
||||
icon: <MediaIcon />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "media",
|
||||
list: "/media",
|
||||
create: "/media/create",
|
||||
edit: "/media/edit/:id",
|
||||
show: "/media/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Медиа",
|
||||
icon: <MediaIcon />,
|
||||
},
|
||||
{
|
||||
name: 'article',
|
||||
list: '/article',
|
||||
create: '/article/create',
|
||||
edit: '/article/edit/:id',
|
||||
show: '/article/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Статьи',
|
||||
icon: <ArticleIcon />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "article",
|
||||
list: "/article",
|
||||
create: "/article/create",
|
||||
edit: "/article/edit/:id",
|
||||
show: "/article/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Статьи",
|
||||
icon: <ArticleIcon />,
|
||||
},
|
||||
{
|
||||
name: 'sight',
|
||||
list: '/sight',
|
||||
create: '/sight/create',
|
||||
edit: '/sight/edit/:id',
|
||||
show: '/sight/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Достопримечательности',
|
||||
icon: <SightIcon />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sight",
|
||||
list: "/sight",
|
||||
create: "/sight/create",
|
||||
edit: "/sight/edit/:id",
|
||||
show: "/sight/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Достопримечательности",
|
||||
icon: <SightIcon />,
|
||||
},
|
||||
{
|
||||
name: 'station',
|
||||
list: '/station',
|
||||
create: '/station/create',
|
||||
edit: '/station/edit/:id',
|
||||
show: '/station/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Остановки',
|
||||
icon: <StationIcon />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "station",
|
||||
list: "/station",
|
||||
create: "/station/create",
|
||||
edit: "/station/edit/:id",
|
||||
show: "/station/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Остановки",
|
||||
icon: <StationIcon />,
|
||||
},
|
||||
{
|
||||
name: 'vehicle',
|
||||
list: '/vehicle',
|
||||
create: '/vehicle/create',
|
||||
edit: '/vehicle/edit/:id',
|
||||
show: '/vehicle/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Транспорт',
|
||||
icon: <VehicleIcon />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vehicle",
|
||||
list: "/vehicle",
|
||||
create: "/vehicle/create",
|
||||
edit: "/vehicle/edit/:id",
|
||||
show: "/vehicle/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Транспорт",
|
||||
icon: <VehicleIcon />,
|
||||
},
|
||||
{
|
||||
name: 'route',
|
||||
list: '/route',
|
||||
create: '/route/create',
|
||||
edit: '/route/edit/:id',
|
||||
show: '/route/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Маршруты',
|
||||
icon: <RouteIcon />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "route",
|
||||
list: "/route",
|
||||
create: "/route/create",
|
||||
edit: "/route/edit/:id",
|
||||
show: "/route/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Маршруты",
|
||||
icon: <RouteIcon />,
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
list: '/user',
|
||||
create: '/user/create',
|
||||
edit: '/user/edit/:id',
|
||||
show: '/user/show/:id',
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: 'Пользователи',
|
||||
icon: <UsersIcon />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
list: "/user",
|
||||
create: "/user/create",
|
||||
edit: "/user/edit/:id",
|
||||
show: "/user/show/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
label: "Пользователи",
|
||||
icon: <UsersIcon />,
|
||||
},
|
||||
]}
|
||||
options={{
|
||||
syncWithLocation: true,
|
||||
warnWhenUnsavedChanges: true, // Включаем глобально
|
||||
useNewQueryKeys: true,
|
||||
projectId: 'Wv044J-t53S3s-PcbJGe',
|
||||
},
|
||||
]}
|
||||
options={{
|
||||
syncWithLocation: true,
|
||||
warnWhenUnsavedChanges: true, // Включаем глобально
|
||||
useNewQueryKeys: true,
|
||||
projectId: "Wv044J-t53S3s-PcbJGe",
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<Authenticated
|
||||
key="authenticated-inner"
|
||||
fallback={<CatchAllNavigate to="/login" />}
|
||||
>
|
||||
<ThemedLayoutV2 Header={Header} Title={SidebarTitle}>
|
||||
<Outlet />
|
||||
</ThemedLayoutV2>
|
||||
</Authenticated>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
index
|
||||
element={<NavigateToResource resource="country" />}
|
||||
/>
|
||||
|
||||
<Route path="/country">
|
||||
<Route index element={<CountryList />} />
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<CountryCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<CountryEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route path="show/:id" element={<CountryShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/city">
|
||||
<Route index element={<CityList />} />
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<CityCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<CityEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route path="show/:id" element={<CityShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/carrier">
|
||||
<Route index element={<CarrierList />} />
|
||||
<Route path="create" element={<CarrierCreate />} />
|
||||
<Route path="edit/:id" element={<CarrierEdit />} />
|
||||
<Route path="show/:id" element={<CarrierShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/media">
|
||||
<Route index element={<MediaList />} />
|
||||
<Route path="create" element={<MediaCreate />} />
|
||||
<Route path="edit/:id" element={<MediaEdit />} />
|
||||
<Route path="show/:id" element={<MediaShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/article">
|
||||
<Route index element={<ArticleList />} />
|
||||
<Route path="create" element={<ArticleCreate />} />
|
||||
<Route path="edit/:id" element={<ArticleEdit />} />
|
||||
<Route path="show/:id" element={<ArticleShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/sight">
|
||||
<Route index element={<SightList />} />
|
||||
<Route path="create" element={<SightCreate />} />
|
||||
<Route path="edit/:id" element={<SightEdit />} />
|
||||
<Route path="show/:id" element={<SightShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/station">
|
||||
<Route index element={<StationList />} />
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<StationCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<StationEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route path="show/:id" element={<StationShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/vehicle">
|
||||
<Route index element={<VehicleList />} />
|
||||
<Route path="create" element={<VehicleCreate />} />
|
||||
<Route path="edit/:id" element={<VehicleEdit />} />
|
||||
<Route path="show/:id" element={<VehicleShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/route">
|
||||
<Route index element={<RouteList />} />
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<RouteCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<RouteEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route path="show/:id" element={<RouteShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/user">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AdminOnly>
|
||||
<UserList />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<UserCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<UserEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="show/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<UserShow />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Route>
|
||||
<Route
|
||||
element={
|
||||
<Authenticated
|
||||
key="authenticated-outer"
|
||||
fallback={<Outlet />}
|
||||
>
|
||||
<NavigateToResource />
|
||||
</Authenticated>
|
||||
}
|
||||
>
|
||||
<Route path="/login" element={<Login />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
<UnsavedChangesNotifier />
|
||||
<DocumentTitleHandler
|
||||
handler={() => {
|
||||
// const cleanedTitle = title.autoGeneratedTitle.split('|')[0].trim()
|
||||
// return `${cleanedTitle} — Белые ночи`
|
||||
return "Белые ночи";
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<Authenticated key="authenticated-inner" fallback={<CatchAllNavigate to="/login" />}>
|
||||
<ThemedLayoutV2 Header={Header} Title={SidebarTitle}>
|
||||
<Outlet />
|
||||
</ThemedLayoutV2>
|
||||
</Authenticated>
|
||||
}
|
||||
>
|
||||
<Route index element={<NavigateToResource resource="country" />} />
|
||||
|
||||
<Route path="/country">
|
||||
<Route index element={<CountryList />} />
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<CountryCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<CountryEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route path="show/:id" element={<CountryShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/city">
|
||||
<Route index element={<CityList />} />
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<CityCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<CityEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route path="show/:id" element={<CityShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/carrier">
|
||||
<Route index element={<CarrierList />} />
|
||||
<Route path="create" element={<CarrierCreate />} />
|
||||
<Route path="edit/:id" element={<CarrierEdit />} />
|
||||
<Route path="show/:id" element={<CarrierShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/media">
|
||||
<Route index element={<MediaList />} />
|
||||
<Route path="create" element={<MediaCreate />} />
|
||||
<Route path="edit/:id" element={<MediaEdit />} />
|
||||
<Route path="show/:id" element={<MediaShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/article">
|
||||
<Route index element={<ArticleList />} />
|
||||
<Route path="create" element={<ArticleCreate />} />
|
||||
<Route path="edit/:id" element={<ArticleEdit />} />
|
||||
<Route path="show/:id" element={<ArticleShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/sight">
|
||||
<Route index element={<SightList />} />
|
||||
<Route path="create" element={<SightCreate />} />
|
||||
<Route path="edit/:id" element={<SightEdit />} />
|
||||
<Route path="show/:id" element={<SightShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/station">
|
||||
<Route index element={<StationList />} />
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<StationCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<StationEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route path="show/:id" element={<StationShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/vehicle">
|
||||
<Route index element={<VehicleList />} />
|
||||
<Route path="create" element={<VehicleCreate />} />
|
||||
<Route path="edit/:id" element={<VehicleEdit />} />
|
||||
<Route path="show/:id" element={<VehicleShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/route">
|
||||
<Route index element={<RouteList />} />
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<RouteCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<RouteEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route path="show/:id" element={<RouteShow />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/user">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AdminOnly>
|
||||
<UserList />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="create"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<UserCreate />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="edit/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<UserEdit />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="show/:id"
|
||||
element={
|
||||
<AdminOnly>
|
||||
<UserShow />
|
||||
</AdminOnly>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Route>
|
||||
<Route
|
||||
element={
|
||||
<Authenticated key="authenticated-outer" fallback={<Outlet />}>
|
||||
<NavigateToResource />
|
||||
</Authenticated>
|
||||
}
|
||||
>
|
||||
<Route path="/login" element={<Login />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
<RefineKbar />
|
||||
<UnsavedChangesNotifier />
|
||||
<DocumentTitleHandler
|
||||
handler={() => {
|
||||
// const cleanedTitle = title.autoGeneratedTitle.split('|')[0].trim()
|
||||
// return `${cleanedTitle} — Белые ночи`
|
||||
return 'Белые ночи'
|
||||
}}
|
||||
/>
|
||||
</Refine>
|
||||
<DevtoolsPanel />
|
||||
</DevtoolsProvider>
|
||||
</RefineSnackbarProvider>
|
||||
</ColorModeContextProvider>
|
||||
</RefineKbarProvider>
|
||||
/>
|
||||
</Refine>
|
||||
<DevtoolsPanel />
|
||||
</DevtoolsProvider>
|
||||
</RefineSnackbarProvider>
|
||||
</ColorModeContextProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
@ -12,12 +12,22 @@ import {
|
||||
useTheme,
|
||||
TextField,
|
||||
Autocomplete,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
TableBody,
|
||||
} from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { axiosInstance } from "../providers/data";
|
||||
|
||||
import { Link } from "react-router";
|
||||
import { TOKEN_KEY } from "../authProvider";
|
||||
import { Droppable, Draggable, DragDropContext } from "@hello-pangea/dnd";
|
||||
|
||||
// TODO: ДОДЕЛАТЬ
|
||||
|
||||
type Field<T> = {
|
||||
label: string;
|
||||
@ -42,6 +52,14 @@ type LinkedItemsProps<T> = {
|
||||
extraField?: ExtraFieldConfig;
|
||||
};
|
||||
|
||||
const reorder = (list, startIndex, endIndex) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||
parentId,
|
||||
parentResource,
|
||||
@ -58,6 +76,20 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||
const [mediaOrder, setMediaOrder] = useState<number>(1);
|
||||
const theme = useTheme();
|
||||
|
||||
const onDragEnd = (result) => {
|
||||
// ドロップ先がない
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
// 配列の順序を入れ替える
|
||||
let movedItems = reorder(
|
||||
linkedItems, // 順序を入れ変えたい配列
|
||||
result.source.index, // 元の配列の位置
|
||||
result.destination.index // 移動先の配列の位置
|
||||
);
|
||||
setLinkedItems(movedItems);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (parentId) {
|
||||
axiosInstance
|
||||
@ -180,7 +212,70 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||
</AccordionSummary>
|
||||
|
||||
<AccordionDetails sx={{ background: theme.palette.background.paper }}>
|
||||
<Stack gap={2}>
|
||||
<TableContainer component={Paper}>
|
||||
<Table aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Действие</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<TableBody
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{linkedItems.length > 0 &&
|
||||
linkedItems.map((item, index) => (
|
||||
<Draggable
|
||||
key={item.id}
|
||||
draggableId={"q" + item.id.toString()}
|
||||
index={index}
|
||||
>
|
||||
{(provided) => (
|
||||
<TableRow
|
||||
ref={provided.innerRef}
|
||||
{...provided.dragHandleProps}
|
||||
{...provided.draggableProps}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
}}
|
||||
>
|
||||
<TableCell style={{ flex: 1, minWidth: "100px" }}>
|
||||
{item.id}
|
||||
</TableCell>
|
||||
<TableCell style={{ flex: 1, minWidth: "100px" }}>
|
||||
{item.name}
|
||||
</TableCell>
|
||||
<TableCell style={{ flex: 1, minWidth: "100px" }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
deleteItem(item.id);
|
||||
}}
|
||||
>
|
||||
Отвязать
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</TableBody>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* <Stack gap={2}>
|
||||
<Grid container gap={1.25}>
|
||||
{isLoading ? (
|
||||
<Typography>Загрузка...</Typography>
|
||||
@ -208,7 +303,8 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||
{childResource === "media" && item.id && (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}/${
|
||||
item.id
|
||||
item.iimport { DragDropContext } from 'react-beautiful-dnd';
|
||||
d
|
||||
}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
|
||||
alt={String(item.media_name)}
|
||||
style={{
|
||||
@ -338,7 +434,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack> */}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import App from "./App";
|
||||
@ -7,8 +6,4 @@ import "./globals.css";
|
||||
const container = document.getElementById("root") as HTMLElement;
|
||||
const root = createRoot(container);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
root.render(<App />);
|
||||
|
@ -130,12 +130,27 @@ export const RouteEdit = () => {
|
||||
(Прямой / Обратный)
|
||||
</Typography>
|
||||
|
||||
<Controller
|
||||
name="path"
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{
|
||||
<TextField
|
||||
{...register("path", {
|
||||
required: "Это поле является обязательным",
|
||||
setValueAs: (value: string) => {
|
||||
try {
|
||||
// Разбиваем строку на строки и парсим каждую строку как пару координат
|
||||
const lines = value.trim().split("\n");
|
||||
return lines.map((line) => {
|
||||
const [lat, lon] = line
|
||||
.trim()
|
||||
.split(/[\s,]+/)
|
||||
.map(Number);
|
||||
if (isNaN(lat) || isNaN(lon)) {
|
||||
throw new Error("Invalid coordinates");
|
||||
}
|
||||
return [lat, lon];
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
validate: (value: unknown) => {
|
||||
if (!Array.isArray(value)) return "Неверный формат";
|
||||
if (value.length === 0)
|
||||
@ -159,49 +174,22 @@ export const RouteEdit = () => {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
value={
|
||||
Array.isArray(field.value)
|
||||
? field.value.map((point) => point.join(" ")).join("\n")
|
||||
: ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const lines = e.target.value.trim().split("\n");
|
||||
const parsed = lines.map((line) => {
|
||||
const [lat, lon] = line
|
||||
.trim()
|
||||
.split(/[\s,]+/)
|
||||
.map(Number);
|
||||
if (isNaN(lat) || isNaN(lon)) {
|
||||
throw new Error("Invalid coordinates");
|
||||
}
|
||||
return [lat, lon];
|
||||
});
|
||||
field.onChange(parsed);
|
||||
} catch {
|
||||
field.onChange([]);
|
||||
}
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={error?.message}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
type="text"
|
||||
label={"Координаты маршрута *"}
|
||||
placeholder="55.7558 37.6173
|
||||
})}
|
||||
error={!!(errors as any)?.path}
|
||||
helperText={(errors as any)?.path?.message}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
type="text"
|
||||
label={"Координаты маршрута *"}
|
||||
name="path"
|
||||
placeholder="55.7558 37.6173
|
||||
55.7539 37.6208"
|
||||
multiline
|
||||
rows={4}
|
||||
sx={{
|
||||
marginBottom: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
multiline
|
||||
rows={4}
|
||||
sx={{
|
||||
marginBottom: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { UseQueryResult, useQuery } from 'react-query';
|
||||
import { MapData } from '@mt/components';
|
||||
import { mapStationsFromApi } from './mapStationsFromApi';
|
||||
|
||||
export function useGetMapData(): UseQueryResult<MapData> {
|
||||
return useQuery<MapData>(
|
||||
'getMapData',
|
||||
async () => {
|
||||
const { stationsOnMap, trackPoints, ...rest } = await fetch(
|
||||
'https://localhost:8443/widgets/route-map/data'
|
||||
).then((res) => res.json());
|
||||
|
||||
return {
|
||||
trackPoints,
|
||||
stationsOnMap: mapStationsFromApi(stationsOnMap, trackPoints),
|
||||
...rest,
|
||||
};
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import { useEffect } from 'react';
|
||||
import { useEventQuery } from '@mt/utils';
|
||||
import { RouteInfoData } from '@mt/components';
|
||||
|
||||
export const useGetRouteInfo = () => {
|
||||
const { data, isSuccess } = useEventQuery(
|
||||
// 'getRouteInfoEvents',
|
||||
'/widgets/route-info/events',
|
||||
['REFRESH_DATA']
|
||||
);
|
||||
|
||||
const routeInfoQuery = useQuery<RouteInfoData>(
|
||||
'getRouteInfo',
|
||||
async () =>
|
||||
await fetch('https://localhost:8443/widgets/route-info/data').then((res) => res.json())
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess && data.length) {
|
||||
routeInfoQuery.refetch();
|
||||
}
|
||||
}, [data, isSuccess]);
|
||||
|
||||
return routeInfoQuery;
|
||||
};
|
@ -1,36 +0,0 @@
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { LocalizedString } from '@mt/i18n';
|
||||
import { useEventQuery } from '@mt/utils';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface Attraction {
|
||||
id: string;
|
||||
name: LocalizedString;
|
||||
iconUrl: string;
|
||||
}
|
||||
|
||||
export function useGetAttractionList(): UseQueryResult<Attraction[]> {
|
||||
const { data, isSuccess } = useEventQuery('/widgets/attraction-with-details-list/events', [
|
||||
'REFRESH_DATA',
|
||||
]);
|
||||
|
||||
const attractionListQuery = useQuery<Attraction[]>(
|
||||
['getAttractionList'],
|
||||
async () =>
|
||||
await fetch('https://localhost:8443/widgets/attraction-with-details-list/data')
|
||||
.then((res) => res.json())
|
||||
.then(({ touristAttractions }) => touristAttractions),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess && data.length) {
|
||||
attractionListQuery.refetch();
|
||||
}
|
||||
}, [data, isSuccess]);
|
||||
|
||||
return attractionListQuery;
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import { UseQueryResult, useQuery } from 'react-query';
|
||||
import { uuid } from '@mt/common-types';
|
||||
import { AttractionDetailsBE } from '../nav-widget.interface';
|
||||
import { AttractionShortPreviewProps } from '@mt/components';
|
||||
|
||||
export function useGetAttractionDetails(
|
||||
touristAttractionId: uuid
|
||||
): UseQueryResult<AttractionShortPreviewProps> {
|
||||
return useQuery(['getAttractionDetails', touristAttractionId], async () => {
|
||||
const {
|
||||
touristAttractionAddress: subtitle,
|
||||
touristAttractionDescription: content,
|
||||
touristAttractionName: title,
|
||||
touristAttractionImageUrl: img,
|
||||
}: AttractionDetailsBE = await fetch(
|
||||
'https://localhost:8443/widgets/attraction-info/data-by-params?' +
|
||||
new URLSearchParams({
|
||||
touristAttractionId: touristAttractionId as string,
|
||||
})
|
||||
).then((res) => res.json());
|
||||
|
||||
return { img, title, subtitle, content };
|
||||
});
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { UseQueryResult, useQuery } from 'react-query';
|
||||
import { Attraction } from '../nav-widget.interface';
|
||||
import { useEventQuery } from '@mt/utils';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useGetAttractions(): UseQueryResult<Attraction[]> {
|
||||
const { data, isSuccess } = useEventQuery(
|
||||
// 'getAttractionsEvents',
|
||||
'/widgets/attraction-list/events',
|
||||
['REFRESH_DATA']
|
||||
);
|
||||
|
||||
const attractionQuery = useQuery(
|
||||
'getAttractions',
|
||||
async () =>
|
||||
await fetch('https://localhost:8443/widgets/attraction-list/data')
|
||||
.then((res) => res.json())
|
||||
.then(({ touristAttractions }) => touristAttractions)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess && data.length) {
|
||||
attractionQuery.refetch();
|
||||
}
|
||||
}, [data, isSuccess]);
|
||||
|
||||
return attractionQuery;
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { UseQueryResult, useQuery } from 'react-query';
|
||||
import { Station } from '../nav-widget.interface';
|
||||
import { useEventQuery } from '@mt/utils';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useGetStations(): UseQueryResult<Station[]> {
|
||||
const { data = [], isSuccess } = useEventQuery(
|
||||
// 'getStationsEvents',
|
||||
'/widgets/station-list/events',
|
||||
['REFRESH_DATA']
|
||||
);
|
||||
|
||||
const stationQuery = useQuery('getStations', async () => {
|
||||
const { stations } = await fetch('https://localhost:8443/widgets/station-list/data').then(
|
||||
(res) => res.json()
|
||||
);
|
||||
|
||||
return stations;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess && data.length) {
|
||||
stationQuery.refetch();
|
||||
}
|
||||
}, [data, isSuccess]);
|
||||
|
||||
return stationQuery;
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import './AttractionShortPreview.css';
|
||||
|
||||
import { LocalizedString, useServerLocalization } from '@mt/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { TouchScrollWrapper } from '../TouchScrollWrapper/TouchScrollWrapper';
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
export interface AttractionShortPreviewProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
||||
img: string;
|
||||
title: LocalizedString;
|
||||
subtitle: LocalizedString;
|
||||
content: LocalizedString;
|
||||
}
|
||||
|
||||
export function AttractionShortPreview({
|
||||
img,
|
||||
title,
|
||||
subtitle,
|
||||
content,
|
||||
className,
|
||||
...props
|
||||
}: AttractionShortPreviewProps) {
|
||||
const localizeText = useServerLocalization();
|
||||
|
||||
return (
|
||||
<div className={classNames(className, 'attraction-card g-flex-column')} {...props}>
|
||||
{img && <img className="attraction-card__image" src={img} alt={localizeText(title)} />}
|
||||
|
||||
<TouchScrollWrapper className="g-flex-column__item">
|
||||
<div className="attraction-card__content">
|
||||
<h4 className="attraction-card__title">{localizeText(title)}</h4>
|
||||
|
||||
<h5 className="attraction-card__subtitle">{localizeText(subtitle)}</h5>
|
||||
|
||||
<p
|
||||
className="attraction-card__text"
|
||||
dangerouslySetInnerHTML={{ __html: localizeText(content) }}
|
||||
/>
|
||||
</div>
|
||||
</TouchScrollWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { Marker, Point } from 'react-simple-maps';
|
||||
import { Icons } from '@mt/components';
|
||||
import { AttractionGroupIconSizeType } from '@mt/common-types';
|
||||
|
||||
import styles from './AttractionMarker.module.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
coordinates: Point;
|
||||
rotate: number;
|
||||
size: AttractionGroupIconSizeType;
|
||||
counter?: number;
|
||||
}
|
||||
|
||||
export const AttractionMarker = ({ coordinates, counter = 0, rotate, size }: Props) => {
|
||||
return (
|
||||
<Marker coordinates={coordinates}>
|
||||
<foreignObject
|
||||
className={cn({
|
||||
[styles.markerLarge]: size === 'LARGE',
|
||||
[styles.markerSmall]: size === 'SMALL',
|
||||
})}
|
||||
>
|
||||
<div className="g-transform-origin__center" style={{ transform: `rotate(${rotate}deg)` }}>
|
||||
<Icons.AttractionIcon className={styles.icon} />
|
||||
|
||||
{counter > 1 && <div className={styles.counter}>{counter}</div>}
|
||||
</div>
|
||||
</foreignObject>
|
||||
</Marker>
|
||||
);
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
import React, { ForwardedRef, forwardRef, useMemo } from 'react';
|
||||
import { Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera } from 'three';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { fitCameraToObject } from './fitCameraToObject';
|
||||
import { BufferGeometry } from 'three/src/core/BufferGeometry';
|
||||
|
||||
export const Model = forwardRef(({ url }: { url: string }, ref: ForwardedRef<Object3D>) => {
|
||||
const { camera } = useThree();
|
||||
const { scene } = useGLTF(url);
|
||||
|
||||
const model = useMemo(() => {
|
||||
const model = (SkeletonUtils as any).clone(scene);
|
||||
model.traverse((el: Mesh<BufferGeometry, MeshPhysicalMaterial>) => {
|
||||
if (el.type === 'Mesh') {
|
||||
el.material.reflectivity = 0.0;
|
||||
el.material.fog = false;
|
||||
el.material.color.setHex(0xffffff);
|
||||
}
|
||||
});
|
||||
|
||||
fitCameraToObject(camera as PerspectiveCamera, scene);
|
||||
scene.updateWorldMatrix(true, true);
|
||||
|
||||
return model;
|
||||
}, [scene, camera]);
|
||||
|
||||
return <primitive key={url} object={model} ref={ref} />;
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
import {
|
||||
ArticleBase,
|
||||
Coordinates,
|
||||
StationOnMap,
|
||||
TransportType,
|
||||
} from "@mt/common-types";
|
||||
import { LocalizedString } from "@mt/i18n";
|
||||
|
||||
// TODO: Rename with BE pointOnMap => coordinates
|
||||
export interface Station extends Omit<StationOnMap, "coordinates"> {
|
||||
pointOnMap: Coordinates;
|
||||
}
|
||||
|
||||
export interface Article extends Omit<ArticleBase, "name"> {
|
||||
title: LocalizedString;
|
||||
}
|
||||
|
||||
export interface Transfer {
|
||||
type: TransportType;
|
||||
name: LocalizedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* TYPES
|
||||
*/
|
||||
export * from "./uuid.type";
|
||||
export * from "./media.type";
|
||||
export * from "./track.type";
|
||||
export * from "./coordinates.type";
|
||||
export * from "./on-map.type";
|
||||
export * from "./transport.type";
|
||||
export * from "./option.type";
|
||||
export * from "./attraction-group-icon-size.type";
|
||||
export * from "./order.type";
|
||||
export * from "./set-state.type";
|
||||
|
||||
/**
|
||||
* INTERFACES
|
||||
*/
|
||||
export * from "./station.interface";
|
||||
export * from "./attraction.interface";
|
||||
export * from "./attraction-widget.interface";
|
||||
export * from "./article.interface";
|
||||
export * from "./lightbox.interface";
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 8.3 MiB After Width: | Height: | Size: 8.3 MiB |
@ -0,0 +1,55 @@
|
||||
import "./AttractionShortPreview.css";
|
||||
|
||||
import { LocalizedString, useServerLocalization } from "@mt/i18n";
|
||||
import classNames from "classnames";
|
||||
import { TouchScrollWrapper } from "../TouchScrollWrapper/TouchScrollWrapper";
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
export interface AttractionShortPreviewProps
|
||||
extends Omit<HTMLAttributes<HTMLElement>, "title" | "content"> {
|
||||
img: string;
|
||||
title: LocalizedString;
|
||||
subtitle: LocalizedString;
|
||||
content: LocalizedString;
|
||||
}
|
||||
|
||||
export function AttractionShortPreview({
|
||||
img,
|
||||
title,
|
||||
subtitle,
|
||||
content,
|
||||
className,
|
||||
...props
|
||||
}: AttractionShortPreviewProps) {
|
||||
const localizeText = useServerLocalization();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(className, "attraction-card g-flex-column")}
|
||||
{...props}
|
||||
>
|
||||
{img && (
|
||||
<img
|
||||
className="attraction-card__image"
|
||||
src={img}
|
||||
alt={localizeText(title)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TouchScrollWrapper className="g-flex-column__item">
|
||||
<div className="attraction-card__content">
|
||||
<h4 className="attraction-card__title">{localizeText(title)}</h4>
|
||||
|
||||
<h5 className="attraction-card__subtitle">
|
||||
{localizeText(subtitle)}
|
||||
</h5>
|
||||
|
||||
<p
|
||||
className="attraction-card__text"
|
||||
dangerouslySetInnerHTML={{ __html: localizeText(content) }}
|
||||
/>
|
||||
</div>
|
||||
</TouchScrollWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
import React, { HTMLAttributes, useEffect } from 'react';
|
||||
import { useServerLocalization } from '@mt/i18n';
|
||||
import cn from 'classnames';
|
||||
import { useSwipeable } from 'react-swipeable';
|
||||
import { ArticleBase } from '@mt/common-types';
|
||||
import './AttractionWidget.css';
|
||||
import { usePrevious } from '@mt/utils';
|
||||
import { AttractionMedia } from './media/AttractionMedia';
|
||||
import { useStore } from 'react-admin';
|
||||
import { TouchScrollWrapper } from '../TouchScrollWrapper/TouchScrollWrapper';
|
||||
import React, { HTMLAttributes, useEffect, useState } from "react";
|
||||
import { useServerLocalization } from "@mt/i18n";
|
||||
import cn from "classnames";
|
||||
import { useSwipeable } from "react-swipeable";
|
||||
import { ArticleBase } from "@mt/common-types";
|
||||
import "./AttractionWidget.css";
|
||||
import { usePrevious } from "@mt/utils";
|
||||
import { AttractionMedia } from "./media/AttractionMedia";
|
||||
import { TouchScrollWrapper } from "../TouchScrollWrapper/TouchScrollWrapper";
|
||||
|
||||
export interface AttractionsWidgetProps extends HTMLAttributes<HTMLElement> {
|
||||
articles: ArticleBase[];
|
||||
@ -15,15 +14,13 @@ export interface AttractionsWidgetProps extends HTMLAttributes<HTMLElement> {
|
||||
isPreviewOnly?: boolean;
|
||||
}
|
||||
|
||||
export const ATTRACTION_WIDGET_TABINDEX_STORE_KEY = 'attractions.widget.tabindex';
|
||||
|
||||
export function AttractionWidget({
|
||||
articles,
|
||||
isIdleMode,
|
||||
isPreviewOnly = false,
|
||||
...props
|
||||
}: AttractionsWidgetProps) {
|
||||
const [activeIndex, setActiveIndex] = useStore(ATTRACTION_WIDGET_TABINDEX_STORE_KEY, 0);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const prevArticles = usePrevious<ArticleBase[]>(articles) || [];
|
||||
const localizeText = useServerLocalization();
|
||||
|
||||
@ -34,7 +31,9 @@ export function AttractionWidget({
|
||||
},
|
||||
onSwipedRight: ({ event }) => {
|
||||
event.preventDefault();
|
||||
setActiveIndex((activeIndex) => (activeIndex - 1 + articles.length) % articles.length);
|
||||
setActiveIndex(
|
||||
(activeIndex) => (activeIndex - 1 + articles.length) % articles.length
|
||||
);
|
||||
},
|
||||
swipeDuration: 500,
|
||||
preventScrollOnSwipe: true,
|
||||
@ -43,7 +42,7 @@ export function AttractionWidget({
|
||||
|
||||
const handleClick = (index: number) => {
|
||||
setActiveIndex(index);
|
||||
document.querySelector('.widget-text.active')!.scrollTop = 0;
|
||||
document.querySelector(".widget-text.active")!.scrollTop = 0;
|
||||
};
|
||||
|
||||
useEffect(() => setActiveIndex(activeIndex), [activeIndex]);
|
||||
@ -68,17 +67,21 @@ export function AttractionWidget({
|
||||
{articles?.map((article, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`widget-slide ${index === activeIndex ? 'active' : ''}`}
|
||||
className={`widget-slide ${index === activeIndex ? "active" : ""}`}
|
||||
onPointerUp={() => handleClick(index)}
|
||||
>
|
||||
<div className="widget-media">
|
||||
<AttractionMedia media={article.media} />
|
||||
</div>
|
||||
|
||||
{index !== 0 && <div className="widget-header">{localizeText(articles[0].text)}</div>}
|
||||
{index !== 0 && (
|
||||
<div className="widget-header">
|
||||
{localizeText(articles[0].text)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TouchScrollWrapper
|
||||
className={cn('widget-text', {
|
||||
className={cn("widget-text", {
|
||||
active: index === activeIndex,
|
||||
preview: article.isPreview,
|
||||
})}
|
||||
@ -95,7 +98,7 @@ export function AttractionWidget({
|
||||
{articles?.map((article, index) => (
|
||||
<div
|
||||
key={`title-${index}`}
|
||||
className={cn('widget-title', {
|
||||
className={cn("widget-title", {
|
||||
active: index === activeIndex,
|
||||
preview: article.isPreview,
|
||||
})}
|
@ -1,9 +1,9 @@
|
||||
import { Media } from '@mt/common-types';
|
||||
import { ImageMedia } from './ImageMedia';
|
||||
import { VideoMedia } from './VideoMedia';
|
||||
import { PhotoSphereMedia } from './PhotoSphereMedia';
|
||||
import { Object3DMedia } from './Object3DMedia';
|
||||
import { memo } from 'react';
|
||||
import { Media } from "@mt/common-types";
|
||||
import { ImageMedia } from "./ImageMedia";
|
||||
import { VideoMedia } from "./VideoMedia";
|
||||
import { PhotoSphereMedia } from "./PhotoSphereMedia";
|
||||
import { Object3DMedia } from "./Object3DMedia";
|
||||
import { memo } from "react";
|
||||
|
||||
export const AttractionMedia = memo(
|
||||
({ media }: { media: Media }) => {
|
||||
@ -12,13 +12,15 @@ export const AttractionMedia = memo(
|
||||
if (!url) return null;
|
||||
|
||||
switch (type) {
|
||||
case 'IMAGE':
|
||||
return <ImageMedia url={url} alt={media.url} watermarkUrl={watermarkUrl} />;
|
||||
case 'VIDEO':
|
||||
case "IMAGE":
|
||||
return (
|
||||
<ImageMedia url={url} alt={media.url} watermarkUrl={watermarkUrl} />
|
||||
);
|
||||
case "VIDEO":
|
||||
return <VideoMedia url={url} watermarkUrl={watermarkUrl} />;
|
||||
case 'PHOTO_SPHERE':
|
||||
case "PHOTO_SPHERE":
|
||||
return <PhotoSphereMedia url={url} watermarkUrl={watermarkUrl} />;
|
||||
case 'OBJECT_3D':
|
||||
case "OBJECT_3D":
|
||||
return <Object3DMedia url={url} watermarkUrl={watermarkUrl} />;
|
||||
default:
|
||||
return null;
|
@ -1,28 +1,31 @@
|
||||
import cn from 'classnames';
|
||||
import React, { useRef } from 'react';
|
||||
import { ReactPhotoSphereViewer } from 'react-photo-sphere-viewer';
|
||||
import cn from "classnames";
|
||||
import React, { useRef } from "react";
|
||||
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
|
||||
|
||||
import { PhotoSphereLightboxData } from '@mt/common-types';
|
||||
import { PhotoSphereLightboxData } from "@mt/common-types";
|
||||
|
||||
import './AttractionMedia.css';
|
||||
import { useLightboxContext } from '../../lightbox';
|
||||
import { Icons } from '@mt/components';
|
||||
import "./AttractionMedia.css";
|
||||
import { useLightboxContext } from "../../lightbox";
|
||||
import { Icons } from "@mt/components";
|
||||
|
||||
interface PhotoSphereMediaProps {
|
||||
url: string;
|
||||
watermarkUrl?: string;
|
||||
}
|
||||
|
||||
export const PhotoSphereMedia = ({ url, watermarkUrl }: PhotoSphereMediaProps) => {
|
||||
export const PhotoSphereMedia = ({
|
||||
url,
|
||||
watermarkUrl,
|
||||
}: PhotoSphereMediaProps) => {
|
||||
// prettier-ignore
|
||||
const { setData, openLightbox } = useLightboxContext<PhotoSphereLightboxData>();
|
||||
// react-photo-sphere-viewer doesn't have exported types, so here's a bit of a hardcoded piece
|
||||
const photoSphereRef = useRef<{ stopAutoRotate: () => void }>();
|
||||
const photoSphereRef = useRef<any>(null);
|
||||
|
||||
const handlePhotoSphereFullscreenOpen = () => {
|
||||
photoSphereRef.current?.stopAutoRotate();
|
||||
setData({
|
||||
type: 'PHOTO_SPHERE',
|
||||
type: "PHOTO_SPHERE",
|
||||
imageUrl: url,
|
||||
watermarkUrl,
|
||||
});
|
||||
@ -35,18 +38,20 @@ export const PhotoSphereMedia = ({ url, watermarkUrl }: PhotoSphereMediaProps) =
|
||||
ref={photoSphereRef}
|
||||
key={url}
|
||||
src={url}
|
||||
height={'350px'}
|
||||
width={'100%'}
|
||||
container={cn('widget-media', {
|
||||
'media-with-watermark': watermarkUrl !== null,
|
||||
height={"350px"}
|
||||
width={"100%"}
|
||||
container={cn("widget-media", {
|
||||
"media-with-watermark": watermarkUrl !== null,
|
||||
})}
|
||||
moveInertia={false}
|
||||
mousemove={true}
|
||||
navbar={['autorotate', 'zoom']}
|
||||
navbar={["autorotate", "zoom"]}
|
||||
keyboard={false}
|
||||
loadingTxt="Загрузка..."
|
||||
/>
|
||||
{watermarkUrl && <img src={watermarkUrl} alt="Watermark" className="watermark" />}
|
||||
{watermarkUrl && (
|
||||
<img src={watermarkUrl} alt="Watermark" className="watermark" />
|
||||
)}
|
||||
{/* the following is a workaround to open lightbox-like preview in the middle of the screen instead of the real fullscreen */}
|
||||
<Icons.FullscreenIcon
|
||||
className="fullscreen-photo-sphere-btn"
|
@ -1,7 +1,7 @@
|
||||
import cn from 'classnames';
|
||||
import React from 'react';
|
||||
import cn from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import './AttractionMedia.css';
|
||||
import "./AttractionMedia.css";
|
||||
|
||||
interface VideoMediaProps {
|
||||
url: string;
|
||||
@ -12,8 +12,8 @@ export const VideoMedia = ({ url, watermarkUrl }: VideoMediaProps) => (
|
||||
<>
|
||||
<video
|
||||
src={url}
|
||||
className={cn('widget-video', {
|
||||
'media-with-watermark': watermarkUrl !== null,
|
||||
className={cn("widget-video", {
|
||||
"media-with-watermark": watermarkUrl !== null,
|
||||
})}
|
||||
autoPlay
|
||||
loop
|
@ -1,17 +1,27 @@
|
||||
import React, { createContext, useState, useContext, ReactNode, useMemo, useEffect } from 'react';
|
||||
import { geoMercator } from 'd3-geo';
|
||||
import { Coordinates, Track, uuid } from '@mt/common-types';
|
||||
import { useNearStation, usePassedTrackIndex } from './hooks';
|
||||
import { AttractionGroup, MapData, StationOnMap } from './map-widget.interface';
|
||||
import { getMapPoint } from './utils';
|
||||
import { EMPTY_SETTING_VALUE, zeroCoordinates } from './map-widget.constant';
|
||||
import { MapSettings, MapWidgetContextType } from './map-widget-context.interface';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useContext,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { geoMercator } from "d3-geo";
|
||||
import { Coordinates, Track, uuid } from "@mt/common-types";
|
||||
import { useNearStation, usePassedTrackIndex } from "./hooks";
|
||||
import { AttractionGroup, MapData, StationOnMap } from "./map-widget.interface";
|
||||
import { getMapPoint } from "./utils";
|
||||
import { EMPTY_SETTING_VALUE, zeroCoordinates } from "./map-widget.constant";
|
||||
import {
|
||||
MapSettings,
|
||||
MapWidgetContextType,
|
||||
} from "./map-widget-context.interface";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const mapCanvasProps = {
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
width: 500,
|
||||
height: 400,
|
||||
@ -24,7 +34,9 @@ export const MapWidgetProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [track, setTrack] = useState<Track | null>(null);
|
||||
const [stations, setStations] = useState<StationOnMap[]>([]);
|
||||
const [updatedStationIds, setUpdatedStationIds] = useState<uuid[]>([]);
|
||||
const [attractionGroups, setAttractionGroups] = useState<AttractionGroup[]>([]);
|
||||
const [attractionGroups, setAttractionGroups] = useState<AttractionGroup[]>(
|
||||
[]
|
||||
);
|
||||
const [rotateAngle, setRotateAngle] = useState<number>(0);
|
||||
|
||||
const [scale, setScale] = useState<number>(0);
|
||||
@ -34,13 +46,17 @@ export const MapWidgetProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [center, setCenter] = useState(zeroCoordinates);
|
||||
const [baseCenter, setBaseCenter] = useState(zeroCoordinates);
|
||||
|
||||
const [currentPosition, setCurrentPosition] = useState<Coordinates | null>(null);
|
||||
const [currentPosition, setCurrentPosition] = useState<Coordinates | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(false);
|
||||
const [isDragMode, setIsDragMode] = useState<boolean>(false);
|
||||
|
||||
const [initialSettingsData, setInitialSettingsData] = useState<MapSettings>(EMPTY_SETTING_VALUE);
|
||||
const [isSettingsDataChanged, setIsSettingsDataChanged] = useState<boolean>(false);
|
||||
const [initialSettingsData, setInitialSettingsData] =
|
||||
useState<MapSettings>(EMPTY_SETTING_VALUE);
|
||||
const [isSettingsDataChanged, setIsSettingsDataChanged] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const isMapDataChanged = useMemo(
|
||||
() => isSettingsDataChanged || updatedStationIds.length > 0,
|
||||
@ -63,11 +79,14 @@ export const MapWidgetProvider = ({ children }: { children: ReactNode }) => {
|
||||
}, [track]);
|
||||
|
||||
const settingsForm = useForm({
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
mode: "onChange",
|
||||
reValidateMode: "onChange",
|
||||
defaultValues: initialSettingsData,
|
||||
});
|
||||
useEffect(() => settingsForm.reset(initialSettingsData), [initialSettingsData]);
|
||||
useEffect(
|
||||
() => settingsForm.reset(initialSettingsData),
|
||||
[initialSettingsData]
|
||||
);
|
||||
|
||||
const onMapDataFetched = (data: MapData) => {
|
||||
setTrack(data.trackPoints);
|
||||
@ -95,7 +114,8 @@ export const MapWidgetProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const onSettingsFormChange = () => {
|
||||
const formData = settingsForm.getValues();
|
||||
const { center, rotateAngle, fullScale, zoomedScale, currentStationId } = formData;
|
||||
const { center, rotateAngle, fullScale, zoomedScale, currentStationId } =
|
||||
formData;
|
||||
|
||||
setBaseCenter(center);
|
||||
setRotateAngle(rotateAngle);
|
||||
@ -120,7 +140,7 @@ export const MapWidgetProvider = ({ children }: { children: ReactNode }) => {
|
||||
const onMapCenterMoved = (center: Coordinates) => {
|
||||
setBaseCenter(center);
|
||||
setCenter(center);
|
||||
settingsForm.setValue('center', center, { shouldDirty: true });
|
||||
settingsForm.setValue("center", center, { shouldDirty: true });
|
||||
|
||||
updateMapDataChanged(settingsForm.getValues());
|
||||
};
|
||||
@ -138,7 +158,7 @@ export const MapWidgetProvider = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onStationUpdate: MapWidgetContextType['onStationUpdate'] = (
|
||||
const onStationUpdate: MapWidgetContextType["onStationUpdate"] = (
|
||||
stationId,
|
||||
{ labelOffset, labelAlignment }
|
||||
) => {
|
||||
@ -149,14 +169,18 @@ export const MapWidgetProvider = ({ children }: { children: ReactNode }) => {
|
||||
};
|
||||
|
||||
setStations((stations) =>
|
||||
stations.map((station) => (station.id === stationId ? updatedStation : station))
|
||||
stations.map((station) =>
|
||||
station.id === stationId ? updatedStation : station
|
||||
)
|
||||
);
|
||||
setUpdatedStationIds((ids) => [...ids, stationId]);
|
||||
};
|
||||
|
||||
const getUpdatedStations = () => {
|
||||
return updatedStationIds.reduce((acc: Record<uuid, any>, id: uuid) => {
|
||||
const { labelAlignment, labelOffset } = stationsMap.get(id) as StationOnMap;
|
||||
const { labelAlignment, labelOffset } = stationsMap.get(
|
||||
id
|
||||
) as StationOnMap;
|
||||
|
||||
acc[id] = {
|
||||
textAlignment: labelAlignment,
|
||||
@ -230,14 +254,20 @@ export const MapWidgetProvider = ({ children }: { children: ReactNode }) => {
|
||||
isMapDataChanged,
|
||||
};
|
||||
|
||||
return <MapWidgetContext.Provider value={contextValue}>{children}</MapWidgetContext.Provider>;
|
||||
return (
|
||||
<MapWidgetContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</MapWidgetContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMapWidgetContext = function (): MapWidgetContextType {
|
||||
const context = useContext(MapWidgetContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useMapWidgetContext must be used within a MapWidgetProvider');
|
||||
throw new Error(
|
||||
"useMapWidgetContext must be used within a MapWidgetProvider"
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
@ -1,15 +1,20 @@
|
||||
import { ComposableMap, ZoomableGroup, ZoomableGroupProps } from 'react-simple-maps';
|
||||
import styles from './MapWidget.module.css';
|
||||
import { mapCanvasProps, useMapWidgetContext } from '../../MapWidgetContext';
|
||||
import { useState } from 'react';
|
||||
import { MapContent } from './MapContent';
|
||||
import {
|
||||
ComposableMap,
|
||||
ZoomableGroup,
|
||||
ZoomableGroupProps,
|
||||
} from "react-simple-maps";
|
||||
import styles from "./MapWidget.module.css";
|
||||
import { mapCanvasProps, useMapWidgetContext } from "../../MapWidgetContext";
|
||||
import { useState } from "react";
|
||||
import { MapContent } from "./MapContent";
|
||||
|
||||
// default coordinates for 3a route: 59.943, 30.331
|
||||
export const MapWidget = () => {
|
||||
const { onMapCenterMoved, projection, isDragMode, rotateAngle } = useMapWidgetContext();
|
||||
const { onMapCenterMoved, projection, isDragMode, rotateAngle } =
|
||||
useMapWidgetContext();
|
||||
const [key, setKey] = useState(42);
|
||||
|
||||
const handleMoveEnd: ZoomableGroupProps['onMoveEnd'] = (e, d3Zoom) => {
|
||||
const handleMoveEnd: ZoomableGroupProps["onMoveEnd"] = (e, d3Zoom) => {
|
||||
const { PI, cos, sin } = Math;
|
||||
const { x, y } = d3Zoom.transform;
|
||||
const { width, height } = mapCanvasProps;
|
@ -0,0 +1,40 @@
|
||||
import { Marker, Point } from "react-simple-maps";
|
||||
import { Icons } from "@mt/components";
|
||||
import { AttractionGroupIconSizeType } from "@mt/common-types";
|
||||
|
||||
import styles from "./AttractionMarker.module.css";
|
||||
import cn from "classnames";
|
||||
|
||||
interface Props {
|
||||
coordinates: Point;
|
||||
rotate: number;
|
||||
size: AttractionGroupIconSizeType;
|
||||
counter?: number;
|
||||
}
|
||||
|
||||
export const AttractionMarker = ({
|
||||
coordinates,
|
||||
counter = 0,
|
||||
rotate,
|
||||
size,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Marker coordinates={coordinates}>
|
||||
<foreignObject
|
||||
className={cn({
|
||||
[styles.markerLarge]: size === "LARGE",
|
||||
[styles.markerSmall]: size === "SMALL",
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="g-transform-origin__center"
|
||||
style={{ transform: `rotate(${rotate}deg)` }}
|
||||
>
|
||||
<Icons.AttractionIcon className={styles.icon} />
|
||||
|
||||
{counter > 1 && <div className={styles.counter}>{counter}</div>}
|
||||
</div>
|
||||
</foreignObject>
|
||||
</Marker>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { AttractionMarker } from './AttractionMarker';
|
||||
import { getMapPoint } from '../../utils';
|
||||
import { useMapWidgetContext } from '../../MapWidgetContext';
|
||||
import { AttractionMarker } from "./AttractionMarker";
|
||||
import { getMapPoint } from "../../utils";
|
||||
import { useMapWidgetContext } from "../../MapWidgetContext";
|
||||
|
||||
export const TrackAttractions = () => {
|
||||
const { attractionGroups, rotateAngle } = useMapWidgetContext();
|
@ -1,11 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Line, Point } from 'react-simple-maps';
|
||||
import { getMapPoint } from '../utils';
|
||||
import { useMapWidgetContext } from '../MapWidgetContext';
|
||||
import { zeroCoordinates } from '../map-widget.constant';
|
||||
import { useMemo } from "react";
|
||||
import { Line, Point } from "react-simple-maps";
|
||||
import { getMapPoint } from "../utils";
|
||||
import { useMapWidgetContext } from "../MapWidgetContext";
|
||||
import { zeroCoordinates } from "../map-widget.constant";
|
||||
|
||||
const passedTrackColor = '#ed1c24';
|
||||
const trackColor = '#cccccc';
|
||||
const passedTrackColor = "#ed1c24";
|
||||
const trackColor = "#cccccc";
|
||||
|
||||
export const TrackLine = () => {
|
||||
const { track, passedTrackIndex, currentPosition } = useMapWidgetContext();
|
||||
@ -16,7 +16,12 @@ export const TrackLine = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Line coordinates={mappedTrack} strokeWidth={2.5} strokeLinecap="round" stroke={trackColor} />
|
||||
<Line
|
||||
coordinates={mappedTrack}
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
stroke={trackColor}
|
||||
/>
|
||||
|
||||
<Line
|
||||
coordinates={[
|
@ -1,27 +1,37 @@
|
||||
import { Marker } from 'react-simple-maps';
|
||||
import { Marker } from "react-simple-maps";
|
||||
// TODO: resolve circular deps
|
||||
import type { uuid } from '@mt/common-types';
|
||||
import type { uuid } from "@mt/common-types";
|
||||
|
||||
import { getMapPoint } from '../../utils';
|
||||
import { getMapPoint } from "../../utils";
|
||||
|
||||
import './TrackStations.css';
|
||||
import { StationLabelEdit } from './StationLabelEdit';
|
||||
import { useMapWidgetContext } from '../../MapWidgetContext';
|
||||
import { StationLabel } from './StationLabel';
|
||||
import "./TrackStations.css";
|
||||
import { StationLabelEdit } from "./StationLabelEdit";
|
||||
import { useMapWidgetContext } from "../../MapWidgetContext";
|
||||
import { StationLabel } from "./StationLabel";
|
||||
|
||||
const colors = {
|
||||
black: '#000000',
|
||||
red: '#ed1c24',
|
||||
grey: '#cccccc',
|
||||
yellow: '#fcd500',
|
||||
black: "#000000",
|
||||
red: "#ed1c24",
|
||||
grey: "#cccccc",
|
||||
yellow: "#fcd500",
|
||||
};
|
||||
|
||||
export const TrackStations = () => {
|
||||
const { stations, currentStation, passedTrackIndex, isOnStation, isEditMode } =
|
||||
useMapWidgetContext();
|
||||
const isTerminalStation = (index: number) => index === 0 || index === stations.length - 1;
|
||||
const {
|
||||
stations,
|
||||
currentStation,
|
||||
passedTrackIndex,
|
||||
isOnStation,
|
||||
isEditMode,
|
||||
} = useMapWidgetContext();
|
||||
const isTerminalStation = (index: number) =>
|
||||
index === 0 || index === stations.length - 1;
|
||||
|
||||
const getStationFill = (id: uuid, trackIndex: number, index: number): string => {
|
||||
const getStationFill = (
|
||||
id: uuid,
|
||||
trackIndex: number,
|
||||
index: number
|
||||
): string => {
|
||||
if (isOnStation && currentStation?.id === id) {
|
||||
return colors.yellow;
|
||||
}
|
||||
@ -49,7 +59,11 @@ export const TrackStations = () => {
|
||||
r={3.5}
|
||||
strokeWidth={isTerminalStation(index) ? 2 : 1.5}
|
||||
/>
|
||||
{isEditMode ? <StationLabelEdit station={it} /> : <StationLabel station={it} />}
|
||||
{isEditMode ? (
|
||||
<StationLabelEdit station={it} />
|
||||
) : (
|
||||
<StationLabel station={it} />
|
||||
)}
|
||||
</Marker>
|
||||
))}
|
||||
</>
|
@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getIntersection, getIntersectionArea } from '../../utils';
|
||||
import { Marker, Point } from 'react-simple-maps';
|
||||
import { Coordinates } from '@mt/common-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './TramMarker.module.css';
|
||||
import { Icons, useMapWidgetContext } from '@mt/components';
|
||||
import { useEffect, useState } from "react";
|
||||
import { getIntersection, getIntersectionArea } from "../../utils";
|
||||
import { Marker, Point } from "react-simple-maps";
|
||||
import { Coordinates } from "@mt/common-types";
|
||||
import cn from "classnames";
|
||||
import styles from "./TramMarker.module.css";
|
||||
import { Icons, useMapWidgetContext } from "@mt/components";
|
||||
|
||||
interface TramMarkerProps {
|
||||
coordinates: Point;
|
||||
@ -17,7 +17,9 @@ export const TramMarker = ({ coordinates, nextStopPoint }: TramMarkerProps) => {
|
||||
const { rotateAngle } = useMapWidgetContext();
|
||||
|
||||
useEffect(() => {
|
||||
const tramRect = document.getElementById('tram-marker')?.getBoundingClientRect();
|
||||
const tramRect = document
|
||||
.getElementById("tram-marker")
|
||||
?.getBoundingClientRect();
|
||||
const nextStopRect = document
|
||||
.getElementById(`${nextStopPoint.lat}:${nextStopPoint.lon}`)
|
||||
?.getBoundingClientRect();
|
||||
@ -36,7 +38,9 @@ export const TramMarker = ({ coordinates, nextStopPoint }: TramMarkerProps) => {
|
||||
|
||||
return (
|
||||
<Marker coordinates={coordinates} id="tram-marker">
|
||||
<foreignObject className={cn(styles.iconContainer, { [styles.flipped]: flipped })}>
|
||||
<foreignObject
|
||||
className={cn(styles.iconContainer, { [styles.flipped]: flipped })}
|
||||
>
|
||||
<Icons.TramMarkerIcon
|
||||
className={`${styles.icon} g-transform-origin__center`}
|
||||
// Inverse angle to compensate map rotation
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { StationOnMap } from '../map-widget.interface';
|
||||
import { getDistance } from '../utils';
|
||||
import { Coordinates } from '@mt/common-types';
|
||||
import { useEffect, useState } from "react";
|
||||
import { StationOnMap } from "../map-widget.interface";
|
||||
import { getDistance } from "../utils";
|
||||
import { Coordinates } from "@mt/common-types";
|
||||
|
||||
const ZOOM_DISTANCE = 100;
|
||||
const ON_STATION_DISTANCE = 15;
|
||||
@ -12,7 +12,9 @@ export function useNearStation(
|
||||
passedTrackIndex: number
|
||||
) {
|
||||
const [nextStation, setNextStation] = useState<StationOnMap | null>(null);
|
||||
const [currentStation, setCurrentStation] = useState<StationOnMap | null>(null);
|
||||
const [currentStation, setCurrentStation] = useState<StationOnMap | null>(
|
||||
null
|
||||
);
|
||||
const [isOnStation, setIsOnStation] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
@ -1,11 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Coordinates, Track } from '@mt/common-types';
|
||||
import { useEffect, useState } from "react";
|
||||
import { Coordinates, Track } from "@mt/common-types";
|
||||
|
||||
import { getDistance, getPointDeviation } from '../utils';
|
||||
import { getDistance, getPointDeviation } from "../utils";
|
||||
|
||||
const APPROXIMATE_DISTANCE = 15; // [meters] half of tramway length (~30 meters)
|
||||
|
||||
export function usePassedTrackIndex(track: Track | null, currentPosition: Coordinates | null) {
|
||||
export function usePassedTrackIndex(
|
||||
track: Track | null,
|
||||
currentPosition: Coordinates | null
|
||||
) {
|
||||
const [passedTrackIndex, setPassedTrackIndex] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
@ -29,7 +32,9 @@ export function usePassedTrackIndex(track: Track | null, currentPosition: Coordi
|
||||
* Is current position more than APPROXIMATE_DISTANCE far from found track point
|
||||
* we need to check that we really reach newPassedIndex. If not — should decrement index
|
||||
*/
|
||||
if (getDistance(track[newPassedIndex], currentPosition) > APPROXIMATE_DISTANCE) {
|
||||
if (
|
||||
getDistance(track[newPassedIndex], currentPosition) > APPROXIMATE_DISTANCE
|
||||
) {
|
||||
const prevIndex = Math.max(newPassedIndex - 1, 0);
|
||||
const nextIndex = Math.min(newPassedIndex + 1, track.length - 1);
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { Coordinates, SetState, uuid } from '@mt/common-types';
|
||||
import { MapData, StationOnMap } from '@mt/components';
|
||||
import { GeoProjection } from 'd3-geo';
|
||||
import { Point } from 'react-simple-maps';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { RouteStation } from '@admin/types';
|
||||
import { Coordinates, SetState, uuid } from "@mt/common-types";
|
||||
import { MapData, StationOnMap } from "@mt/components";
|
||||
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { RouteStation } from "@mt/common-types";
|
||||
import { GeoProjection } from "d3-geo";
|
||||
|
||||
export interface MapWidgetContextType {
|
||||
// External data
|
||||
track: MapData['trackPoints'] | null;
|
||||
stations: MapData['stationsOnMap'];
|
||||
attractionGroups: MapData['touristAttractionGroupsOnMap'];
|
||||
rotateAngle: MapData['mapRotateAngle'];
|
||||
track: MapData["trackPoints"] | null;
|
||||
stations: MapData["stationsOnMap"];
|
||||
attractionGroups: MapData["touristAttractionGroupsOnMap"];
|
||||
rotateAngle: MapData["mapRotateAngle"];
|
||||
center: Coordinates;
|
||||
projection: GeoProjection;
|
||||
currentPosition: Coordinates | null;
|
||||
@ -24,6 +24,7 @@ export interface MapWidgetContextType {
|
||||
setCurrentPosition: SetState<Coordinates | null>;
|
||||
|
||||
isDragMode: boolean;
|
||||
|
||||
setIsDragMode: SetState<boolean>;
|
||||
isEditMode: boolean;
|
||||
setIsEditMode: SetState<boolean>;
|
||||
@ -34,7 +35,7 @@ export interface MapWidgetContextType {
|
||||
onMapCenterMoved: (center: Coordinates) => void;
|
||||
onStationUpdate: (
|
||||
stationId: uuid,
|
||||
data: Partial<Pick<StationOnMap, 'labelAlignment' | 'labelOffset'>>
|
||||
data: Partial<Pick<StationOnMap, "labelAlignment" | "labelOffset">>
|
||||
) => void;
|
||||
getUpdatedStations: () => Partial<RouteStation>;
|
||||
isMapDataChanged: boolean;
|
@ -1,4 +1,4 @@
|
||||
import { MapSettings } from './map-widget-context.interface';
|
||||
import { MapSettings } from "./map-widget-context.interface";
|
||||
|
||||
export const zeroCoordinates = { lat: 0, lon: 0 };
|
||||
|
@ -4,8 +4,8 @@ import {
|
||||
StationOnMap as StationOnMapBase,
|
||||
Track,
|
||||
uuid,
|
||||
} from '@mt/common-types';
|
||||
import { Transfer } from '@front/types';
|
||||
Transfer,
|
||||
} from "@mt/common-types";
|
||||
|
||||
export type PointOnTrack = Coordinates & {
|
||||
trackIndex: number;
|
@ -1,8 +1,8 @@
|
||||
import React, { HTMLAttributes, useContext, useEffect, useRef } from 'react';
|
||||
import { LocalizationContext, LocalizedString } from '@mt/i18n';
|
||||
import React, { HTMLAttributes, useContext, useEffect, useRef } from "react";
|
||||
import { LocalizationContext, LocalizedString } from "@mt/i18n";
|
||||
|
||||
import styles from './RouteInfoWidget.module.css';
|
||||
import cn from 'classnames';
|
||||
import styles from "./RouteInfoWidget.module.css";
|
||||
import cn from "classnames";
|
||||
|
||||
export interface RouteInfoData {
|
||||
routeNumber: string;
|
||||
@ -13,7 +13,11 @@ interface RouteInfoWidgetProps extends HTMLAttributes<HTMLDivElement> {
|
||||
routeInfo?: RouteInfoData;
|
||||
}
|
||||
|
||||
export function RouteInfoWidget({ routeInfo, className, ...props }: RouteInfoWidgetProps) {
|
||||
export function RouteInfoWidget({
|
||||
routeInfo,
|
||||
className,
|
||||
...props
|
||||
}: RouteInfoWidgetProps) {
|
||||
const contentContainerRef = useRef<HTMLDivElement>(null);
|
||||
const titleRefs = useRef<Array<HTMLSpanElement>>([]);
|
||||
|
||||
@ -42,7 +46,7 @@ export function RouteInfoWidget({ routeInfo, className, ...props }: RouteInfoWid
|
||||
|
||||
return (
|
||||
<div className={cn(styles.root, className)} {...props}>
|
||||
<div className={styles.number}>{routeInfo?.routeNumber || '--'}</div>
|
||||
<div className={styles.number}>{routeInfo?.routeNumber || "--"}</div>
|
||||
|
||||
{routeInfo ? (
|
||||
<div className={styles.content} ref={contentContainerRef}>
|
||||
@ -52,11 +56,13 @@ export function RouteInfoWidget({ routeInfo, className, ...props }: RouteInfoWid
|
||||
</span>
|
||||
</div>
|
||||
<div className={cn(styles.title, styles.titleEnd)}>
|
||||
<span ref={(ref) => (titleRefs.current[1] = ref!)}>{routeInfo.lastStationName.ru}</span>
|
||||
<span ref={(ref) => (titleRefs.current[1] = ref!)}>
|
||||
{routeInfo.lastStationName.ru}
|
||||
</span>
|
||||
</div>
|
||||
<div className={cn(styles.title, styles.titleTranslation)}>
|
||||
<span ref={(ref) => (titleRefs.current[2] = ref!)}>
|
||||
{locale === 'zh'
|
||||
{locale === "zh"
|
||||
? `${routeInfo.firstStationName.zh} – ${routeInfo.lastStationName.zh}`
|
||||
: `${routeInfo.firstStationName.en} – ${routeInfo.lastStationName.en}`}
|
||||
</span>
|
@ -1,9 +1,17 @@
|
||||
import { HTMLAttributes, PointerEvent, WheelEvent, useEffect, useRef, useState } from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './TouchScrollWrapper.module.css';
|
||||
import { useCssProperty } from '@mt/utils';
|
||||
import {
|
||||
HTMLAttributes,
|
||||
PointerEvent,
|
||||
WheelEvent,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import cn from "classnames";
|
||||
import styles from "./TouchScrollWrapper.module.css";
|
||||
import { useCssProperty } from "@mt/utils";
|
||||
|
||||
const getNumberPxFormatter = (numberStr: string | null) => Number(numberStr?.replace(/px$/, ''));
|
||||
const getNumberPxFormatter = (numberStr: string | null) =>
|
||||
Number(numberStr?.replace(/px$/, ""));
|
||||
const setNumberPxFormatter = (number: number) => `${number}px`;
|
||||
|
||||
const { abs, min } = Math;
|
||||
@ -18,14 +26,17 @@ export const TouchScrollWrapper = ({
|
||||
const scrollbarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollbarHeight = useCssProperty<number>(
|
||||
'--scrollbar-height',
|
||||
"--scrollbar-height",
|
||||
scrollbarRef,
|
||||
setNumberPxFormatter,
|
||||
getNumberPxFormatter
|
||||
);
|
||||
const scrollbarVisibility = useCssProperty<string>('--scrollbar-visibility', scrollbarRef);
|
||||
const scrollbarVisibility = useCssProperty<string>(
|
||||
"--scrollbar-visibility",
|
||||
scrollbarRef
|
||||
);
|
||||
const scrollbarOffset = useCssProperty<number>(
|
||||
'--scrollbar-offset',
|
||||
"--scrollbar-offset",
|
||||
scrollbarRef,
|
||||
setNumberPxFormatter
|
||||
);
|
||||
@ -57,10 +68,11 @@ export const TouchScrollWrapper = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (containerHeight >= contentHeight) {
|
||||
scrollbarVisibility.value = 'hidden';
|
||||
scrollbarVisibility.value = "hidden";
|
||||
} else {
|
||||
scrollbarHeight.value = (containerHeight / contentHeight) * containerHeight + 1;
|
||||
scrollbarVisibility.value = 'visible';
|
||||
scrollbarHeight.value =
|
||||
(containerHeight / contentHeight) * containerHeight + 1;
|
||||
scrollbarVisibility.value = "visible";
|
||||
}
|
||||
}, [contentHeight, containerHeight]);
|
||||
|
||||
@ -73,7 +85,7 @@ export const TouchScrollWrapper = ({
|
||||
const swipeDistance = startSwipeY - e.clientY;
|
||||
|
||||
if (
|
||||
e.pointerType !== 'touch' ||
|
||||
e.pointerType !== "touch" ||
|
||||
containerHeight >= contentHeight ||
|
||||
(pointerId && pointerId !== e.pointerId) ||
|
||||
abs(swipeDistance) < 2
|
Before Width: | Height: | Size: 422 B After Width: | Height: | Size: 422 B |
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 714 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 556 B After Width: | Height: | Size: 556 B |
Before Width: | Height: | Size: 599 B After Width: | Height: | Size: 599 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@ -1,11 +1,11 @@
|
||||
import { createElement } from 'react';
|
||||
import { ReactComponent as IconCloudy } from './icons/cond_cloudy.svg';
|
||||
import { ReactComponent as IconRainy } from './icons/cond_rainy.svg';
|
||||
import { ReactComponent as IconPartlyCloudy } from './icons/cond_partlycloudy.svg';
|
||||
import { ReactComponent as IconSnow } from './icons/cond_snow.svg';
|
||||
import { ReactComponent as IconSnowy } from './icons/cond_snowy.svg';
|
||||
import { ReactComponent as IconSunny } from './icons/cond_sunny.svg';
|
||||
import { ReactComponent as IconThunder } from './icons/cond_thunder.svg';
|
||||
import { createElement } from "react";
|
||||
import IconCloudy from "./icons/cond_cloudy.svg";
|
||||
import IconRainy from "./icons/cond_rainy.svg";
|
||||
import IconPartlyCloudy from "./icons/cond_partlycloudy.svg";
|
||||
import IconSnow from "./icons/cond_snow.svg";
|
||||
import IconSnowy from "./icons/cond_snowy.svg";
|
||||
import IconSunny from "./icons/cond_sunny.svg";
|
||||
import IconThunder from "./icons/cond_thunder.svg";
|
||||
|
||||
interface WeatherWidgetIconProps {
|
||||
icon: string | null;
|
||||
@ -30,11 +30,11 @@ export function WeatherWidgetIcon({ icon, size = 16 }: WeatherWidgetIconProps) {
|
||||
style={{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
textAlign: 'center',
|
||||
margin: '0 auto',
|
||||
textAlign: "center",
|
||||
margin: "0 auto",
|
||||
fontSize: `${size}px`,
|
||||
lineHeight: 1,
|
||||
overflow: 'hidden',
|
||||
overflow: "hidden",
|
||||
}}
|
||||
children="--"
|
||||
/>
|
||||
@ -43,6 +43,6 @@ export function WeatherWidgetIcon({ icon, size = 16 }: WeatherWidgetIconProps) {
|
||||
return createElement(svg, {
|
||||
width: size,
|
||||
height: size,
|
||||
style: { margin: '0 auto', display: 'block' },
|
||||
style: { margin: "0 auto", display: "block" },
|
||||
});
|
||||
}
|
@ -1,16 +1,16 @@
|
||||
import { WeatherWidgetIcon } from './weather-widget-icon';
|
||||
import { WeatherWidgetIcon } from "./weather-widget-icon";
|
||||
|
||||
import { ReactComponent as IconHumidity } from './icons/det_humidity.svg';
|
||||
import { ReactComponent as IconWind } from './icons/det_wind.svg';
|
||||
import { WeatherDayRow, WeatherWidgetData } from './weather.interface';
|
||||
import IconHumidity from "./icons/det_humidity.svg";
|
||||
import IconWind from "./icons/det_wind.svg";
|
||||
import { WeatherDayRow, WeatherWidgetData } from "./weather.interface";
|
||||
|
||||
const Weekdays = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
||||
const Weekdays = ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
|
||||
|
||||
const WRow = ({ temperature, weekday, condition }: WeatherDayRow) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
marginBottom: '8px',
|
||||
display: "flex",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@ -22,26 +22,29 @@ const WRow = ({ temperature, weekday, condition }: WeatherDayRow) => (
|
||||
<WeatherWidgetIcon icon={condition} size={16} />
|
||||
</div>
|
||||
<div style={{ marginLeft: 8, minWidth: 22 }} children={Weekdays[weekday]} />
|
||||
<div style={{ marginLeft: 8 }} children={`${temperature ?? '--'}°`} />
|
||||
<div style={{ marginLeft: 8 }} children={`${temperature ?? "--"}°`} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export function WeatherWidgetRight({ forecasts, weatherInfo }: WeatherWidgetData) {
|
||||
export function WeatherWidgetRight({
|
||||
forecasts,
|
||||
weatherInfo,
|
||||
}: WeatherWidgetData) {
|
||||
const wd = new Date().getDay();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
width: '50%',
|
||||
textAlign: "center",
|
||||
width: "50%",
|
||||
fontSize: 18,
|
||||
lineHeight: '21px',
|
||||
lineHeight: "21px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '1px solid #999',
|
||||
marginBottom: '8px',
|
||||
marginTop: '8px',
|
||||
borderBottom: "1px solid #999",
|
||||
marginBottom: "8px",
|
||||
marginTop: "8px",
|
||||
}}
|
||||
>
|
||||
{[...forecasts].slice(0, 3).map((d, idx) => (
|
||||
@ -51,22 +54,22 @@ export function WeatherWidgetRight({ forecasts, weatherInfo }: WeatherWidgetData
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<IconHumidity width={16} height={16} style={{ marginRight: 8 }} />
|
||||
<b children={weatherInfo?.humidity ?? '--'} />%
|
||||
<b children={weatherInfo?.humidity ?? "--"} />%
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<IconWind width={16} height={16} style={{ marginRight: 8 }} />
|
||||
<b children={weatherInfo?.windSpeed ?? '--'} />
|
||||
<b children={weatherInfo?.windSpeed ?? "--"} />
|
||||
м/с
|
||||
</div>
|
||||
</div>
|