From 9e34a71e145b098b3d89ad3169cb792fd3b73797 Mon Sep 17 00:00:00 2001 From: itoshi Date: Sun, 20 Apr 2025 10:55:12 +0300 Subject: [PATCH] fetching data from api for route preview --- src/App.tsx | 661 +++++++++--------- src/components/LinkedItems.tsx | 20 +- src/pages/media/show.tsx | 68 +- src/pages/route/edit.tsx | 6 +- src/pages/route/show.tsx | 86 ++- .../WeatherWidget/icons/DetHumidity.tsx | 16 + .../WeatherWidget/icons/DetWind.tsx | 24 + .../WeatherWidget/weather-widget-icon.tsx | 6 +- .../WeatherWidget/weather-widget-right.tsx | 4 +- .../route-preview/components/RoutePreview.tsx | 8 + .../components/RoutePreviewContainer.tsx | 83 +++ .../RoutePreviewDashboard.module.css | 79 +++ .../RoutePreviewDashboard.tsx | 47 ++ .../SettingsPanel/SettingsPanel.module.css | 28 + .../SettingsPanel/SettingsPanel.tsx | 105 +++ .../settigs-fields/CoordinateField.tsx | 59 ++ .../settigs-fields/RotationField.tsx | 61 ++ .../settigs-fields/ScaleField.tsx | 50 ++ .../settigs-fields/StationSelectField.tsx | 65 ++ .../settigs-fields/field-props.interface.ts | 8 + .../SettingsPanel/settigs-fields/index.ts | 5 + .../route-preview/hooks/useGetRouteData.ts | 62 ++ .../route-preview/hooks/useUpdateRouteData.ts | 21 + src/preview/components/route-preview/index.ts | 1 + .../route-preview/mappers/mapRouteFromApi.ts | 141 ++++ .../route-preview/mappers/mapRouteToApi.ts | 25 + src/preview/types/constants.ts | 5 + src/preview/types/index.ts | 2 + src/preview/types/station.ts | 84 +++ src/preview/widgets/dashboard/Dashboard.tsx | 145 ++-- src/providers/data.ts | 4 +- svg.d.ts | 8 + tsconfig.json | 2 +- vite.config.ts | 2 +- 34 files changed, 1534 insertions(+), 457 deletions(-) create mode 100644 src/preview/components/WeatherWidget/icons/DetHumidity.tsx create mode 100644 src/preview/components/WeatherWidget/icons/DetWind.tsx create mode 100644 src/preview/components/route-preview/components/RoutePreview.tsx create mode 100644 src/preview/components/route-preview/components/RoutePreviewContainer.tsx create mode 100644 src/preview/components/route-preview/components/RoutePreviewDashboard/RoutePreviewDashboard.module.css create mode 100644 src/preview/components/route-preview/components/RoutePreviewDashboard/RoutePreviewDashboard.tsx create mode 100644 src/preview/components/route-preview/components/SettingsPanel/SettingsPanel.module.css create mode 100644 src/preview/components/route-preview/components/SettingsPanel/SettingsPanel.tsx create mode 100644 src/preview/components/route-preview/components/SettingsPanel/settigs-fields/CoordinateField.tsx create mode 100644 src/preview/components/route-preview/components/SettingsPanel/settigs-fields/RotationField.tsx create mode 100644 src/preview/components/route-preview/components/SettingsPanel/settigs-fields/ScaleField.tsx create mode 100644 src/preview/components/route-preview/components/SettingsPanel/settigs-fields/StationSelectField.tsx create mode 100644 src/preview/components/route-preview/components/SettingsPanel/settigs-fields/field-props.interface.ts create mode 100644 src/preview/components/route-preview/components/SettingsPanel/settigs-fields/index.ts create mode 100644 src/preview/components/route-preview/hooks/useGetRouteData.ts create mode 100644 src/preview/components/route-preview/hooks/useUpdateRouteData.ts create mode 100644 src/preview/components/route-preview/index.ts create mode 100644 src/preview/components/route-preview/mappers/mapRouteFromApi.ts create mode 100644 src/preview/components/route-preview/mappers/mapRouteToApi.ts create mode 100644 src/preview/types/constants.ts create mode 100644 src/preview/types/station.ts create mode 100644 svg.d.ts diff --git a/src/App.tsx b/src/App.tsx index 16c2827..1356396 100644 --- a/src/App.tsx +++ b/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 ( - - - - - - - , + + + + + + + + , + }, }, - }, - { - name: "city", - list: "/city", - create: "/city/create", - edit: "/city/edit/:id", - show: "/city/show/:id", - meta: { - canDelete: true, - label: "Города", - icon: , + { + name: "city", + list: "/city", + create: "/city/create", + edit: "/city/edit/:id", + show: "/city/show/:id", + meta: { + canDelete: true, + label: "Города", + icon: , + }, }, - }, - { - name: "carrier", - list: "/carrier", - create: "/carrier/create", - edit: "/carrier/edit/:id", - show: "/carrier/show/:id", - meta: { - canDelete: true, - label: "Перевозчики", - icon: , + { + name: "carrier", + list: "/carrier", + create: "/carrier/create", + edit: "/carrier/edit/:id", + show: "/carrier/show/:id", + meta: { + canDelete: true, + label: "Перевозчики", + icon: , + }, }, - }, - { - name: "media", - list: "/media", - create: "/media/create", - edit: "/media/edit/:id", - show: "/media/show/:id", - meta: { - canDelete: true, - label: "Медиа", - icon: , + { + name: "media", + list: "/media", + create: "/media/create", + edit: "/media/edit/:id", + show: "/media/show/:id", + meta: { + canDelete: true, + label: "Медиа", + icon: , + }, }, - }, - { - name: "article", - list: "/article", - create: "/article/create", - edit: "/article/edit/:id", - show: "/article/show/:id", - meta: { - canDelete: true, - label: "Статьи", - icon: , + { + name: "article", + list: "/article", + create: "/article/create", + edit: "/article/edit/:id", + show: "/article/show/:id", + meta: { + canDelete: true, + label: "Статьи", + icon: , + }, }, - }, - { - name: "sight", - list: "/sight", - create: "/sight/create", - edit: "/sight/edit/:id", - show: "/sight/show/:id", - meta: { - canDelete: true, - label: "Достопримечательности", - icon: , + { + name: "sight", + list: "/sight", + create: "/sight/create", + edit: "/sight/edit/:id", + show: "/sight/show/:id", + meta: { + canDelete: true, + label: "Достопримечательности", + icon: , + }, }, - }, - { - name: "station", - list: "/station", - create: "/station/create", - edit: "/station/edit/:id", - show: "/station/show/:id", - meta: { - canDelete: true, - label: "Остановки", - icon: , + { + name: "station", + list: "/station", + create: "/station/create", + edit: "/station/edit/:id", + show: "/station/show/:id", + meta: { + canDelete: true, + label: "Остановки", + icon: , + }, }, - }, - { - name: "vehicle", - list: "/vehicle", - create: "/vehicle/create", - edit: "/vehicle/edit/:id", - show: "/vehicle/show/:id", - meta: { - canDelete: true, - label: "Транспорт", - icon: , + { + name: "vehicle", + list: "/vehicle", + create: "/vehicle/create", + edit: "/vehicle/edit/:id", + show: "/vehicle/show/:id", + meta: { + canDelete: true, + label: "Транспорт", + icon: , + }, }, - }, - { - name: "route", - list: "/route", - create: "/route/create", - edit: "/route/edit/:id", - show: "/route/show/:id", - meta: { - canDelete: true, - label: "Маршруты", - icon: , + { + name: "route", + list: "/route", + create: "/route/create", + edit: "/route/edit/:id", + show: "/route/show/:id", + meta: { + canDelete: true, + label: "Маршруты", + icon: , + }, }, - }, - { - name: "user", - list: "/user", - create: "/user/create", - edit: "/user/edit/:id", - show: "/user/show/:id", - meta: { - canDelete: true, - label: "Пользователи", - icon: , + { + name: "user", + list: "/user", + create: "/user/create", + edit: "/user/edit/:id", + show: "/user/show/:id", + meta: { + canDelete: true, + label: "Пользователи", + icon: , + }, }, - }, - ]} - options={{ - syncWithLocation: true, - warnWhenUnsavedChanges: true, // Включаем глобально - useNewQueryKeys: true, - projectId: "Wv044J-t53S3s-PcbJGe", - }} - > - - } - > - - - - - } - > + ]} + options={{ + syncWithLocation: true, + warnWhenUnsavedChanges: true, // Включаем глобально + useNewQueryKeys: true, + projectId: "Wv044J-t53S3s-PcbJGe", + }} + > + } - /> - - - } /> - - - - } - /> - - - - } - /> - } /> - - } /> - - - } /> - - - - } - /> - - - - } - /> - } /> - - - - } /> - } /> - } /> - } /> - - - - } /> - } /> - } /> - } /> - - - - } /> - } /> - } /> - } /> - - - - } /> - } /> - } /> - } /> - - - - } /> - - - - } - /> - - - - } - /> - } /> - - - - } /> - } /> - } /> - } /> - - - - } /> - - - - } - /> - - - - } - /> - } /> - - - + element={ + } + > + + + + + } + > - - - } - /> - - - - } - /> - - - - } - /> - - - - } + element={} /> + + + } /> + + + + } + /> + + + + } + /> + } /> + + } /> + + + } /> + + + + } + /> + + + + } + /> + } /> + + + + } /> + } /> + } /> + } /> + + + + } /> + } /> + } /> + } /> + + + + } /> + } /> + } /> + } /> + + + + } /> + } /> + } /> + } /> + + + + } /> + + + + } + /> + + + + } + /> + } /> + + + + } /> + } /> + } /> + } /> + + + + } /> + + + + } + /> + + + + } + /> + } /> + } /> + + + + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + } /> + } + > + + + } + > + } /> + + - } /> - - } - > - - - } - > - } /> - - - - - { - // const cleanedTitle = title.autoGeneratedTitle.split('|')[0].trim() - // return `${cleanedTitle} — Белые ночи` - return "Белые ночи"; - }} - /> - - - - - - + + { + // const cleanedTitle = title.autoGeneratedTitle.split('|')[0].trim() + // return `${cleanedTitle} — Белые ночи` + return "Белые ночи"; + }} + /> + + + + + + + ); } diff --git a/src/components/LinkedItems.tsx b/src/components/LinkedItems.tsx index 2d58fb9..f9a2afb 100644 --- a/src/components/LinkedItems.tsx +++ b/src/components/LinkedItems.tsx @@ -48,6 +48,7 @@ type LinkedItemsProps = { title: string; type: "show" | "edit"; extraField?: ExtraFieldConfig; + dragAllowed?: boolean; }; const reorder = (list: any[], startIndex: number, endIndex: number) => { @@ -63,6 +64,7 @@ export const LinkedItems = ({ childResource, fields, title, + dragAllowed = false, type, }: LinkedItemsProps) => { const [items, setItems] = useState([]); @@ -84,6 +86,16 @@ export const LinkedItems = ({ 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 = ({ - {type === "edit" && } + {type === "edit" && dragAllowed && ( + + )} {fields.map((field) => ( {field.label} @@ -239,7 +253,7 @@ export const LinkedItems = ({ key={item.id} draggableId={"q" + String(item.id)} index={index} - isDragDisabled={type !== "edit"} + isDragDisabled={type !== "edit" && dragAllowed} > {(provided) => ( ({ {...provided.draggableProps} hover > - {type === "edit" && ( + {type === "edit" && dragAllowed && ( diff --git a/src/pages/media/show.tsx b/src/pages/media/show.tsx index 7de79c1..be12f60 100644 --- a/src/pages/media/show.tsx +++ b/src/pages/media/show.tsx @@ -43,35 +43,51 @@ export const MediaShow = () => { )} {record && record.media_type === 2 && ( - - - Видео доступно для скачивания по ссылке: - - - + + Видео доступно для скачивания по ссылке: + + + + )} {fields.map(({ label, data, render }) => ( diff --git a/src/pages/route/edit.tsx b/src/pages/route/edit.tsx index 9b490d7..1d98e83 100644 --- a/src/pages/route/edit.tsx +++ b/src/pages/route/edit.tsx @@ -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} /> diff --git a/src/pages/route/show.tsx b/src/pages/route/show.tsx index 6d2b2d2..6355843 100644 --- a/src/pages/route/show.tsx +++ b/src/pages/route/show.tsx @@ -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[][]) => {value ? 'прямое' : 'обратное'}, + label: "Направление маршрута", + data: "route_direction", + render: (value: number[][]) => ( + + {value ? "прямое" : "обратное"} + + ), }, { - label: 'Координаты маршрута', - data: 'path', + label: "Координаты маршрута", + data: "path", render: (value: number[][]) => ( theme.palette.background.paper, p: 2, borderRadius: 1, - maxHeight: '200px', - overflow: 'auto', + maxHeight: "200px", + overflow: "auto", }} > - {JSON.stringify(value)} - {/* {value?.map((point, index) => ( - - Точка {index + 1}: [{point[0]}, {point[1]}] + {value?.map((point, index) => ( + + {point[0]}, {point[1]} - ))} */} + ))} ), }, - ] + ]; return ( - {fields.map(({label, data, render}) => ( + {fields.map(({ label, data, render }) => ( {label} - {render ? render(record?.[data]) : } + {render ? ( + render(record?.[data]) + ) : ( + + )} ))} {record?.id && ( <> - type="show" parentId={record.id} parentResource="route" childResource="station" fields={stationFields} title="станции" /> + + type="show" + parentId={record.id} + parentResource="route" + childResource="station" + fields={stationFields} + title="станции" + /> - type="show" parentId={record.id} parentResource="route" childResource="vehicle" fields={vehicleFields} title="транспортные средства" /> + + type="show" + parentId={record.id} + parentResource="route" + childResource="vehicle" + fields={vehicleFields} + title="транспортные средства" + /> )} - ) -} + ); +}; diff --git a/src/preview/components/WeatherWidget/icons/DetHumidity.tsx b/src/preview/components/WeatherWidget/icons/DetHumidity.tsx new file mode 100644 index 0000000..0bad0f7 --- /dev/null +++ b/src/preview/components/WeatherWidget/icons/DetHumidity.tsx @@ -0,0 +1,16 @@ +export const DetHumidity = () => { + return ( + + + + ); +}; diff --git a/src/preview/components/WeatherWidget/icons/DetWind.tsx b/src/preview/components/WeatherWidget/icons/DetWind.tsx new file mode 100644 index 0000000..d0c1226 --- /dev/null +++ b/src/preview/components/WeatherWidget/icons/DetWind.tsx @@ -0,0 +1,24 @@ +export const DetWind = () => { + return ( + + + + + + ); +}; diff --git a/src/preview/components/WeatherWidget/weather-widget-icon.tsx b/src/preview/components/WeatherWidget/weather-widget-icon.tsx index 2b973e9..cc2816b 100644 --- a/src/preview/components/WeatherWidget/weather-widget-icon.tsx +++ b/src/preview/components/WeatherWidget/weather-widget-icon.tsx @@ -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); } diff --git a/src/preview/components/WeatherWidget/weather-widget-right.tsx b/src/preview/components/WeatherWidget/weather-widget-right.tsx index 4792e83..b03e118 100644 --- a/src/preview/components/WeatherWidget/weather-widget-right.tsx +++ b/src/preview/components/WeatherWidget/weather-widget-right.tsx @@ -59,7 +59,7 @@ export function WeatherWidgetRight({ marginBottom: 8, }} > - + %
- +  м/с
diff --git a/src/preview/components/route-preview/components/RoutePreview.tsx b/src/preview/components/route-preview/components/RoutePreview.tsx new file mode 100644 index 0000000..1d4fcb4 --- /dev/null +++ b/src/preview/components/route-preview/components/RoutePreview.tsx @@ -0,0 +1,8 @@ +import { MapWidgetProvider } from "@mt/components"; +import { RoutePreviewContainer } from "./RoutePreviewContainer"; + +export const RoutePreview = () => ( + + + +); diff --git a/src/preview/components/route-preview/components/RoutePreviewContainer.tsx b/src/preview/components/route-preview/components/RoutePreviewContainer.tsx new file mode 100644 index 0000000..c7ea2b5 --- /dev/null +++ b/src/preview/components/route-preview/components/RoutePreviewContainer.tsx @@ -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(); + + 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 ( +
+ Загрузка... +
+ ); + } + + if (isError) { + return ( +
+ Ошибка получения данных +
+ ); + } + + if (mappedData) { + return ( + + + {/* */} + + ); + } +}; diff --git a/src/preview/components/route-preview/components/RoutePreviewDashboard/RoutePreviewDashboard.module.css b/src/preview/components/route-preview/components/RoutePreviewDashboard/RoutePreviewDashboard.module.css new file mode 100644 index 0000000..17bed23 --- /dev/null +++ b/src/preview/components/route-preview/components/RoutePreviewDashboard/RoutePreviewDashboard.module.css @@ -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); +} diff --git a/src/preview/components/route-preview/components/RoutePreviewDashboard/RoutePreviewDashboard.tsx b/src/preview/components/route-preview/components/RoutePreviewDashboard/RoutePreviewDashboard.tsx new file mode 100644 index 0000000..0f13323 --- /dev/null +++ b/src/preview/components/route-preview/components/RoutePreviewDashboard/RoutePreviewDashboard.tsx @@ -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 { + routeInfo: RouteInfoData; +} + +export const RoutePreviewDashboard = ({ children, routeInfo }: Props) => { + const { setLocale } = useContext(LocalizationContext); + const [openNav, setOpenNav] = useState(false); + const [openTransfers, setOpenTransfer] = useState(false); + + return ( +
+ {/* + +
+
+ + +
*/} + + {children} + + {/*
+ setOpenTransfer(!openTransfers)} + /> + + {openTransfers ?
: null} +
+
+ +
*/} +
+ ); +}; diff --git a/src/preview/components/route-preview/components/SettingsPanel/SettingsPanel.module.css b/src/preview/components/route-preview/components/SettingsPanel/SettingsPanel.module.css new file mode 100644 index 0000000..f196ae0 --- /dev/null +++ b/src/preview/components/route-preview/components/SettingsPanel/SettingsPanel.module.css @@ -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); +} diff --git a/src/preview/components/route-preview/components/SettingsPanel/SettingsPanel.tsx b/src/preview/components/route-preview/components/SettingsPanel/SettingsPanel.tsx new file mode 100644 index 0000000..656a850 --- /dev/null +++ b/src/preview/components/route-preview/components/SettingsPanel/SettingsPanel.tsx @@ -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 ( + +
onSettingsFormChange()} + onSubmit={settingsForm.handleSubmit(onSubmit)} + > + setOpened(!isOpened)} + /> + +
+ + + + + setIsDragMode((isDrag) => !isDrag)} /> + } + /> + + + + onSettingsFormChange()} + /> + + + + + + +
+ +
+ ); +}; diff --git a/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/CoordinateField.tsx b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/CoordinateField.tsx new file mode 100644 index 0000000..8ea6036 --- /dev/null +++ b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/CoordinateField.tsx @@ -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 ( + ( + { + onChange?.(e); + handleChange(e, field); + }} + error={getFieldState(field.name).invalid} + helperText={getFieldState(field.name).error?.message || hint || ''} + /> + )} + /> + ); +}; diff --git a/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/RotationField.tsx b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/RotationField.tsx new file mode 100644 index 0000000..fd14d52 --- /dev/null +++ b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/RotationField.tsx @@ -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 ( + ( + handleChange(e, field)} + error={getFieldState(field.name).invalid} + helperText={getFieldState(field.name).error?.message || hint || ''} + {...props} + /> + )} + /> + ); +}; diff --git a/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/ScaleField.tsx b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/ScaleField.tsx new file mode 100644 index 0000000..5eb6cea --- /dev/null +++ b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/ScaleField.tsx @@ -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 ( + ( + handleChange(e, field)} + error={getFieldState(field.name).invalid} + helperText={getFieldState(field.name).error?.message || hint || ''} + {...props} + /> + )} + /> + ); +}; diff --git a/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/StationSelectField.tsx b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/StationSelectField.tsx new file mode 100644 index 0000000..16ab6ce --- /dev/null +++ b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/StationSelectField.tsx @@ -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 ( + ( + + {label} + + + {hint} + + )} + /> + ); +}; diff --git a/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/field-props.interface.ts b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/field-props.interface.ts new file mode 100644 index 0000000..a462593 --- /dev/null +++ b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/field-props.interface.ts @@ -0,0 +1,8 @@ +import { FieldPath } from 'react-hook-form'; +import { MapSettings } from '@mt/components'; + +export interface FieldProps { + name: FieldPath; + label: string; + hint?: string; +} diff --git a/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/index.ts b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/index.ts new file mode 100644 index 0000000..b435cd7 --- /dev/null +++ b/src/preview/components/route-preview/components/SettingsPanel/settigs-fields/index.ts @@ -0,0 +1,5 @@ +export * from './RotationField'; +export * from './ScaleField'; +export * from './CoordinateField'; +export * from './StationSelectField'; +export * from './field-props.interface'; diff --git a/src/preview/components/route-preview/hooks/useGetRouteData.ts b/src/preview/components/route-preview/hooks/useGetRouteData.ts new file mode 100644 index 0000000..6ca56f6 --- /dev/null +++ b/src/preview/components/route-preview/hooks/useGetRouteData.ts @@ -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(null); + + const { + data: routeData, + isSuccess, + isError, + isLoading, + } = useOne({ + resource: "route", + id: routeId, + }); + + const [stations, setStations] = useState([]); + const [sights, setSights] = useState([]); + + useEffect(() => { + fetchStations(routeId, setStations, setSights); + }, [routeData]); + + // useEffect(() => { + // if (!routeData) { + // return; + // } + + // // const data = mapRouteFromApi(routeData, []); + + // setMappedData(data); + // }, [routeData]); + + return { routeData, mappedData, isLoading, isError }; +}; diff --git a/src/preview/components/route-preview/hooks/useUpdateRouteData.ts b/src/preview/components/route-preview/hooks/useUpdateRouteData.ts new file mode 100644 index 0000000..507d07d --- /dev/null +++ b/src/preview/components/route-preview/hooks/useUpdateRouteData.ts @@ -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] + ); +}; diff --git a/src/preview/components/route-preview/index.ts b/src/preview/components/route-preview/index.ts new file mode 100644 index 0000000..a469b61 --- /dev/null +++ b/src/preview/components/route-preview/index.ts @@ -0,0 +1 @@ +export { RoutePreview } from './components/RoutePreview'; diff --git a/src/preview/components/route-preview/mappers/mapRouteFromApi.ts b/src/preview/components/route-preview/mappers/mapRouteFromApi.ts new file mode 100644 index 0000000..9e3619d --- /dev/null +++ b/src/preview/components/route-preview/mappers/mapRouteFromApi.ts @@ -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 & 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(); + 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 +) { + 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, + }; + }); +} diff --git a/src/preview/components/route-preview/mappers/mapRouteToApi.ts b/src/preview/components/route-preview/mappers/mapRouteToApi.ts new file mode 100644 index 0000000..21654e4 --- /dev/null +++ b/src/preview/components/route-preview/mappers/mapRouteToApi.ts @@ -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, + previousData +): DeepPartial => { + 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; + }), + }; +}; diff --git a/src/preview/types/constants.ts b/src/preview/types/constants.ts new file mode 100644 index 0000000..550a82a --- /dev/null +++ b/src/preview/types/constants.ts @@ -0,0 +1,5 @@ +export const LocalizedStringDefaults = { + ru: "", + en: "", + zh: "", +}; diff --git a/src/preview/types/index.ts b/src/preview/types/index.ts index 11be2c8..a55e4da 100644 --- a/src/preview/types/index.ts +++ b/src/preview/types/index.ts @@ -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"; diff --git a/src/preview/types/station.ts b/src/preview/types/station.ts new file mode 100644 index 0000000..14b0700 --- /dev/null +++ b/src/preview/types/station.ts @@ -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 = { + 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 = { + [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 ?? ""; +}; diff --git a/src/preview/widgets/dashboard/Dashboard.tsx b/src/preview/widgets/dashboard/Dashboard.tsx index b45b824..5fb808f 100644 --- a/src/preview/widgets/dashboard/Dashboard.tsx +++ b/src/preview/widgets/dashboard/Dashboard.tsx @@ -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(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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) => { + const file = e.target.files?.[0]; + if (file) { + const objectUrl = URL.createObjectURL(file); + setVideoUrl(objectUrl); + setError(null); + setIsLoading(false); + } + }; + return ( - - - -
-
- - -
+ {isLoading &&
Загрузка видео...
} + {error &&
{error}
} - - - -
- -
- + {videoUrl && ( +
+
+ )}
); diff --git a/src/providers/data.ts b/src/providers/data.ts index 2a7b309..c3a7980 100644 --- a/src/providers/data.ts +++ b/src/providers/data.ts @@ -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) => { // Добавляем токен авторизации diff --git a/svg.d.ts b/svg.d.ts new file mode 100644 index 0000000..0f17c2f --- /dev/null +++ b/svg.d.ts @@ -0,0 +1,8 @@ +declare module "*.svg" { + import * as React from "react"; + export const ReactComponent: React.FunctionComponent< + React.SVGProps + >; + const src: string; + export default src; +} diff --git a/tsconfig.json b/tsconfig.json index 8e41bce..72ea825 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,7 @@ "@mt/utils": ["src/preview/utils"] } }, - "include": ["src"], + "include": ["src", "svg.d.ts"], "references": [ { "path": "./tsconfig.node.json" diff --git a/vite.config.ts b/vite.config.ts index 78c747f..dc658a8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,7 +4,7 @@ import svgr from "vite-plugin-svgr"; import react from "@vitejs/plugin-react"; export default defineConfig({ - plugins: [react(), svgr()], + plugins: [svgr(), react()], resolve: { alias: { "@mt/common-types": path.resolve(__dirname, "./src/preview/types"),