fetching data from api for route preview

This commit is contained in:
Илья Куприец 2025-04-20 10:55:12 +03:00
parent 029a2de97e
commit 9e34a71e14
34 changed files with 1534 additions and 457 deletions

View File

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

View File

@ -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 />

View File

@ -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 }) => (

View File

@ -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>

View File

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

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

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

View File

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

View File

@ -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 ?? "--"} />
&nbsp;м/с
</div>

View File

@ -0,0 +1,8 @@
import { MapWidgetProvider } from "@mt/components";
import { RoutePreviewContainer } from "./RoutePreviewContainer";
export const RoutePreview = () => (
<MapWidgetProvider>
<RoutePreviewContainer />
</MapWidgetProvider>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 || ''}
/>
)}
/>
);
};

View File

@ -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}
/>
)}
/>
);
};

View File

@ -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}
/>
)}
/>
);
};

View File

@ -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>
)}
/>
);
};

View File

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

View File

@ -0,0 +1,5 @@
export * from './RotationField';
export * from './ScaleField';
export * from './CoordinateField';
export * from './StationSelectField';
export * from './field-props.interface';

View File

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

View File

@ -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]
);
};

View File

@ -0,0 +1 @@
export { RoutePreview } from './components/RoutePreview';

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

View File

@ -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;
}),
};
};

View File

@ -0,0 +1,5 @@
export const LocalizedStringDefaults = {
ru: "",
en: "",
zh: "",
};

View File

@ -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";

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

View File

@ -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>
);

View File

@ -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) => {
// Добавляем токен авторизации

8
svg.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
declare module "*.svg" {
import * as React from "react";
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement>
>;
const src: string;
export default src;
}

View File

@ -24,7 +24,7 @@
"@mt/utils": ["src/preview/utils"]
}
},
"include": ["src"],
"include": ["src", "svg.d.ts"],
"references": [
{
"path": "./tsconfig.node.json"

View File

@ -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"),