fetching data from api for route preview
This commit is contained in:
		
							
								
								
									
										661
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										661
									
								
								src/App.tsx
									
									
									
									
									
								
							| @@ -78,355 +78,360 @@ import { AdminOnly } from "./components/AdminOnly"; | ||||
| import { Dashboard } from "./preview/widgets/dashboard/Dashboard"; | ||||
| import { QueryClient } from "@tanstack/react-query"; | ||||
| import { QueryClientProvider } from "@tanstack/react-query"; | ||||
| import { LoadingProvider } from "@mt/utils"; | ||||
| import { RoutePreview } from "./preview/components/route-preview/components/RoutePreview"; | ||||
|  | ||||
| const queryClient = new QueryClient(); | ||||
| function App() { | ||||
|   return ( | ||||
|     <QueryClientProvider client={queryClient}> | ||||
|       <BrowserRouter> | ||||
|         <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 />, | ||||
|       <LoadingProvider> | ||||
|         <BrowserRouter> | ||||
|           <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", | ||||
|                 }} | ||||
|               > | ||||
|                 <Routes> | ||||
|                   <Route | ||||
|                     element={ | ||||
|                       <Authenticated | ||||
|                         key="authenticated-inner" | ||||
|                         fallback={<CatchAllNavigate to="/login" />} | ||||
|                       > | ||||
|                         <ThemedLayoutV2 Header={Header} Title={SidebarTitle}> | ||||
|                           <Outlet /> | ||||
|                         </ThemedLayoutV2> | ||||
|                       </Authenticated> | ||||
|                     } | ||||
|                   > | ||||
|                   ]} | ||||
|                   options={{ | ||||
|                     syncWithLocation: true, | ||||
|                     warnWhenUnsavedChanges: true, // Включаем глобально | ||||
|                     useNewQueryKeys: true, | ||||
|                     projectId: "Wv044J-t53S3s-PcbJGe", | ||||
|                   }} | ||||
|                 > | ||||
|                   <Routes> | ||||
|                     <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="dashboard" element={<Dashboard />} /> | ||||
|  | ||||
|                     <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"> | ||||
|                       element={ | ||||
|                         <Authenticated | ||||
|                           key="authenticated-inner" | ||||
|                           fallback={<CatchAllNavigate to="/login" />} | ||||
|                         > | ||||
|                           <ThemedLayoutV2 Header={Header} Title={SidebarTitle}> | ||||
|                             <Outlet /> | ||||
|                           </ThemedLayoutV2> | ||||
|                         </Authenticated> | ||||
|                       } | ||||
|                     > | ||||
|                       <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> | ||||
|                         } | ||||
|                         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="dashboard" element={<Dashboard />} /> | ||||
|  | ||||
|                       <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 path="preview/:id" element={<RoutePreview />} /> | ||||
|                       </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> | ||||
|  | ||||
|                     <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 "Белые ночи"; | ||||
|                   }} | ||||
|                 /> | ||||
|               </Refine> | ||||
|               <DevtoolsPanel /> | ||||
|             </DevtoolsProvider> | ||||
|           </RefineSnackbarProvider> | ||||
|         </ColorModeContextProvider> | ||||
|       </BrowserRouter> | ||||
|                   <UnsavedChangesNotifier /> | ||||
|                   <DocumentTitleHandler | ||||
|                     handler={() => { | ||||
|                       // const cleanedTitle = title.autoGeneratedTitle.split('|')[0].trim() | ||||
|                       // return `${cleanedTitle} — Белые ночи` | ||||
|                       return "Белые ночи"; | ||||
|                     }} | ||||
|                   /> | ||||
|                 </Refine> | ||||
|                 <DevtoolsPanel /> | ||||
|               </DevtoolsProvider> | ||||
|             </RefineSnackbarProvider> | ||||
|           </ColorModeContextProvider> | ||||
|         </BrowserRouter> | ||||
|       </LoadingProvider> | ||||
|     </QueryClientProvider> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -48,6 +48,7 @@ type LinkedItemsProps<T> = { | ||||
|   title: string; | ||||
|   type: "show" | "edit"; | ||||
|   extraField?: ExtraFieldConfig; | ||||
|   dragAllowed?: boolean; | ||||
| }; | ||||
|  | ||||
| const reorder = (list: any[], startIndex: number, endIndex: number) => { | ||||
| @@ -63,6 +64,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({ | ||||
|   childResource, | ||||
|   fields, | ||||
|   title, | ||||
|   dragAllowed = false, | ||||
|   type, | ||||
| }: LinkedItemsProps<T>) => { | ||||
|   const [items, setItems] = useState<T[]>([]); | ||||
| @@ -84,6 +86,16 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({ | ||||
|  | ||||
|     setLinkedItems(reorderedItems); | ||||
|  | ||||
|     axiosInstance.post( | ||||
|       `${import.meta.env.VITE_KRBL_API}/route/${parentId}/station`, | ||||
|       { | ||||
|         after_station: 3, | ||||
|         offset_x: 90, | ||||
|         offset_y: 15, | ||||
|         station_id: linkedItems[result.destination.index].id + 1, | ||||
|       } | ||||
|     ); | ||||
|  | ||||
|     // If you need to save the new order to the backend, you would add that here | ||||
|     // For example: | ||||
|     // saveNewOrder(reorderedItems); | ||||
| @@ -217,7 +229,9 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({ | ||||
|               <Table> | ||||
|                 <TableHead> | ||||
|                   <TableRow> | ||||
|                     {type === "edit" && <TableCell width="40px"></TableCell>} | ||||
|                     {type === "edit" && dragAllowed && ( | ||||
|                       <TableCell width="40px"></TableCell> | ||||
|                     )} | ||||
|                     {fields.map((field) => ( | ||||
|                       <TableCell key={String(field.data)}> | ||||
|                         {field.label} | ||||
| @@ -239,7 +253,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({ | ||||
|                           key={item.id} | ||||
|                           draggableId={"q" + String(item.id)} | ||||
|                           index={index} | ||||
|                           isDragDisabled={type !== "edit"} | ||||
|                           isDragDisabled={type !== "edit" && dragAllowed} | ||||
|                         > | ||||
|                           {(provided) => ( | ||||
|                             <TableRow | ||||
| @@ -247,7 +261,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({ | ||||
|                               {...provided.draggableProps} | ||||
|                               hover | ||||
|                             > | ||||
|                               {type === "edit" && ( | ||||
|                               {type === "edit" && dragAllowed && ( | ||||
|                                 <TableCell {...provided.dragHandleProps}> | ||||
|                                   <IconButton size="small"> | ||||
|                                     <DragIndicatorIcon /> | ||||
|   | ||||
| @@ -43,35 +43,51 @@ export const MediaShow = () => { | ||||
|         )} | ||||
|  | ||||
|         {record && record.media_type === 2 && ( | ||||
|           <Box | ||||
|             sx={{ | ||||
|               p: 2, | ||||
|               border: "1px solid text.pimary", | ||||
|               borderRadius: 2, | ||||
|               bgcolor: "primary.light", | ||||
|               width: "fit-content", | ||||
|             }} | ||||
|           > | ||||
|             <Typography | ||||
|               variant="body1" | ||||
|               gutterBottom | ||||
|               sx={{ | ||||
|                 color: "#FFFFFF", | ||||
|               }} | ||||
|             > | ||||
|               Видео доступно для скачивания по ссылке: | ||||
|             </Typography> | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               href={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||
|           <> | ||||
|             <video | ||||
|               src={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||
|                 record?.id | ||||
|               }/download?token=${token}`} | ||||
|               target="_blank" | ||||
|               sx={{ mt: 1, width: "100%" }} | ||||
|               style={{ | ||||
|                 maxWidth: "50%", | ||||
|  | ||||
|                 objectFit: "contain", | ||||
|                 borderRadius: 30, | ||||
|               }} | ||||
|               controls | ||||
|               autoPlay | ||||
|               muted | ||||
|             /> | ||||
|             <Box | ||||
|               sx={{ | ||||
|                 p: 2, | ||||
|                 border: "1px solid text.pimary", | ||||
|                 borderRadius: 2, | ||||
|                 bgcolor: "primary.light", | ||||
|                 width: "fit-content", | ||||
|               }} | ||||
|             > | ||||
|               Скачать видео | ||||
|             </Button> | ||||
|           </Box> | ||||
|               <Typography | ||||
|                 variant="body1" | ||||
|                 gutterBottom | ||||
|                 sx={{ | ||||
|                   color: "#FFFFFF", | ||||
|                 }} | ||||
|               > | ||||
|                 Видео доступно для скачивания по ссылке: | ||||
|               </Typography> | ||||
|               <Button | ||||
|                 variant="contained" | ||||
|                 href={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||
|                   record?.id | ||||
|                 }/download?token=${token}`} | ||||
|                 target="_blank" | ||||
|                 sx={{ mt: 1, width: "100%" }} | ||||
|               > | ||||
|                 Скачать видео | ||||
|               </Button> | ||||
|             </Box> | ||||
|           </> | ||||
|         )} | ||||
|  | ||||
|         {fields.map(({ label, data, render }) => ( | ||||
|   | ||||
| @@ -134,17 +134,14 @@ export const RouteEdit = () => { | ||||
|           {...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 { | ||||
| @@ -305,6 +302,7 @@ export const RouteEdit = () => { | ||||
|             childResource="station" | ||||
|             fields={stationFields} | ||||
|             title="станции" | ||||
|             dragAllowed={true} | ||||
|           /> | ||||
|  | ||||
|           <LinkedItems<VehicleItem> | ||||
|   | ||||
| @@ -1,67 +1,93 @@ | ||||
| import {Stack, Typography, Box} from '@mui/material' | ||||
| import {useShow} from '@refinedev/core' | ||||
| import {Show, TextFieldComponent as TextField} from '@refinedev/mui' | ||||
| import {LinkedItems} from '../../components/LinkedItems' | ||||
| import {StationItem, VehicleItem, stationFields, vehicleFields} from './types' | ||||
| import { Stack, Typography, Box } from "@mui/material"; | ||||
| import { useShow } from "@refinedev/core"; | ||||
| import { Show, TextFieldComponent as TextField } from "@refinedev/mui"; | ||||
| import { LinkedItems } from "../../components/LinkedItems"; | ||||
| import { | ||||
|   StationItem, | ||||
|   VehicleItem, | ||||
|   stationFields, | ||||
|   vehicleFields, | ||||
| } from "./types"; | ||||
|  | ||||
| export const RouteShow = () => { | ||||
|   const {query} = useShow({}) | ||||
|   const {data, isLoading} = query | ||||
|   const record = data?.data | ||||
|   const { query } = useShow({}); | ||||
|   const { data, isLoading } = query; | ||||
|   const record = data?.data; | ||||
|  | ||||
|   const fields = [ | ||||
|     {label: 'Перевозчик', data: 'carrier'}, | ||||
|     {label: 'Номер маршрута', data: 'route_number'}, | ||||
|     { label: "Перевозчик", data: "carrier" }, | ||||
|     { label: "Номер маршрута", data: "route_number" }, | ||||
|     { | ||||
|       label: 'Направление маршрута', | ||||
|       data: 'route_direction', | ||||
|       render: (value: number[][]) => <Typography style={{color: value ? '#48989f' : '#7f6b58'}}>{value ? 'прямое' : 'обратное'}</Typography>, | ||||
|       label: "Направление маршрута", | ||||
|       data: "route_direction", | ||||
|       render: (value: number[][]) => ( | ||||
|         <Typography style={{ color: value ? "#48989f" : "#7f6b58" }}> | ||||
|           {value ? "прямое" : "обратное"} | ||||
|         </Typography> | ||||
|       ), | ||||
|     }, | ||||
|     { | ||||
|       label: 'Координаты маршрута', | ||||
|       data: 'path', | ||||
|       label: "Координаты маршрута", | ||||
|       data: "path", | ||||
|       render: (value: number[][]) => ( | ||||
|         <Box | ||||
|           sx={{ | ||||
|             fontFamily: 'monospace', | ||||
|             fontFamily: "monospace", | ||||
|             bgcolor: (theme) => theme.palette.background.paper, | ||||
|             p: 2, | ||||
|             borderRadius: 1, | ||||
|             maxHeight: '200px', | ||||
|             overflow: 'auto', | ||||
|             maxHeight: "200px", | ||||
|             overflow: "auto", | ||||
|           }} | ||||
|         > | ||||
|           {JSON.stringify(value)} | ||||
|           {/* {value?.map((point, index) => ( | ||||
|             <Typography key={index} sx={{mb: 0.5}}> | ||||
|               Точка {index + 1}: [{point[0]}, {point[1]}] | ||||
|           {value?.map((point, index) => ( | ||||
|             <Typography key={index} sx={{ mb: 0.5 }}> | ||||
|               {point[0]}, {point[1]} | ||||
|             </Typography> | ||||
|           ))} */} | ||||
|           ))} | ||||
|         </Box> | ||||
|       ), | ||||
|     }, | ||||
|   ] | ||||
|   ]; | ||||
|  | ||||
|   return ( | ||||
|     <Show isLoading={isLoading}> | ||||
|       <Stack gap={4}> | ||||
|         {fields.map(({label, data, render}) => ( | ||||
|         {fields.map(({ label, data, render }) => ( | ||||
|           <Stack key={data} gap={1}> | ||||
|             <Typography variant="body1" fontWeight="bold"> | ||||
|               {label} | ||||
|             </Typography> | ||||
|             {render ? render(record?.[data]) : <TextField value={record?.[data]} />} | ||||
|             {render ? ( | ||||
|               render(record?.[data]) | ||||
|             ) : ( | ||||
|               <TextField value={record?.[data]} /> | ||||
|             )} | ||||
|           </Stack> | ||||
|         ))} | ||||
|  | ||||
|         {record?.id && ( | ||||
|           <> | ||||
|             <LinkedItems<StationItem> type="show" parentId={record.id} parentResource="route" childResource="station" fields={stationFields} title="станции" /> | ||||
|             <LinkedItems<StationItem> | ||||
|               type="show" | ||||
|               parentId={record.id} | ||||
|               parentResource="route" | ||||
|               childResource="station" | ||||
|               fields={stationFields} | ||||
|               title="станции" | ||||
|             /> | ||||
|  | ||||
|             <LinkedItems<VehicleItem> type="show" parentId={record.id} parentResource="route" childResource="vehicle" fields={vehicleFields} title="транспортные средства" /> | ||||
|             <LinkedItems<VehicleItem> | ||||
|               type="show" | ||||
|               parentId={record.id} | ||||
|               parentResource="route" | ||||
|               childResource="vehicle" | ||||
|               fields={vehicleFields} | ||||
|               title="транспортные средства" | ||||
|             /> | ||||
|           </> | ||||
|         )} | ||||
|       </Stack> | ||||
|     </Show> | ||||
|   ) | ||||
| } | ||||
|   ); | ||||
| }; | ||||
|   | ||||
							
								
								
									
										16
									
								
								src/preview/components/WeatherWidget/icons/DetHumidity.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/preview/components/WeatherWidget/icons/DetHumidity.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| export const DetHumidity = () => { | ||||
|   return ( | ||||
|     <svg | ||||
|       width="64" | ||||
|       height="64" | ||||
|       viewBox="0 0 64 64" | ||||
|       fill="none" | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|     > | ||||
|       <path | ||||
|         d="M32 63.68C19.42 63.68 9.19 53.45 9.19 40.87C9.19 28.25 22.63 9.87001 28.41 2.56001C29.28 1.45001 30.59 0.820007 32 0.820007C33.41 0.820007 34.72 1.45001 35.59 2.56001C41.37 9.88001 54.81 28.26 54.81 40.87C54.81 53.44 44.58 63.68 32 63.68ZM32 4.81001C31.9 4.81001 31.7 4.84001 31.55 5.03001C27.24 10.48 13.19 29.18 13.19 40.86C13.19 51.23 21.63 59.67 32 59.67C42.37 59.67 50.81 51.23 50.81 40.86C50.81 29.18 36.76 10.48 32.46 5.03001C32.3 4.84001 32.1 4.81001 32 4.81001Z" | ||||
|         fill="white" | ||||
|       /> | ||||
|     </svg> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										24
									
								
								src/preview/components/WeatherWidget/icons/DetWind.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/preview/components/WeatherWidget/icons/DetWind.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| export const DetWind = () => { | ||||
|   return ( | ||||
|     <svg | ||||
|       width="64" | ||||
|       height="64" | ||||
|       viewBox="0 0 64 64" | ||||
|       fill="none" | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|     > | ||||
|       <path | ||||
|         d="M22.03 23.32H4.59998C3.49998 23.32 2.59998 22.42 2.59998 21.32C2.59998 20.22 3.49998 19.32 4.59998 19.32H22.02C26 19.32 29.24 16.08 29.24 12.1C29.24 8.12001 26 4.88 22.02 4.88C18.04 4.88 14.8 8.12001 14.8 12.1C14.8 13.2 13.9 14.1 12.8 14.1C11.7 14.1 10.8 13.2 10.8 12.1C10.8 5.91001 15.84 0.880005 22.02 0.880005C28.2 0.880005 33.24 5.92001 33.24 12.1C33.24 18.28 28.21 23.32 22.03 23.32Z" | ||||
|         fill="white" | ||||
|       /> | ||||
|       <path | ||||
|         d="M50.17 34.3H14.15C13.05 34.3 12.15 33.4 12.15 32.3C12.15 31.2 13.05 30.3 14.15 30.3H50.17C54.15 30.3 57.39 27.06 57.39 23.08C57.39 19.1 54.15 15.86 50.17 15.86C46.19 15.86 42.95 19.1 42.95 23.08C42.95 24.18 42.05 25.08 40.95 25.08C39.85 25.08 38.95 24.18 38.95 23.08C38.95 16.89 43.99 11.86 50.17 11.86C56.36 11.86 61.39 16.9 61.39 23.08C61.39 29.26 56.36 34.3 50.17 34.3Z" | ||||
|         fill="white" | ||||
|       /> | ||||
|       <path | ||||
|         d="M40.63 63.13C34.44 63.13 29.41 58.09 29.41 51.91C29.41 50.81 30.31 49.91 31.41 49.91C32.51 49.91 33.41 50.81 33.41 51.91C33.41 55.89 36.65 59.13 40.63 59.13C44.61 59.13 47.85 55.89 47.85 51.91C47.85 47.93 44.61 44.69 40.63 44.69H4.59998C3.49998 44.69 2.59998 43.79 2.59998 42.69C2.59998 41.59 3.49998 40.69 4.59998 40.69H40.62C46.81 40.69 51.84 45.73 51.84 51.91C51.84 58.09 46.82 63.13 40.63 63.13Z" | ||||
|         fill="white" | ||||
|       /> | ||||
|     </svg> | ||||
|   ); | ||||
| }; | ||||
| @@ -40,9 +40,5 @@ export function WeatherWidgetIcon({ icon, size = 16 }: WeatherWidgetIconProps) { | ||||
|       /> | ||||
|     ); | ||||
|  | ||||
|   return createElement(svg, { | ||||
|     width: size, | ||||
|     height: size, | ||||
|     style: { margin: "0 auto", display: "block" }, | ||||
|   }); | ||||
|   return createElement(svg); | ||||
| } | ||||
|   | ||||
| @@ -59,7 +59,7 @@ export function WeatherWidgetRight({ | ||||
|           marginBottom: 8, | ||||
|         }} | ||||
|       > | ||||
|         <IconHumidity width={16} height={16} style={{ marginRight: 8 }} /> | ||||
|         <IconHumidity /> | ||||
|         <b children={weatherInfo?.humidity ?? "--"} />% | ||||
|       </div> | ||||
|       <div | ||||
| @@ -68,7 +68,7 @@ export function WeatherWidgetRight({ | ||||
|           alignItems: "center", | ||||
|         }} | ||||
|       > | ||||
|         <IconWind width={16} height={16} style={{ marginRight: 8 }} /> | ||||
|         <IconWind /> | ||||
|         <b children={weatherInfo?.windSpeed ?? "--"} /> | ||||
|          м/с | ||||
|       </div> | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| import { MapWidgetProvider } from "@mt/components"; | ||||
| import { RoutePreviewContainer } from "./RoutePreviewContainer"; | ||||
|  | ||||
| export const RoutePreview = () => ( | ||||
|   <MapWidgetProvider> | ||||
|     <RoutePreviewContainer /> | ||||
|   </MapWidgetProvider> | ||||
| ); | ||||
| @@ -0,0 +1,83 @@ | ||||
| import { useGetRouteData } from "../hooks/useGetRouteData"; | ||||
| import { useUpdateRouteData } from "../hooks/useUpdateRouteData"; | ||||
| import { | ||||
|   MapSettings, | ||||
|   MapWidget, | ||||
|   RouteInfoData, | ||||
|   useMapWidgetContext, | ||||
| } from "@mt/components"; | ||||
| import { SettingsPanel } from "./SettingsPanel/SettingsPanel"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { RoutePreviewDashboard } from "./RoutePreviewDashboard/RoutePreviewDashboard"; | ||||
| import { LocalizedStringDefaults } from "@mt/common-types"; | ||||
| import { useParams } from "react-router"; | ||||
|  | ||||
| export const RoutePreviewContainer = () => { | ||||
|   const { id } = useParams(); | ||||
|   const routeId = id as string; | ||||
|   const { routeData, mappedData, isLoading, isError } = | ||||
|     useGetRouteData(routeId); | ||||
|   // const updateRouteView = useUpdateRouteData(routeId, routeData); | ||||
|   const [routeInfo, setRouteInfo] = useState<RouteInfoData>(); | ||||
|  | ||||
|   const { | ||||
|     onMapDataFetched, | ||||
|     setCurrentPosition, | ||||
|     middleTrackCoordinates, | ||||
|     setIsEditMode, | ||||
|     getUpdatedStations, | ||||
|   } = useMapWidgetContext(); | ||||
|  | ||||
|   // const handleSubmit = (mapSettings: MapSettings) => { | ||||
|   //   updateRouteView(mapSettings, getUpdatedStations()); | ||||
|   // }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!mappedData) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     onMapDataFetched(mappedData); | ||||
|     setCurrentPosition(middleTrackCoordinates); | ||||
|     setIsEditMode(true); | ||||
|  | ||||
|     setRouteInfo({ | ||||
|       routeNumber: routeData.number, | ||||
|       firstStationName: | ||||
|         mappedData.stationsOnMap.at(0)?.shortName ?? LocalizedStringDefaults, | ||||
|       lastStationName: | ||||
|         mappedData.stationsOnMap.at(-1)?.shortName ?? LocalizedStringDefaults, | ||||
|     }); | ||||
|   }, [mappedData, middleTrackCoordinates]); | ||||
|  | ||||
|   if (isLoading) { | ||||
|     return ( | ||||
|       <div | ||||
|         className="g-flex-column g-flex--align-center g-flex--justify-center" | ||||
|         style={{ height: "1000px" }} | ||||
|       > | ||||
|         Загрузка... | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   if (isError) { | ||||
|     return ( | ||||
|       <div | ||||
|         className="g-flex-column g-flex--align-center g-flex--justify-center" | ||||
|         style={{ height: "1000px" }} | ||||
|       > | ||||
|         Ошибка получения данных | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   if (mappedData) { | ||||
|     return ( | ||||
|       <RoutePreviewDashboard routeInfo={routeInfo!}> | ||||
|         <MapWidget /> | ||||
|         {/* <SettingsPanel onSubmit={handleSubmit} /> */} | ||||
|       </RoutePreviewDashboard> | ||||
|     ); | ||||
|   } | ||||
| }; | ||||
| @@ -0,0 +1,79 @@ | ||||
| .root { | ||||
|   background-color: #000000; | ||||
|   width: 1920px; | ||||
|   height: 1080px; | ||||
|  | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
|   display: flex; | ||||
| } | ||||
|  | ||||
| .container { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|  | ||||
|   margin-left: 0; | ||||
|  | ||||
|   transition: margin-left ease-in-out 0.3s; | ||||
| } | ||||
|  | ||||
| .pushed { | ||||
|   margin-left: 290px; | ||||
| } | ||||
|  | ||||
| .leftTopWrapper { | ||||
|   position: absolute; | ||||
|   z-index: 10; | ||||
| } | ||||
|  | ||||
| .routeNumber { | ||||
|   margin: 32px; | ||||
| } | ||||
|  | ||||
| .weatherWidget { | ||||
|   margin: 32px; | ||||
| } | ||||
|  | ||||
| .rightSidebar { | ||||
|   flex-shrink: 0; | ||||
|   margin: 32px 32px 32px 0; | ||||
|   height: calc(100% - 57px); | ||||
|   width: 545px; | ||||
|   color: #ffffff; | ||||
|   background: #806c59; | ||||
|   border: 2px solid #806c59; | ||||
|   border-radius: 10px; | ||||
| } | ||||
|  | ||||
| .transferPlaceholder { | ||||
|   position: absolute; | ||||
|   bottom: 12px; | ||||
|   right: 32px; | ||||
| } | ||||
|  | ||||
| .toggleTransferBtn { | ||||
|   width: 48px; | ||||
|   height: 48px; | ||||
|   border: 0; | ||||
|   padding: 0; | ||||
|   background: none; | ||||
|   cursor: pointer; | ||||
| } | ||||
|  | ||||
| .transferPopup { | ||||
|   position: absolute; | ||||
|   right: 48px; | ||||
|   bottom: 0; | ||||
|  | ||||
|   width: 371px; | ||||
|   height: 180px; | ||||
|  | ||||
|   padding: 16px; | ||||
|   margin-right: 16px; | ||||
|   border-radius: 10px; | ||||
|  | ||||
|   background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), | ||||
|     rgba(179, 165, 152, 0.4); | ||||
|   box-shadow: inset 4px 4px 12px rgba(255, 255, 255, 0.12); | ||||
| } | ||||
| @@ -0,0 +1,47 @@ | ||||
| import { | ||||
|   Drawer, | ||||
|   Icons, | ||||
|   RouteInfoData, | ||||
|   RouteInfoWidget, | ||||
|   WeatherWidget, | ||||
| } from "@mt/components"; | ||||
| import { HTMLAttributes, useContext, useState } from "react"; | ||||
| import cn from "classnames"; | ||||
| import { LocalizationContext } from "@mt/i18n"; | ||||
| import styles from "./RoutePreviewDashboard.module.css"; | ||||
|  | ||||
| interface Props extends HTMLAttributes<HTMLDivElement> { | ||||
|   routeInfo: RouteInfoData; | ||||
| } | ||||
|  | ||||
| export const RoutePreviewDashboard = ({ children, routeInfo }: Props) => { | ||||
|   const { setLocale } = useContext(LocalizationContext); | ||||
|   const [openNav, setOpenNav] = useState(false); | ||||
|   const [openTransfers, setOpenTransfer] = useState(false); | ||||
|  | ||||
|   return ( | ||||
|     <div className={styles.root}> | ||||
|       {/* <Drawer isOpen={openNav} onToggle={setOpenNav} onLocaleChange={setLocale} /> | ||||
|  | ||||
|       <div className={cn(styles.container, { [styles.pushed]: openNav })}> | ||||
|         <div className={styles.leftTopWrapper}> | ||||
|           <RouteInfoWidget className={styles.routeNumber} routeInfo={routeInfo} /> | ||||
|           <WeatherWidget className={styles.weatherWidget} /> | ||||
|         </div> */} | ||||
|  | ||||
|       {children} | ||||
|  | ||||
|       {/* <div className={styles.transferPlaceholder}> | ||||
|           <Icons.InfoBtn | ||||
|             className={styles.toggleTransferBtn} | ||||
|             onClick={() => setOpenTransfer(!openTransfers)} | ||||
|           /> | ||||
|  | ||||
|           {openTransfers ? <div className={styles.transferPopup} /> : null} | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div className={styles.rightSidebar} /> */} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,28 @@ | ||||
| .root { | ||||
|   position: fixed; | ||||
|   z-index: 100; | ||||
|   right: var(--scroll-bar-width, 0); | ||||
|   bottom: var(--horizontal-scroll-bar-width, 0); | ||||
|   width: 600px; | ||||
|   background-color: #cccccc; | ||||
|  | ||||
|   transform: translateY(100%); | ||||
|   padding: 10px; | ||||
| } | ||||
|  | ||||
| .rootOpened { | ||||
|   transform: translateY(0); | ||||
| } | ||||
|  | ||||
| .togglePanelBtn { | ||||
|   cursor: pointer; | ||||
|   position: absolute; | ||||
|   top: -10px; | ||||
|   right: 10px; | ||||
|   transform: translateY(-100%); | ||||
|   color: #ffffff; | ||||
|   border: 1px solid; | ||||
|   border-radius: 5px; | ||||
|   padding: 1.5px; | ||||
|   background: rgba(0, 0, 0, 0.5); | ||||
| } | ||||
| @@ -0,0 +1,105 @@ | ||||
| import cn from 'classnames'; | ||||
| import SettingsIcon from '@mui/icons-material/Settings'; | ||||
| import { Button, FormControlLabel, Switch } from '@mui/material'; | ||||
| import { FormProvider } from 'react-hook-form'; | ||||
| import { useState } from 'react'; | ||||
|  | ||||
| import { useMapWidgetContext, MapSettings } from '@mt/components'; | ||||
| import { CoordinateField, ScaleField, RotationField, StationSelectField } from './settigs-fields'; | ||||
| import styles from './SettingsPanel.module.css'; | ||||
| import { useToggleScrollable } from '@mt/utils'; | ||||
|  | ||||
| interface Props { | ||||
|   onSubmit?: (data: MapSettings) => void; | ||||
| } | ||||
|  | ||||
| export const SettingsPanel = ({ onSubmit }: Props) => { | ||||
|   const [isOpened, setOpened] = useState(true); | ||||
|   const { enableScroll, disableScroll } = useToggleScrollable(); | ||||
|  | ||||
|   const { | ||||
|     settingsForm, | ||||
|     onSettingsFormChange, | ||||
|     stations, | ||||
|     isDragMode, | ||||
|     setIsDragMode, | ||||
|     isMapDataChanged, | ||||
|   } = useMapWidgetContext(); | ||||
|  | ||||
|   const handleGoBack = () => { | ||||
|     if ( | ||||
|       isMapDataChanged && | ||||
|       !window.confirm('Изменения не сохранены! Вы уверены, что хотите покинуть страницу?') | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     window.history.back(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <FormProvider {...settingsForm}> | ||||
|       <form | ||||
|         className={cn('g-flex', styles.root, { [styles.rootOpened]: isOpened })} | ||||
|         onChange={() => onSettingsFormChange()} | ||||
|         onSubmit={settingsForm.handleSubmit(onSubmit)} | ||||
|       > | ||||
|         <SettingsIcon | ||||
|           className={styles.togglePanelBtn} | ||||
|           fontSize="large" | ||||
|           onClick={() => setOpened(!isOpened)} | ||||
|         /> | ||||
|  | ||||
|         <div className="g-flex-column g-flex__item"> | ||||
|           <RotationField | ||||
|             name="rotateAngle" | ||||
|             label="Угол поворота карты" | ||||
|             onFocus={disableScroll} | ||||
|             onBlur={enableScroll} | ||||
|           /> | ||||
|  | ||||
|           <CoordinateField name="center" label="Координаты центра" /> | ||||
|  | ||||
|           <FormControlLabel | ||||
|             label="Включите для изменения центра карты с помощью мышки" | ||||
|             control={ | ||||
|               <Switch checked={isDragMode} onChange={() => setIsDragMode((isDrag) => !isDrag)} /> | ||||
|             } | ||||
|           /> | ||||
|  | ||||
|           <ScaleField | ||||
|             name="fullScale" | ||||
|             label="Масштаб 1" | ||||
|             onFocus={disableScroll} | ||||
|             onBlur={enableScroll} | ||||
|           /> | ||||
|  | ||||
|           <StationSelectField | ||||
|             name="currentStationId" | ||||
|             label="Выберете остановку" | ||||
|             hint="Для настройки масштаба при приближении к остановкам" | ||||
|             stations={stations} | ||||
|             onOpen={disableScroll} | ||||
|             onClose={enableScroll} | ||||
|             onChange={() => onSettingsFormChange()} | ||||
|           /> | ||||
|  | ||||
|           <ScaleField | ||||
|             name="zoomedScale" | ||||
|             label="Масштаб 2" | ||||
|             onFocus={disableScroll} | ||||
|             onBlur={enableScroll} | ||||
|           /> | ||||
|  | ||||
|           <Button type="submit" disabled={!isMapDataChanged}> | ||||
|             Сохранить изменения | ||||
|           </Button> | ||||
|  | ||||
|           <Button type="button" onClick={handleGoBack}> | ||||
|             Назад | ||||
|           </Button> | ||||
|         </div> | ||||
|       </form> | ||||
|     </FormProvider> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,59 @@ | ||||
| import { TextField } from '@mui/material'; | ||||
| import { Controller, useFormContext } from 'react-hook-form'; | ||||
| import { ChangeEvent, useCallback } from 'react'; | ||||
| import { Coordinates } from '@mt/common-types'; | ||||
| import { FieldProps } from './field-props.interface'; | ||||
|  | ||||
| interface CoordinateFieldProps extends FieldProps { | ||||
|   onChange?: (event: ChangeEvent) => void; | ||||
| } | ||||
|  | ||||
| const COORDINATE_PATTERN = /^(-?\d+(\.\d+)?),(\s*)(-?\d+(\.\d+)?)$/; | ||||
| const formatValue = (value: string | Coordinates) => | ||||
|   typeof value === 'string' ? value : `${value.lat}, ${value.lon}`; | ||||
| export const CoordinateField = ({ name, label, hint, onChange }: CoordinateFieldProps) => { | ||||
|   const { getFieldState, control, setError, clearErrors } = useFormContext(); | ||||
|  | ||||
|   const handleChange = useCallback( | ||||
|     ({ target }, field) => { | ||||
|       const { value } = target; | ||||
|  | ||||
|       let newValue: string | { lat: number; lon: number } = value; | ||||
|  | ||||
|       if (COORDINATE_PATTERN.test(value)) { | ||||
|         const matches = value.match(COORDINATE_PATTERN) || []; | ||||
|         const [lat, lon] = (matches[0] ?? '').split(','); | ||||
|  | ||||
|         newValue = { lat: Number(lat?.trim()), lon: Number(lon?.trim()) }; | ||||
|         clearErrors(); | ||||
|       } else { | ||||
|         setError(field.name, { | ||||
|           message: 'Неверный формат координаты', | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       return field.onChange(newValue); | ||||
|     }, | ||||
|     [clearErrors, setError] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Controller | ||||
|       name={name} | ||||
|       control={control} | ||||
|       render={({ field }) => ( | ||||
|         <TextField | ||||
|           type="text" | ||||
|           label={label} | ||||
|           value={formatValue(field.value)} | ||||
|           onChange={(e) => { | ||||
|             onChange?.(e); | ||||
|             handleChange(e, field); | ||||
|           }} | ||||
|           error={getFieldState(field.name).invalid} | ||||
|           helperText={getFieldState(field.name).error?.message || hint || ''} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,61 @@ | ||||
| import { Controller, useFormContext } from 'react-hook-form'; | ||||
| import { TextField } from '@mui/material'; | ||||
| import { useCallback } from 'react'; | ||||
| import { FieldProps } from './field-props.interface'; | ||||
| import { TextFieldProps } from '@mui/material/TextField/TextField'; | ||||
|  | ||||
| function isValidNumberInRange(value: string | number) { | ||||
|   const numberValue = Number(value); | ||||
|  | ||||
|   // Check if the value is a valid number, not NaN, within the range of -360 to 360, | ||||
|   // and disallow leading zeros (except for the value "0") | ||||
|   return ( | ||||
|     !isNaN(numberValue) && | ||||
|     /^-?(0|[1-9]\d*)$/.test(value.toString()) && | ||||
|     numberValue >= -360 && | ||||
|     numberValue <= 360 | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export const RotationField = ({ name, label, hint, ...props }: FieldProps & TextFieldProps) => { | ||||
|   const { control, getFieldState, setError, clearErrors } = useFormContext(); | ||||
|  | ||||
|   const handleChange = useCallback( | ||||
|     ({ target }, field) => { | ||||
|       const { value } = target; | ||||
|  | ||||
|       let newValue: string | number = value; | ||||
|  | ||||
|       if (isValidNumberInRange(value)) { | ||||
|         newValue = Number(value); | ||||
|         clearErrors(field.name); | ||||
|       } else { | ||||
|         setError(field.name, { | ||||
|           message: 'Неверный формат. Значение должно быть числом от -360 до 360', | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       return field.onChange(newValue); | ||||
|     }, | ||||
|     [clearErrors, setError] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Controller | ||||
|       name={name} | ||||
|       control={control} | ||||
|       render={({ field }) => ( | ||||
|         <TextField | ||||
|           type="number" | ||||
|           label={label} | ||||
|           inputProps={{ min: -360, max: 360 }} | ||||
|           value={field.value} | ||||
|           onChange={(e) => handleChange(e, field)} | ||||
|           error={getFieldState(field.name).invalid} | ||||
|           helperText={getFieldState(field.name).error?.message || hint || ''} | ||||
|           {...props} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,50 @@ | ||||
| import { TextField } from '@mui/material'; | ||||
| import { Controller, useFormContext } from 'react-hook-form'; | ||||
| import { useCallback } from 'react'; | ||||
| import { FieldProps } from './field-props.interface'; | ||||
| import { TextFieldProps } from '@mui/material/TextField/TextField'; | ||||
|  | ||||
| const SCALE_PATTERN = /^[1-9]\d*$/; | ||||
|  | ||||
| export const ScaleField = ({ label, name, hint, ...props }: FieldProps & TextFieldProps) => { | ||||
|   const { control, getFieldState, setError, clearErrors } = useFormContext(); | ||||
|  | ||||
|   const handleChange = useCallback( | ||||
|     ({ target }, field) => { | ||||
|       const { value } = target; | ||||
|  | ||||
|       let newValue: string | number = value; | ||||
|  | ||||
|       if (SCALE_PATTERN.test(value)) { | ||||
|         newValue = Number(value); | ||||
|         clearErrors(field.name); | ||||
|       } else { | ||||
|         setError(field.name, { | ||||
|           message: 'Неверный формат. Масштаб должен быть положительным числом', | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       return field.onChange(newValue); | ||||
|     }, | ||||
|     [clearErrors, setError] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Controller | ||||
|       control={control} | ||||
|       name={name} | ||||
|       render={({ field }) => ( | ||||
|         <TextField | ||||
|           type="number" | ||||
|           label={label} | ||||
|           inputProps={{ step: 1000, min: 0 }} | ||||
|           value={field.value} | ||||
|           onChange={(e) => handleChange(e, field)} | ||||
|           error={getFieldState(field.name).invalid} | ||||
|           helperText={getFieldState(field.name).error?.message || hint || ''} | ||||
|           {...props} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,65 @@ | ||||
| import { | ||||
|   FormControl, | ||||
|   FormHelperText, | ||||
|   InputLabel, | ||||
|   MenuItem, | ||||
|   Select, | ||||
|   SelectChangeEvent, | ||||
| } from '@mui/material'; | ||||
| import { StationOnMap } from '@mt/components'; | ||||
| import { useId } from 'react'; | ||||
| import { Controller, useFormContext } from 'react-hook-form'; | ||||
| import { FieldProps } from './field-props.interface'; | ||||
|  | ||||
| interface StationSelectFieldProps extends FieldProps { | ||||
|   onChange: () => void; | ||||
|   onOpen?: () => void; | ||||
|   onClose?: () => void; | ||||
|   stations: StationOnMap[]; | ||||
| } | ||||
|  | ||||
| export const StationSelectField = ({ | ||||
|   name, | ||||
|   onChange, | ||||
|   stations, | ||||
|   label, | ||||
|   hint, | ||||
|   ...selectProps | ||||
| }: StationSelectFieldProps) => { | ||||
|   const labelId = useId(); | ||||
|   const { control } = useFormContext(); | ||||
|  | ||||
|   const handleChange = ({ target: { value } }: SelectChangeEvent, field) => { | ||||
|     field.onChange(value); | ||||
|     onChange(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Controller | ||||
|       control={control} | ||||
|       name={name} | ||||
|       defaultValue="" | ||||
|       render={({ field }) => ( | ||||
|         <FormControl> | ||||
|           <InputLabel id={labelId}>{label}</InputLabel> | ||||
|           <Select | ||||
|             labelId={labelId} | ||||
|             value={field.value} | ||||
|             onChange={(e) => handleChange(e, field)} | ||||
|             {...selectProps} | ||||
|           > | ||||
|             <MenuItem value="">Станция не выбрана</MenuItem> | ||||
|  | ||||
|             {stations.map((station) => ( | ||||
|               <MenuItem value={station.id} key={station.id}> | ||||
|                 {station.name.ru} | ||||
|               </MenuItem> | ||||
|             ))} | ||||
|           </Select> | ||||
|  | ||||
|           <FormHelperText>{hint}</FormHelperText> | ||||
|         </FormControl> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,8 @@ | ||||
| import { FieldPath } from 'react-hook-form'; | ||||
| import { MapSettings } from '@mt/components'; | ||||
|  | ||||
| export interface FieldProps { | ||||
|   name: FieldPath<MapSettings>; | ||||
|   label: string; | ||||
|   hint?: string; | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| export * from './RotationField'; | ||||
| export * from './ScaleField'; | ||||
| export * from './CoordinateField'; | ||||
| export * from './StationSelectField'; | ||||
| export * from './field-props.interface'; | ||||
| @@ -0,0 +1,62 @@ | ||||
| import { MapData } from "@mt/components"; | ||||
|  | ||||
| import { Route, Station } from "@mt/common-types"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { mapRouteFromApi } from "../mappers/mapRouteFromApi"; | ||||
| import { useOne, useMany } from "@refinedev/core"; | ||||
| import { axiosInstance } from "../../../../providers/data"; | ||||
|  | ||||
| const fetchStations = async ( | ||||
|   routeId: string, | ||||
|   setStations: (stations: any[]) => void, | ||||
|   setSights: (sights: any[]) => void | ||||
| ) => { | ||||
|   const stations = (await axiosInstance.get(`/route/${routeId}/station`)).data; | ||||
|   const sights = (await axiosInstance.get(`/route/${routeId}/sight`)).data; | ||||
|   setStations(stations); | ||||
|   setSights(sights); | ||||
|  | ||||
|   const stationsPath = stations.map((station: any) => [ | ||||
|     station.latitude, | ||||
|     station.longitude, | ||||
|   ]); | ||||
|   const sightsPath = sights.map((sight: any) => [ | ||||
|     sight.latitude, | ||||
|     sight.longitude, | ||||
|   ]); | ||||
|  | ||||
|   console.log(stationsPath, sightsPath); | ||||
| }; | ||||
|  | ||||
| export const useGetRouteData = (routeId: string) => { | ||||
|   const [mappedData, setMappedData] = useState<MapData>(null); | ||||
|  | ||||
|   const { | ||||
|     data: routeData, | ||||
|     isSuccess, | ||||
|     isError, | ||||
|     isLoading, | ||||
|   } = useOne<Route>({ | ||||
|     resource: "route", | ||||
|     id: routeId, | ||||
|   }); | ||||
|  | ||||
|   const [stations, setStations] = useState<any[]>([]); | ||||
|   const [sights, setSights] = useState<any[]>([]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetchStations(routeId, setStations, setSights); | ||||
|   }, [routeData]); | ||||
|  | ||||
|   // useEffect(() => { | ||||
|   //   if (!routeData) { | ||||
|   //     return; | ||||
|   //   } | ||||
|  | ||||
|   //   // const data = mapRouteFromApi(routeData, []); | ||||
|  | ||||
|   //   setMappedData(data); | ||||
|   // }, [routeData]); | ||||
|  | ||||
|   return { routeData, mappedData, isLoading, isError }; | ||||
| }; | ||||
| @@ -0,0 +1,21 @@ | ||||
| import { useCallback } from "react"; | ||||
|  | ||||
| import { mapRouteToApi } from "../mappers/mapRouteToApi"; | ||||
| import { MapSettings } from "@mt/components"; | ||||
| import { Route } from "@mt/common-types"; | ||||
| import { useUpdate } from "@refinedev/core"; | ||||
|  | ||||
| export const useUpdateRouteData = (routeId: string, previousData: Route) => { | ||||
|   const update = useUpdate(); | ||||
|  | ||||
|   return useCallback( | ||||
|     (mapSettings: MapSettings, updatedStations) => { | ||||
|       update({ | ||||
|         resource: "routes", | ||||
|         id: routeId, | ||||
|         data: mapRouteToApi(mapSettings, updatedStations, previousData), | ||||
|       }); | ||||
|     }, | ||||
|     [update, routeId, previousData] | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										1
									
								
								src/preview/components/route-preview/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/preview/components/route-preview/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export { RoutePreview } from './components/RoutePreview'; | ||||
							
								
								
									
										141
									
								
								src/preview/components/route-preview/mappers/mapRouteFromApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/preview/components/route-preview/mappers/mapRouteFromApi.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| import { MapData, StationOnMap } from "@mt/components"; | ||||
| import { TEMP_STATION_TYPE_MAP } from "@mt/common-types"; | ||||
| import { | ||||
|   Coordinates, | ||||
|   Route, | ||||
|   RouteStation, | ||||
|   Station, | ||||
|   Track, | ||||
|   TransferStation, | ||||
| } from "@mt/common-types"; | ||||
|  | ||||
| export function mapRouteFromApi( | ||||
|   routeData: Route, | ||||
|   stations: Station[] | ||||
| ): MapData { | ||||
|   const { | ||||
|     generalInfo, | ||||
|     attractionGroupings, | ||||
|     stations: routeStations, | ||||
|   } = routeData; | ||||
|   const { rotate, scale1, scale2, centerCoordinates, track } = generalInfo; | ||||
|  | ||||
|   return { | ||||
|     mapRotateAngle: rotate, | ||||
|     fullMapScale: scale1, | ||||
|     zoomedMapScale: scale2, | ||||
|     centerOfMapPoint: centerCoordinates, | ||||
|     trackPoints: track, | ||||
|     stationsOnMap: mapStationsFromApi(routeStations, stations, track), | ||||
|     touristAttractionGroupsOnMap: attractionGroupings | ||||
|       .filter(({ coordinates }) => Boolean(coordinates)) | ||||
|       .map(({ iconSize, coordinates, attractionIds }) => ({ | ||||
|         iconSize, | ||||
|         pointOnMap: coordinates, | ||||
|         touristAttractionsOnMap: attractionIds.map((id) => ({ | ||||
|           id, | ||||
|           pointOnMap: coordinates, | ||||
|         })), | ||||
|       })), | ||||
|   }; | ||||
| } | ||||
|  | ||||
| function mapStationsFromApi( | ||||
|   routeStations: RouteStation[], | ||||
|   stations: Station[], | ||||
|   track: Track | ||||
| ): MapData["stationsOnMap"] { | ||||
|   const stationsMap = new Map(stations.map((station) => [station.id, station])); | ||||
|   const unionStations: Array<Omit<RouteStation, "stationId"> & Station> = | ||||
|     routeStations.map(({ stationId, ...station }) => ({ | ||||
|       ...station, | ||||
|       ...stationsMap.get(stationId), | ||||
|     })); | ||||
|  | ||||
|   const mappedStations = unionStations.map((station) => { | ||||
|     const { | ||||
|       id, | ||||
|       name, | ||||
|       shortName, | ||||
|       coordinates, | ||||
|       textAlignment, | ||||
|       mapOffsets, | ||||
|       stationTypeId, | ||||
|       transferStations, | ||||
|       iconUrl, | ||||
|     } = station; | ||||
|  | ||||
|     return { | ||||
|       id, | ||||
|       name, | ||||
|       shortName, | ||||
|       coordinates, | ||||
|       transferStationInfos: mapTransfersToMap(transferStations, stationsMap), | ||||
|       labelAlignment: textAlignment, | ||||
|       labelOffset: mapOffsets, | ||||
|       iconUrl, | ||||
|       stationTypeId, | ||||
|     }; | ||||
|   }); | ||||
|  | ||||
|   const stationsOnMap = mappedStations.map((station) => { | ||||
|     const { coordinates } = station; | ||||
|     const trackIndex = track.findIndex( | ||||
|       (trackPoint) => | ||||
|         coordinates.lat === trackPoint.lat && coordinates.lon === trackPoint.lon | ||||
|     ); | ||||
|  | ||||
|     return { | ||||
|       ...station, | ||||
|       pointOnMap: { | ||||
|         ...coordinates, | ||||
|         trackIndex, | ||||
|       }, | ||||
|     }; | ||||
|   }) as MapData["stationsOnMap"]; | ||||
|  | ||||
|   return sortStationsByTrackOrder(track, stationsOnMap); | ||||
| } | ||||
|  | ||||
| function sortStationsByTrackOrder( | ||||
|   track: Track, | ||||
|   stations: StationOnMap[] | ||||
| ): StationOnMap[] { | ||||
|   // Create a map to store the index of each coordinate in the second array | ||||
|   const coordinateIndexMap = new Map<string, number>(); | ||||
|   track.forEach((coordinate, index) => { | ||||
|     coordinateIndexMap.set(getCoordinateString(coordinate), index); | ||||
|   }); | ||||
|  | ||||
|   // Sort the first array based on the order of coordinates in the second array | ||||
|   return [...stations].sort((a, b) => { | ||||
|     const indexA = | ||||
|       coordinateIndexMap.get(getCoordinateString(a.coordinates)) || 0; | ||||
|     const indexB = | ||||
|       coordinateIndexMap.get(getCoordinateString(b.coordinates)) || 0; | ||||
|  | ||||
|     return indexA - indexB; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // TODO: move to shared utils and refactor across the project | ||||
| function getCoordinateString(coordinate: Coordinates): string { | ||||
|   return `${coordinate.lat}:${coordinate.lon}`; | ||||
| } | ||||
|  | ||||
| function mapTransfersToMap( | ||||
|   transferStations: TransferStation[], | ||||
|   stationsMap: Map<string, Station> | ||||
| ) { | ||||
|   return transferStations | ||||
|     .filter(({ isShowOnMap }) => isShowOnMap) | ||||
|     .sort(({ ordinal: ordinalA }, { ordinal: ordinalB }) => ordinalA - ordinalB) | ||||
|     .map(({ stationId }) => { | ||||
|       const { stationTypeId, shortName } = stationsMap.get(stationId); | ||||
|  | ||||
|       return { | ||||
|         type: TEMP_STATION_TYPE_MAP[stationTypeId].type, | ||||
|         name: shortName, | ||||
|       }; | ||||
|     }); | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| import { Route, RouteStation } from "@admin/types"; | ||||
| import { MapSettings } from "@mt/components"; | ||||
| import { DeepPartial } from "react-hook-form"; | ||||
|  | ||||
| export const mapRouteToApi = ( | ||||
|   { rotateAngle, fullScale, zoomedScale, center }: MapSettings, | ||||
|   updatedStations: Partial<RouteStation>, | ||||
|   previousData | ||||
| ): DeepPartial<Route> => { | ||||
|   return { | ||||
|     ...previousData, | ||||
|     generalInfo: { | ||||
|       ...previousData.generalInfo, | ||||
|       rotate: rotateAngle, | ||||
|       scale1: fullScale, | ||||
|       scale2: zoomedScale, | ||||
|       centerCoordinates: center, | ||||
|     }, | ||||
|     stations: previousData.stations.map((station: RouteStation) => { | ||||
|       return updatedStations[station.stationId] | ||||
|         ? { ...station, ...updatedStations[station.stationId] } | ||||
|         : station; | ||||
|     }), | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										5
									
								
								src/preview/types/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/preview/types/constants.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export const LocalizedStringDefaults = { | ||||
|   ru: "", | ||||
|   en: "", | ||||
|   zh: "", | ||||
| }; | ||||
| @@ -244,3 +244,5 @@ export * from "./attraction.interface"; | ||||
| export * from "./attraction-widget.interface"; | ||||
| export * from "./article.interface"; | ||||
| export * from "./lightbox.interface"; | ||||
| export * from "./station"; | ||||
| export * from "./constants"; | ||||
|   | ||||
							
								
								
									
										84
									
								
								src/preview/types/station.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/preview/types/station.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| // TODO: resolve circular deps | ||||
| import { TransportType, uuid } from "@mt/common-types"; | ||||
|  | ||||
| interface StationTypeOption { | ||||
|   id: uuid; | ||||
|   type: TransportType; | ||||
|   name: string; | ||||
| } | ||||
|  | ||||
| // TODO: Temp Solution! Remove after implement TRANSPORT TYPES in Admin Panel with BE | ||||
|  | ||||
| const StationTypeToUuidMap: Record<TransportType, uuid> = { | ||||
|   TRAM: "3fa85f64-5717-4562-b3fc-2c963f66afa6", | ||||
|   TROLLEY: "cbb26715-8126-4129-9647-a3817b08d897", | ||||
|   BUS: "9664f5eb-2c80-4f02-9c87-663012a7f92e", | ||||
|   TRAIN: "7b657db6-aaa0-4d5e-b469-2ff94f5ca25e", | ||||
|   METRO_RED: "a95a25ab-c002-4814-9767-e56e2e8b732c", | ||||
|   METRO_GREEN: "a8f9c6ae-20e6-4113-9b0d-33710908c178", | ||||
|   METRO_BLUE: "0764576e-729a-4a44-8f34-166177e5d50a", | ||||
|   METRO_PURPLE: "5b0679ed-c614-4658-a30b-33c3c61734d4", | ||||
|   METRO_ORANGE: "4f5d142d-40f0-4a47-9aa9-b9b0e5bc7d15", | ||||
| }; | ||||
|  | ||||
| export const TEMP_STATION_TYPE_MAP: Record<uuid, StationTypeOption> = { | ||||
|   [StationTypeToUuidMap.TRAM]: { | ||||
|     id: StationTypeToUuidMap.TRAM, | ||||
|     type: "TRAM" as TransportType, | ||||
|     name: "Трамвай", | ||||
|   }, | ||||
|   [StationTypeToUuidMap.TROLLEY]: { | ||||
|     id: StationTypeToUuidMap.TROLLEY, | ||||
|     type: "TROLLEY" as TransportType, | ||||
|     name: "Троллейбус", | ||||
|   }, | ||||
|   [StationTypeToUuidMap.BUS]: { | ||||
|     id: StationTypeToUuidMap.BUS, | ||||
|     type: "BUS" as TransportType, | ||||
|     name: "Автобус", | ||||
|   }, | ||||
|   [StationTypeToUuidMap.TRAIN]: { | ||||
|     id: StationTypeToUuidMap.TRAIN, | ||||
|     type: "TRAIN" as TransportType, | ||||
|     name: "Поезд", | ||||
|   }, | ||||
|   [StationTypeToUuidMap.METRO_RED]: { | ||||
|     id: StationTypeToUuidMap.METRO_RED, | ||||
|     type: "METRO_RED" as TransportType, | ||||
|     name: "Метро (красная ветка)", | ||||
|   }, | ||||
|   [StationTypeToUuidMap.METRO_GREEN]: { | ||||
|     id: StationTypeToUuidMap.METRO_GREEN, | ||||
|     type: "METRO_GREEN" as TransportType, | ||||
|     name: "Метро (зеленая ветка)", | ||||
|   }, | ||||
|   [StationTypeToUuidMap.METRO_BLUE]: { | ||||
|     id: StationTypeToUuidMap.METRO_BLUE, | ||||
|     type: "METRO_BLUE" as TransportType, | ||||
|     name: "Метро (голубая ветка)", | ||||
|   }, | ||||
|   [StationTypeToUuidMap.METRO_PURPLE]: { | ||||
|     id: StationTypeToUuidMap.METRO_PURPLE, | ||||
|     type: "METRO_PURPLE" as TransportType, | ||||
|     name: "Метро (фиолетовая ветка)", | ||||
|   }, | ||||
|   [StationTypeToUuidMap.METRO_ORANGE]: { | ||||
|     id: StationTypeToUuidMap.METRO_ORANGE, | ||||
|     type: "METRO_ORANGE" as TransportType, | ||||
|     name: "Метро (оранжевая ветка)", | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const TEMP_STATION_TYPES: StationTypeOption[] = Object.values( | ||||
|   TEMP_STATION_TYPE_MAP | ||||
| ); | ||||
|  | ||||
| export const TEMP_ROUTE_TYPES: StationTypeOption[] = [ | ||||
|   TEMP_STATION_TYPE_MAP[StationTypeToUuidMap.TRAM], | ||||
|   TEMP_STATION_TYPE_MAP[StationTypeToUuidMap.TROLLEY], | ||||
|   TEMP_STATION_TYPE_MAP[StationTypeToUuidMap.BUS], | ||||
| ]; | ||||
|  | ||||
| export const getStationTypeById = (id: uuid): string => { | ||||
|   return TEMP_STATION_TYPES.find((type) => type.id === id)?.name ?? ""; | ||||
| }; | ||||
| @@ -1,81 +1,114 @@ | ||||
| import styled from "@emotion/styled"; | ||||
| import { Drawer } from "@mt/components"; | ||||
| import { Locale } from "@mt/i18n"; | ||||
| import { NavWidgetContainer } from "../nav-widget/nav-widget-container"; | ||||
| import { RouteInfoWidgetContainer } from "../RouteInfoWidgetContainer/RouteInfoWidgetContainer"; | ||||
| import { WeatherWidgetContainer } from "../WeatherWidget/WeatherWidgetContainer"; | ||||
| import { MapWidgetContainer } from "../MapWidgetContainer/MapWidgetContainer"; | ||||
| import { OperativeInfoWidget } from "../operative-info-widget/operative-info-widget"; | ||||
| import { AttractionWidgetContainer } from "../attractions-widget/AttractionWidgetContainer"; | ||||
| import { useState, useEffect } from "react"; | ||||
|  | ||||
| const StyledDashboard = styled.div` | ||||
|   background-color: #000; | ||||
|  | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
|  | ||||
|   display: flex; | ||||
|  | ||||
|   .nav-widget--opened + .container { | ||||
|     margin-left: 290px; | ||||
|   } | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|  | ||||
|   .container { | ||||
|     position: relative; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|  | ||||
|     margin-left: 0; | ||||
|  | ||||
|     transition: margin-left ease-in-out 0.3s; | ||||
|  | ||||
|     .left-top-wrapper { | ||||
|       position: absolute; | ||||
|       .route-number { | ||||
|         margin: 32px; | ||||
|       } | ||||
|  | ||||
|       .weather-widget { | ||||
|         margin: 32px; | ||||
|       } | ||||
|     } | ||||
|     max-width: 960px; | ||||
|     padding: 24px; | ||||
|     box-sizing: border-box; | ||||
|   } | ||||
|  | ||||
|   .right-sidebar { | ||||
|     flex-shrink: 0; | ||||
|   .video-wrapper { | ||||
|     width: 100%; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     margin-top: 24px; | ||||
|   } | ||||
|  | ||||
|   video { | ||||
|     width: 100%; | ||||
|     max-height: 80vh; | ||||
|     border-radius: 12px; | ||||
|     object-fit: cover; | ||||
|   } | ||||
|  | ||||
|   .error { | ||||
|     color: red; | ||||
|     margin-top: 12px; | ||||
|   } | ||||
|  | ||||
|   .loader { | ||||
|     color: white; | ||||
|     margin-top: 12px; | ||||
|   } | ||||
|  | ||||
|   .upload { | ||||
|     margin-top: 24px; | ||||
|     color: white; | ||||
|  | ||||
|     input { | ||||
|       margin-left: 12px; | ||||
|     } | ||||
|   } | ||||
| `; | ||||
|  | ||||
| export function Dashboard() { | ||||
|   const [videoUrl, setVideoUrl] = useState<string | undefined>(); | ||||
|   const [isLoading, setIsLoading] = useState<boolean>(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchVideo = async () => { | ||||
|       try { | ||||
|         const response = await fetch( | ||||
|           `https://wn.krbl.ru/media/981d44f9-85e7-4d1d-994b-9eab631ba5d1/download?token=${localStorage.getItem( | ||||
|             "refine-auth" | ||||
|           )}` | ||||
|         ); // <-- укажи тут свой URL | ||||
|         if (!response.ok) throw new Error("Не удалось загрузить видео"); | ||||
|  | ||||
|         const blob = await response.blob(); | ||||
|         const objectUrl = URL.createObjectURL(blob); | ||||
|         setVideoUrl(objectUrl); | ||||
|         setIsLoading(false); | ||||
|       } catch (err) { | ||||
|         setError("Ошибка при загрузке видео"); | ||||
|         setIsLoading(false); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     fetchVideo(); | ||||
|  | ||||
|     // Очистка при размонтировании | ||||
|     return () => { | ||||
|       if (videoUrl) { | ||||
|         URL.revokeObjectURL(videoUrl); | ||||
|       } | ||||
|     }; | ||||
|   }, []); | ||||
|  | ||||
|   const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     const file = e.target.files?.[0]; | ||||
|     if (file) { | ||||
|       const objectUrl = URL.createObjectURL(file); | ||||
|       setVideoUrl(objectUrl); | ||||
|       setError(null); | ||||
|       setIsLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <StyledDashboard> | ||||
|       <Drawer | ||||
|         onToggle={function (isOpened: boolean): void { | ||||
|           throw new Error("Function not implemented."); | ||||
|         }} | ||||
|         isOpen={false} | ||||
|         onLocaleChange={function (locale: Locale): void { | ||||
|           throw new Error("Function not implemented."); | ||||
|         }} | ||||
|       ></Drawer> | ||||
|  | ||||
|       <NavWidgetContainer /> | ||||
|  | ||||
|       <div className="container"> | ||||
|         <div className="left-top-wrapper"> | ||||
|           <RouteInfoWidgetContainer className="route-number" /> | ||||
|           <WeatherWidgetContainer className="weather-widget" /> | ||||
|         </div> | ||||
|         {isLoading && <div className="loader">Загрузка видео...</div>} | ||||
|         {error && <div className="error">{error}</div>} | ||||
|  | ||||
|         <MapWidgetContainer /> | ||||
|  | ||||
|         <OperativeInfoWidget /> | ||||
|       </div> | ||||
|  | ||||
|       <div className="right-sidebar"> | ||||
|         <AttractionWidgetContainer /> | ||||
|         {videoUrl && ( | ||||
|           <div className="video-wrapper"> | ||||
|             <video src={videoUrl} autoPlay controls muted /> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|     </StyledDashboard> | ||||
|   ); | ||||
|   | ||||
| @@ -4,7 +4,9 @@ import axios from "axios"; | ||||
| import { TOKEN_KEY } from "../authProvider"; | ||||
| import Cookies from "js-cookie"; | ||||
|  | ||||
| export const axiosInstance = axios.create(); | ||||
| export const axiosInstance = axios.create({ | ||||
|   baseURL: import.meta.env.VITE_KRBL_API, | ||||
| }); | ||||
|  | ||||
| axiosInstance.interceptors.request.use((config) => { | ||||
|   // Добавляем токен авторизации | ||||
|   | ||||
		Reference in New Issue
	
	Block a user