added route preview with updated dnd

This commit is contained in:
Илья Куприец 2025-04-16 13:51:44 +03:00
parent 4dd149f2af
commit 029a2de97e
23 changed files with 7488 additions and 9168 deletions

View File

@ -11,6 +11,7 @@
"@mui/lab": "^6.0.0-beta.14", "@mui/lab": "^6.0.0-beta.14",
"@mui/material": "^6.1.7", "@mui/material": "^6.1.7",
"@mui/x-data-grid": "^7.22.2", "@mui/x-data-grid": "^7.22.2",
"@photo-sphere-viewer/core": "^5.13.1",
"@react-three/drei": "^10.0.6", "@react-three/drei": "^10.0.6",
"@react-three/fiber": "^9.1.2", "@react-three/fiber": "^9.1.2",
"@refinedev/cli": "^2.16.21", "@refinedev/cli": "^2.16.21",
@ -31,9 +32,9 @@
"i18next": "^24.2.2", "i18next": "^24.2.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"react": "^18.0.0", "react": "19.0.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.0.0", "react-dom": "19.0.0",
"react-draggable": "^4.4.6", "react-draggable": "^4.4.6",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-hook-form": "^7.30.0", "react-hook-form": "^7.30.0",

File diff suppressed because it is too large Load Diff

View File

@ -75,9 +75,14 @@ import {
} from "./components/ui/Icons"; } from "./components/ui/Icons";
import SidebarTitle from "./components/ui/SidebarTitle"; import SidebarTitle from "./components/ui/SidebarTitle";
import { AdminOnly } from "./components/AdminOnly"; import { AdminOnly } from "./components/AdminOnly";
import { Dashboard } from "./preview/widgets/dashboard/Dashboard";
import { QueryClient } from "@tanstack/react-query";
import { QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() { function App() {
return ( return (
<QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>
<ColorModeContextProvider> <ColorModeContextProvider>
<CssBaseline /> <CssBaseline />
@ -257,6 +262,7 @@ function App() {
/> />
<Route path="show/:id" element={<CountryShow />} /> <Route path="show/:id" element={<CountryShow />} />
</Route> </Route>
<Route path="dashboard" element={<Dashboard />} />
<Route path="/city"> <Route path="/city">
<Route index element={<CityList />} /> <Route index element={<CityList />} />
@ -421,6 +427,7 @@ function App() {
</RefineSnackbarProvider> </RefineSnackbarProvider>
</ColorModeContextProvider> </ColorModeContextProvider>
</BrowserRouter> </BrowserRouter>
</QueryClientProvider>
); );
} }

View File

@ -19,15 +19,13 @@ import {
TableRow, TableRow,
Paper, Paper,
TableBody, TableBody,
IconButton,
} from "@mui/material"; } from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import { axiosInstance } from "../providers/data"; import { axiosInstance } from "../providers/data";
import { Link } from "react-router";
import { TOKEN_KEY } from "../authProvider"; import { TOKEN_KEY } from "../authProvider";
import { Droppable, Draggable, DragDropContext } from "@hello-pangea/dnd"; import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
// TODO: ДОДЕЛАТЬ
type Field<T> = { type Field<T> = {
label: string; label: string;
@ -52,11 +50,10 @@ type LinkedItemsProps<T> = {
extraField?: ExtraFieldConfig; extraField?: ExtraFieldConfig;
}; };
const reorder = (list, startIndex, endIndex) => { const reorder = (list: any[], startIndex: number, endIndex: number) => {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed); result.splice(endIndex, 0, removed);
return result; return result;
}; };
@ -76,18 +73,20 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
const [mediaOrder, setMediaOrder] = useState<number>(1); const [mediaOrder, setMediaOrder] = useState<number>(1);
const theme = useTheme(); const theme = useTheme();
const onDragEnd = (result) => { const onDragEnd = (result: any) => {
// ドロップ先がない if (!result.destination) return;
if (!result.destination) {
return; const reorderedItems = reorder(
} linkedItems,
// 配列の順序を入れ替える result.source.index,
let movedItems = reorder( result.destination.index
linkedItems, // 順序を入れ変えたい配列
result.source.index, // 元の配列の位置
result.destination.index // 移動先の配列の位置
); );
setLinkedItems(movedItems);
setLinkedItems(reorderedItems);
// If you need to save the new order to the backend, you would add that here
// For example:
// saveNewOrder(reorderedItems);
}; };
useEffect(() => { useEffect(() => {
@ -212,57 +211,68 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ background: theme.palette.background.paper }}> <AccordionDetails sx={{ background: theme.palette.background.paper }}>
<Stack gap={2}>
<DragDropContext onDragEnd={onDragEnd}>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table aria-label="simple table"> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>ID</TableCell> {type === "edit" && <TableCell width="40px"></TableCell>}
<TableCell>Name</TableCell> {fields.map((field) => (
<TableCell>Действие</TableCell> <TableCell key={String(field.data)}>
{field.label}
</TableCell>
))}
{type === "edit" && (
<TableCell width="120px">Действие</TableCell>
)}
</TableRow> </TableRow>
</TableHead> </TableHead>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable"> <Droppable droppableId="droppable">
{(provided) => ( {(provided) => (
<TableBody <TableBody
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps} {...provided.droppableProps}
> >
{linkedItems.length > 0 && {linkedItems.map((item, index) => (
linkedItems.map((item, index) => (
<Draggable <Draggable
key={item.id} key={item.id}
draggableId={"q" + item.id.toString()} draggableId={"q" + String(item.id)}
index={index} index={index}
isDragDisabled={type !== "edit"}
> >
{(provided) => ( {(provided) => (
<TableRow <TableRow
ref={provided.innerRef} ref={provided.innerRef}
{...provided.dragHandleProps}
{...provided.draggableProps} {...provided.draggableProps}
style={{ hover
...provided.draggableProps.style,
}}
> >
<TableCell style={{ flex: 1, minWidth: "100px" }}> {type === "edit" && (
{item.id} <TableCell {...provided.dragHandleProps}>
<IconButton size="small">
<DragIndicatorIcon />
</IconButton>
</TableCell> </TableCell>
<TableCell style={{ flex: 1, minWidth: "100px" }}> )}
{item.name} {fields.map((field) => (
<TableCell key={String(field.data)}>
{field.render
? field.render(item[field.data])
: item[field.data]}
</TableCell> </TableCell>
<TableCell style={{ flex: 1, minWidth: "100px" }}> ))}
{type === "edit" && (
<TableCell>
<Button <Button
variant="outlined" variant="outlined"
color="error" color="error"
size="small" size="small"
onClick={(e) => { onClick={() => deleteItem(item.id)}
e.preventDefault();
deleteItem(item.id);
}}
> >
Отвязать Отвязать
</Button> </Button>
</TableCell> </TableCell>
)}
</TableRow> </TableRow>
)} )}
</Draggable> </Draggable>
@ -271,85 +281,18 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
</TableBody> </TableBody>
)} )}
</Droppable> </Droppable>
</DragDropContext>
</Table> </Table>
</TableContainer> </TableContainer>
</DragDropContext>
{/* <Stack gap={2}> {linkedItems.length === 0 && !isLoading && (
<Grid container gap={1.25}> <Typography color="textSecondary" textAlign="center" py={2}>
{isLoading ? ( {title} не найдены
<Typography>Загрузка...</Typography>
) : linkedItems.length > 0 ? (
linkedItems.map((item, index) => (
<Box
component={Link}
to={`/${childResource}/show/${item.id}`}
key={index}
sx={{
marginTop: "8px",
padding: "14px",
borderRadius: 2,
border: `2px solid ${theme.palette.divider}`,
width: childResource === "article" ? "100%" : "auto",
textDecoration: "none",
color: "inherit",
display: "block",
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
}}
>
<Stack gap={0.25}>
{childResource === "media" && item.id && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}/${
item.iimport { DragDropContext } from 'react-beautiful-dnd';
d
}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
alt={String(item.media_name)}
style={{
width: "100%",
height: "120px",
objectFit: "contain",
marginBottom: "8px",
borderRadius: 4,
}}
/>
)}
{fields.map(({ label, data, render }) => (
<Typography
variant="body2"
color="textSecondary"
key={String(data)}
>
<strong>{label}:</strong>{" "}
{render ? render(item[data]) : item[data]}
</Typography> </Typography>
))}
{type === "edit" && (
<Button
variant="outlined"
color="error"
size="small"
onClick={(e) => {
e.preventDefault();
deleteItem(item.id);
}}
sx={{ mt: 1.5 }}
>
Отвязать
</Button>
)} )}
</Stack>
</Box>
))
) : (
<Typography color="textSecondary">{title} не найдены</Typography>
)}
</Grid>
{type === "edit" && ( {type === "edit" && (
<Stack gap={2}> <Stack gap={2} mt={2}>
<Typography variant="subtitle1">Добавить {title}</Typography> <Typography variant="subtitle1">Добавить {title}</Typography>
<Autocomplete <Autocomplete
fullWidth fullWidth
@ -373,7 +316,6 @@ d
option.id === value?.id option.id === value?.id
} }
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
// return options.filter((option) => String(option[fields[0].data]).toLowerCase().includes(inputValue.toLowerCase()))
const searchWords = inputValue const searchWords = inputValue
.toLowerCase() .toLowerCase()
.split(" ") .split(" ")
@ -398,7 +340,7 @@ d
value={pageNum} value={pageNum}
onChange={(e) => { onChange={(e) => {
const newValue = Number(e.target.value); const newValue = Number(e.target.value);
const minValue = linkedItems.length + 1; // page number on articles lenght const minValue = linkedItems.length + 1;
setPageNum(newValue < minValue ? minValue : newValue); setPageNum(newValue < minValue ? minValue : newValue);
}} }}
fullWidth fullWidth
@ -407,7 +349,7 @@ d
</FormControl> </FormControl>
)} )}
{childResource === "media" && type === "edit" && ( {childResource === "media" && (
<FormControl fullWidth> <FormControl fullWidth>
<TextField <TextField
type="number" type="number"
@ -429,12 +371,13 @@ d
variant="contained" variant="contained"
onClick={linkItem} onClick={linkItem}
disabled={!selectedItemId} disabled={!selectedItemId}
sx={{ alignSelf: "flex-start" }}
> >
Добавить Добавить
</Button> </Button>
</Stack> </Stack>
)} )}
</Stack> */} </Stack>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
); );

View File

@ -1,44 +1,49 @@
import {Box, TextField} from '@mui/material' import { Icons } from "../../preview/components";
import {Edit} from '@refinedev/mui' import { Box, TextField } from "@mui/material";
import {useForm} from '@refinedev/react-hook-form' import { Edit } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
export const CountryEdit = () => { export const CountryEdit = () => {
const { const {
saveButtonProps, saveButtonProps,
register, register,
formState: {errors}, formState: { errors },
} = useForm({}) } = useForm({});
return ( return (
<Edit saveButtonProps={saveButtonProps}> <Edit saveButtonProps={saveButtonProps}>
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off"> <Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField <TextField
{...register('code', { {...register("code", {
required: 'Это поле является обязательным', required: "Это поле является обязательным",
})} })}
error={!!(errors as any)?.code} error={!!(errors as any)?.code}
helperText={(errors as any)?.code?.message} helperText={(errors as any)?.code?.message}
margin="normal" margin="normal"
fullWidth fullWidth
InputLabelProps={{shrink: true}} InputLabelProps={{ shrink: true }}
type="text" type="text"
label={'Код *'} label={"Код *"}
name="code" name="code"
/> />
<TextField <TextField
{...register('name', { {...register("name", {
required: 'Это поле является обязательным', required: "Это поле является обязательным",
})} })}
error={!!(errors as any)?.name} error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message} helperText={(errors as any)?.name?.message}
margin="normal" margin="normal"
fullWidth fullWidth
InputLabelProps={{shrink: true}} InputLabelProps={{ shrink: true }}
type="text" type="text"
label={'Название *'} label={"Название *"}
name="name" name="name"
/> />
</Box> </Box>
</Edit> </Edit>
) );
} };

View File

@ -1,10 +1,10 @@
import cn from 'classnames'; import cn from "classnames";
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from "react";
import './AttractionMedia.css'; import "./AttractionMedia.css";
import ModelViewer from '../../model-viewer/ModelViewer'; import ModelViewer from "../../model-viewer/ModelViewer";
import { Icons, useLightboxContext } from '@mt/components'; import { Icons, useLightboxContext } from "@mt/components";
import { Object3DLightboxData } from '@mt/common-types'; import { Object3DLightboxData } from "@mt/common-types";
interface Object3DMediaProps { interface Object3DMediaProps {
url: string; url: string;
@ -19,7 +19,7 @@ export const Object3DMedia = ({ url, watermarkUrl }: Object3DMediaProps) => {
const handle3DFullscreenOpen = () => { const handle3DFullscreenOpen = () => {
setAutoRotate(false); setAutoRotate(false);
setData({ setData({
type: 'OBJECT_3D', type: "OBJECT_3D",
modelUrl: url, modelUrl: url,
watermarkUrl, watermarkUrl,
}); });
@ -33,12 +33,14 @@ export const Object3DMedia = ({ url, watermarkUrl }: Object3DMediaProps) => {
return ( return (
<div className="widget-media__wrapper"> <div className="widget-media__wrapper">
<div <div
className={cn('widget-3d-model', { className={cn("widget-3d-model", {
'media-with-watermark': watermarkUrl !== null, "media-with-watermark": watermarkUrl !== null,
})} })}
> >
<ModelViewer key={url} pathToModel={url} autoRotate={autoRotate} /> <ModelViewer key={url} pathToModel={url} autoRotate={autoRotate} />
{watermarkUrl && <img src={watermarkUrl} alt="Watermark" className="watermark" />} {watermarkUrl && (
<img src={watermarkUrl} alt="Watermark" className="watermark" />
)}
</div> </div>
<Icons.FullscreenIcon <Icons.FullscreenIcon

View File

@ -1,9 +1,9 @@
import cn from 'classnames'; import cn from "classnames";
import { HTMLAttributes, ReactNode } from 'react'; import { HTMLAttributes, ReactNode } from "react";
import { StyledDrawer } from './Drawer.styles'; import { StyledDrawer } from "./Drawer.styles";
import { Icons } from '@mt/components'; import { Icons } from "@mt/components";
import { Locale, LocaleSwitcher } from '@mt/i18n'; import { Locale, LocaleSwitcher } from "@mt/i18n";
export interface DrawerProps extends HTMLAttributes<HTMLDivElement> { export interface DrawerProps extends HTMLAttributes<HTMLDivElement> {
onToggle: (isOpened: boolean) => void; onToggle: (isOpened: boolean) => void;
@ -24,11 +24,17 @@ export function Drawer({
...props ...props
}: DrawerProps) { }: DrawerProps) {
return ( return (
<StyledDrawer className={cn('g-flex-column', { 'nav-widget--opened': isOpen })} {...props}> <StyledDrawer
className={cn("g-flex-column", { "nav-widget--opened": isOpen })}
{...props}
>
{children} {children}
<div className="g-flex actions"> <div className="g-flex actions">
<div className="action-btn toggle-btn" onPointerUp={() => onToggle(!isOpen)}> <div
className="action-btn toggle-btn"
onPointerUp={() => onToggle(!isOpen)}
>
<Icons.ArrowBtn /> <Icons.ArrowBtn />
</div> </div>

View File

@ -5,9 +5,20 @@ import {
} from "react-simple-maps"; } from "react-simple-maps";
import styles from "./MapWidget.module.css"; import styles from "./MapWidget.module.css";
import { mapCanvasProps, useMapWidgetContext } from "../../MapWidgetContext"; import { mapCanvasProps, useMapWidgetContext } from "../../MapWidgetContext";
import { useState } from "react"; import { useState, FC, ReactNode } from "react";
import { MapContent } from "./MapContent"; import { MapContent } from "./MapContent";
// Create wrapper components to handle type issues
const ComposableMapWrapper: FC<any> = (props) => {
// @ts-ignore - Ignore type issues with the ComposableMap component
return <ComposableMap {...props} />;
};
const ZoomableGroupWrapper: FC<ZoomableGroupProps> = (props) => {
// @ts-ignore - Ignore type issues with the ZoomableGroup component
return <ZoomableGroup {...props} />;
};
// default coordinates for 3a route: 59.943, 30.331 // default coordinates for 3a route: 59.943, 30.331
export const MapWidget = () => { export const MapWidget = () => {
const { onMapCenterMoved, projection, isDragMode, rotateAngle } = const { onMapCenterMoved, projection, isDragMode, rotateAngle } =
@ -34,13 +45,12 @@ export const MapWidget = () => {
}; };
return ( return (
<ComposableMap <ComposableMapWrapper
// Cast to any need due to error in react-simple-maps typings
projection={projection as any} projection={projection as any}
className={styles.mapWidget} className={styles.mapWidget}
{...mapCanvasProps} {...mapCanvasProps}
> >
<ZoomableGroup <ZoomableGroupWrapper
key={key} key={key}
center={projection.center()} center={projection.center()}
onMoveEnd={handleMoveEnd} onMoveEnd={handleMoveEnd}
@ -49,7 +59,7 @@ export const MapWidget = () => {
maxZoom={1} maxZoom={1}
> >
<MapContent /> <MapContent />
</ZoomableGroup> </ZoomableGroupWrapper>
</ComposableMap> </ComposableMapWrapper>
); );
}; };

View File

@ -1,4 +1,4 @@
import { Marker, Point } from "react-simple-maps"; import { Point, Marker } from "react-simple-maps";
import { Icons } from "@mt/components"; import { Icons } from "@mt/components";
import { AttractionGroupIconSizeType } from "@mt/common-types"; import { AttractionGroupIconSizeType } from "@mt/common-types";

View File

@ -0,0 +1,7 @@
export const MyComponent = () => {
return (
<div style={{ width: "100px", height: "100px", backgroundColor: "red" }}>
MyComponent
</div>
);
};

View File

@ -1,6 +1,6 @@
import { HTMLAttributes, ReactNode } from "react"; import { HTMLAttributes, ReactNode } from "react";
import { Icons } from "../icons"; import { Icons } from "../Icons";
import { TransportType } from "@mt/common-types"; import { TransportType } from "@mt/common-types";
const transportStopIcons: Record<TransportType, ReactNode> = { const transportStopIcons: Record<TransportType, ReactNode> = {

View File

@ -8,4 +8,5 @@ export * from "./MapWidget";
export * from "./Drawer"; export * from "./Drawer";
export * from "./lightbox"; export * from "./lightbox";
export * from "./model-viewer"; export * from "./model-viewer";
export * from "./icons"; export { Icons } from "./Icons";
export * from "./MyComponent";

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from "react";
import { useLightboxContext } from './LightboxContext'; import { useLightboxContext } from "./LightboxContext";
import './Lightbox.css'; import "./Lightbox.css";
import { LightboxData } from '@mt/common-types'; import { LightboxData } from "@mt/common-types";
import { LightboxContent } from './LightboxContent'; import { LightboxContent } from "./LightboxContent";
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from "react-intl";
export const Lightbox = () => { export const Lightbox = () => {
// prettier-ignore // prettier-ignore
@ -13,14 +13,24 @@ export const Lightbox = () => {
return lightboxActive ? ( return lightboxActive ? (
<div className="lightbox-overlay"> <div className="lightbox-overlay">
<div className="lightbox-content"> <div className="lightbox-content">
<div className="lightbox-content__wrapper" style={{ height: '749px', width: '1220px' }}> <div
className="lightbox-content__wrapper"
style={{ height: "749px", width: "1220px" }}
>
<LightboxContent /> <LightboxContent />
{data.watermarkUrl && ( {data.watermarkUrl && (
<img src={data.watermarkUrl} alt="Watermark" className="watermark" /> <img
src={data.watermarkUrl}
alt="Watermark"
className="watermark"
/>
)} )}
<button className="lightbox-content__close-btn" onPointerUp={closeLightbox}> <button
className="lightbox-content__close-btn"
onPointerUp={closeLightbox}
>
<FormattedMessage id="close" /> <FormattedMessage id="close" />
</button> </button>
</div> </div>

View File

@ -1,8 +1,8 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from "react";
// TODO: resolve circular deps (probably we should move icons to a separate lib) // TODO: resolve circular deps (probably we should move icons to a separate lib)
import { Icons } from '@mt/components'; import { Icons } from "@mt/components";
import { Locale, localesMap } from '../i18n.interface'; import { Locale, localesMap } from "../i18n.interface";
import './LocaleSwitcher.css'; import "./LocaleSwitcher.css";
interface LocaleSwitcherProps { interface LocaleSwitcherProps {
onLocaleChange: (locale: Locale) => void; onLocaleChange: (locale: Locale) => void;
@ -10,7 +10,7 @@ interface LocaleSwitcherProps {
export const LocaleSwitcher = ({ onLocaleChange }: LocaleSwitcherProps) => { export const LocaleSwitcher = ({ onLocaleChange }: LocaleSwitcherProps) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [selectedLocale, setSelectedLocale] = useState<Locale>('ru'); const [selectedLocale, setSelectedLocale] = useState<Locale>("ru");
const handleLocaleChange = useCallback( const handleLocaleChange = useCallback(
(locale: Locale) => { (locale: Locale) => {
@ -24,7 +24,10 @@ export const LocaleSwitcher = ({ onLocaleChange }: LocaleSwitcherProps) => {
return ( return (
<div className="locale-switcher"> <div className="locale-switcher">
{!isOpen ? ( {!isOpen ? (
<button className="locale-switcher__button" onPointerUp={() => setIsOpen(!isOpen)}> <button
className="locale-switcher__button"
onPointerUp={() => setIsOpen(!isOpen)}
>
<Icons.I18NIcon /> <Icons.I18NIcon />
</button> </button>
) : ( ) : (
@ -32,7 +35,9 @@ export const LocaleSwitcher = ({ onLocaleChange }: LocaleSwitcherProps) => {
{Object.entries(localesMap).map(([label, locale]) => ( {Object.entries(localesMap).map(([label, locale]) => (
<button <button
key={locale} key={locale}
className={`locale-switcher__option ${selectedLocale === locale ? 'selected' : ''}`} className={`locale-switcher__option ${
selectedLocale === locale ? "selected" : ""
}`}
onPointerUp={() => handleLocaleChange(locale)} onPointerUp={() => handleLocaleChange(locale)}
> >
{label} {label}

View File

@ -2,7 +2,6 @@ import {
LongPollingQueryOptions, LongPollingQueryOptions,
useLongPollingQuery, useLongPollingQuery,
} from "./useLongPollingQuery"; } from "./useLongPollingQuery";
import { useId } from "react";
export type EventQueryData<T> = T & { export type EventQueryData<T> = T & {
["@type"]: string; ["@type"]: string;

View File

@ -1,19 +1,20 @@
import { MapWidget, useMapWidgetContext } from '@mt/components'; import { MapWidget, useMapWidgetContext } from "@mt/components";
import { useGetMapData } from './useGetMapData'; import { useGetMapData } from "./useGetMapData";
import { EventQueryData, useEventQuery, useLoading } from '@mt/utils'; import { EventQueryData, useEventQuery, useLoading } from "@mt/utils";
import { useEffect } from 'react'; import { useEffect } from "react";
// TODO: resolve circular deps // TODO: resolve circular deps
import { Coordinates } from '@mt/common-types'; import { Coordinates } from "@mt/common-types";
export const MapWidgetContainer = () => { export const MapWidgetContainer = () => {
const { isLoading } = useLoading(); const { isLoading } = useLoading();
const { data, refetch } = useGetMapData(); const { data, refetch } = useGetMapData();
const { currentPosition, setCurrentPosition, onMapDataFetched } = useMapWidgetContext(); const { currentPosition, setCurrentPosition, onMapDataFetched } =
useMapWidgetContext();
const { data: events = [], isSuccess } = useEventQuery('/widgets/route-map/events', [ const { data: events = [], isSuccess } = useEventQuery(
'REFRESH_DATA', "/widgets/route-map/events",
'UPDATE_CURRENT_POINT_ON_TRACK', ["REFRESH_DATA", "UPDATE_CURRENT_POINT_ON_TRACK"]
]); );
useEffect(() => { useEffect(() => {
if (!data) { if (!data) {
@ -28,7 +29,7 @@ export const MapWidgetContainer = () => {
return; return;
} }
if (events.some((e) => e['@type'] === 'REFRESH_DATA')) { if (events.some((e) => e["@type"] === "REFRESH_DATA")) {
refetch(); refetch();
return; return;

View File

@ -1,10 +1,12 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { AttractionWidgetContainer } from "../attractions-widget/AttractionWidgetContainer"; import { Drawer } from "@mt/components";
import { WeatherWidgetContainer } from "../WeatherWidget/WeatherWidgetContainer"; import { Locale } from "@mt/i18n";
import { OperativeInfoWidget } from "../operative-info-widget/operative-info-widget";
import { NavWidgetContainer } from "../nav-widget/nav-widget-container"; import { NavWidgetContainer } from "../nav-widget/nav-widget-container";
import { MapWidgetContainer } from "../MapWidgetContainer";
import { RouteInfoWidgetContainer } from "../RouteInfoWidgetContainer/RouteInfoWidgetContainer"; 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";
const StyledDashboard = styled.div` const StyledDashboard = styled.div`
background-color: #000; background-color: #000;
@ -49,6 +51,16 @@ const StyledDashboard = styled.div`
export function Dashboard() { export function Dashboard() {
return ( return (
<StyledDashboard> <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 /> <NavWidgetContainer />
<div className="container"> <div className="container">

View File

@ -1,7 +1,7 @@
import { HTMLAttributes } from 'react'; import { HTMLAttributes } from "react";
import { NavWidget } from './nav-widget'; import { NavWidget } from "./nav-widget";
import { useGetStations } from './hooks/useGetStations'; import { useGetStations } from "./hooks/useGetStations";
import { useGetAttractions } from './hooks'; import { useGetAttractions } from "./hooks";
export function NavWidgetContainer(props: HTMLAttributes<HTMLDivElement>) { export function NavWidgetContainer(props: HTMLAttributes<HTMLDivElement>) {
const { data: stations } = useGetStations(); const { data: stations } = useGetStations();

View File

@ -1,16 +1,23 @@
import cn from 'classnames'; import cn from "classnames";
import { HTMLAttributes, ReactNode, useContext, useEffect, useMemo, useState } from 'react'; import {
import { LocalizationContext, useServerLocalization } from '@mt/i18n'; HTMLAttributes,
import { Order, uuid } from '@mt/common-types'; ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { LocalizationContext, useServerLocalization } from "@mt/i18n";
import { Order, uuid } from "@mt/common-types";
import { AttractionCard, AccordionListTab, NestedItems } from './components'; import { AttractionCard, AccordionListTab, NestedItems } from "./components";
import { Drawer, Icons } from '@mt/components'; import { Drawer, Icons } from "@mt/components";
import { Attraction, Station } from './nav-widget.interface'; import { Attraction, Station } from "./nav-widget.interface";
import { HomeTab } from './components'; import { HomeTab } from "./components";
import { StyledNavWidget } from './nav-widget.styles'; import { StyledNavWidget } from "./nav-widget.styles";
export type NavTabs = 'stationsTab' | 'attractionsTab'; export type NavTabs = "stationsTab" | "attractionsTab";
export interface NavWidgetProps extends HTMLAttributes<HTMLDivElement> { export interface NavWidgetProps extends HTMLAttributes<HTMLDivElement> {
stations: Station[]; stations: Station[];
@ -22,14 +29,19 @@ export function NavWidget({ stations, attractions }: NavWidgetProps) {
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const [openedTab, setOpenedTab] = useState<NavTabs | null>(null); const [openedTab, setOpenedTab] = useState<NavTabs | null>(null);
const [attractionId, setAttractionId] = useState<uuid | null>(null); const [attractionId, setAttractionId] = useState<uuid | null>(null);
const [attractionOrder, setAttractionOrder] = useState<Order>('asc'); const [attractionOrder, setAttractionOrder] = useState<Order>("asc");
const sortAttractionsBtn: ReactNode = useMemo(() => { const sortAttractionsBtn: ReactNode = useMemo(() => {
if (openedTab === 'attractionsTab') { if (openedTab === "attractionsTab") {
return ( return (
<div <div
className={cn([{ 'order-btn-inverse': attractionOrder === 'desc' }, 'action-btn'])} className={cn([
onPointerUp={() => setAttractionOrder((prev) => (prev === 'asc' ? 'desc' : 'asc'))} { "order-btn-inverse": attractionOrder === "desc" },
"action-btn",
])}
onPointerUp={() =>
setAttractionOrder((prev) => (prev === "asc" ? "desc" : "asc"))
}
> >
<Icons.SortIcon /> <Icons.SortIcon />
</div> </div>
@ -64,8 +76,10 @@ export function NavWidget({ stations, attractions }: NavWidgetProps) {
})) }))
.sort( .sort(
(a, b) => (a, b) =>
localizeText(a.name).toLowerCase().localeCompare(localizeText(b.name).toLowerCase()) * localizeText(a.name)
(attractionOrder === 'asc' ? 1 : -1) .toLowerCase()
.localeCompare(localizeText(b.name).toLowerCase()) *
(attractionOrder === "asc" ? 1 : -1)
); );
}, [attractions, attractionOrder, locale]); }, [attractions, attractionOrder, locale]);
@ -74,7 +88,7 @@ export function NavWidget({ stations, attractions }: NavWidgetProps) {
return ( return (
<Drawer <Drawer
className={cn({ 'nav-widget--opened': isOpen })} className={cn({ "nav-widget--opened": isOpen })}
isOpen={isOpen} isOpen={isOpen}
onToggle={setIsOpen} onToggle={setIsOpen}
onHomeBtnClick={() => setOpenedTab(null)} onHomeBtnClick={() => setOpenedTab(null)}
@ -86,14 +100,14 @@ export function NavWidget({ stations, attractions }: NavWidgetProps) {
<AccordionListTab <AccordionListTab
titleId="stops" titleId="stops"
isOpened={openedTab === 'stationsTab'} isOpened={openedTab === "stationsTab"}
items={mappedStations} items={mappedStations}
onNestedItemClick={setAttractionId} onNestedItemClick={setAttractionId}
/> />
<AccordionListTab <AccordionListTab
titleId="attractions" titleId="attractions"
isOpened={openedTab === 'attractionsTab'} isOpened={openedTab === "attractionsTab"}
items={mappedAttractions} items={mappedAttractions}
onExpandChange={handleExpandChange} onExpandChange={handleExpandChange}
/> />

View File

@ -5,22 +5,23 @@
"lib": ["DOM", "DOM.Iterable", "ESNext"], "lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false, "allowJs": false,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": false, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": false,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": ".",
"paths": { "paths": {
"@mt/common-types": ["./src/preview/types"], "@mt/common-types": ["src/preview/types"],
"@mt/components": ["./src/preview/components"], "@mt/components": ["src/preview/components"],
"@mt/i18n": ["./src/preview/i18n"], "@mt/i18n": ["src/preview/i18n"],
"@mt/widgets": ["./src/preview/widgets"], "@mt/widgets": ["src/preview/widgets"],
"@mt/utils": ["./src/preview/utils"] "@mt/utils": ["src/preview/utils"]
} }
}, },
"include": ["src"], "include": ["src"],

View File

@ -2,7 +2,17 @@
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node" "moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@mt/common-types": ["src/preview/types"],
"@mt/components": ["src/preview/components"],
"@mt/i18n": ["src/preview/i18n"],
"@mt/widgets": ["src/preview/widgets"],
"@mt/utils": ["src/preview/utils"]
}
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }

View File

@ -1,7 +1,7 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import * as path from "path"; import * as path from "path";
import svgr from "vite-plugin-svgr"; import svgr from "vite-plugin-svgr";
import react from "@vitejs/plugin-react";
export default defineConfig({ export default defineConfig({
plugins: [react(), svgr()], plugins: [react(), svgr()],

6875
yarn.lock Normal file

File diff suppressed because it is too large Load Diff