Add Map page
with basic logic
This commit is contained in:
parent
a8777a974a
commit
5814e65953
@ -22,6 +22,7 @@
|
||||
"lucide-react": "^0.511.0",
|
||||
"mobx": "^6.13.7",
|
||||
"mobx-react-lite": "^4.1.0",
|
||||
"ol": "^10.5.0",
|
||||
"path": "^0.12.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
@ -5,6 +5,11 @@ import {
|
||||
LoginPage,
|
||||
MainPage,
|
||||
SightPage,
|
||||
MapPage,
|
||||
MediaListPage,
|
||||
PreviewMediaPage,
|
||||
EditMediaPage,
|
||||
// CreateMediaPage,
|
||||
} from "@pages";
|
||||
import { authStore, createSightStore, editSightStore } from "@shared";
|
||||
import { Layout } from "@widgets";
|
||||
@ -71,6 +76,11 @@ export const Router = () => {
|
||||
<Route path="sight/:id" element={<EditSightPage />} />
|
||||
<Route path="sight/create" element={<CreateSightPage />} />
|
||||
<Route path="devices" element={<DevicesPage />} />
|
||||
<Route path="map" element={<MapPage />} />
|
||||
<Route path="media" element={<MediaListPage />} />
|
||||
<Route path="media/:id" element={<PreviewMediaPage />} />
|
||||
<Route path="media/:id/edit" element={<EditMediaPage />} />
|
||||
{/* <Route path="media/create" element={<CreateMediaPage />} /> */}
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
126
src/pages/CreateMediaPage/index.tsx
Normal file
126
src/pages/CreateMediaPage/index.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
// import { Button, Paper, Typography, Box, Alert, Snackbar } from "@mui/material";
|
||||
// import { useNavigate } from "react-router-dom";
|
||||
// import { ArrowLeft, Upload } from "lucide-react";
|
||||
// import { observer } from "mobx-react-lite";
|
||||
// import { useState, DragEvent, useRef, useEffect } from "react";
|
||||
// import { editSightStore, UploadMediaDialog } from "@shared";
|
||||
|
||||
// export const CreateMediaPage = observer(() => {
|
||||
// const navigate = useNavigate();
|
||||
// const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
// const [error, setError] = useState<string | null>(null);
|
||||
// const [success, setSuccess] = useState(false);
|
||||
// const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
// const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||
|
||||
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
// setIsDragging(false);
|
||||
|
||||
// const files = Array.from(e.dataTransfer.files);
|
||||
// if (files.length > 0) {
|
||||
// editSightStore.fileToUpload = files[0];
|
||||
// setUploadDialogOpen(true);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// setIsDragging(true);
|
||||
// };
|
||||
|
||||
// const handleDragLeave = () => {
|
||||
// setIsDragging(false);
|
||||
// };
|
||||
|
||||
// const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const files = e.target.files;
|
||||
// if (files && files.length > 0) {
|
||||
// editSightStore.fileToUpload = files[0];
|
||||
// setUploadDialogOpen(true);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleUploadSuccess = () => {
|
||||
// setSuccess(true);
|
||||
// setUploadDialogOpen(false);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="w-full h-full p-8">
|
||||
// <div className="flex items-center gap-4 mb-8">
|
||||
// <Button
|
||||
// variant="outlined"
|
||||
// startIcon={<ArrowLeft size={20} />}
|
||||
// onClick={() => navigate("/media")}
|
||||
// >
|
||||
// Назад
|
||||
// </Button>
|
||||
// <Typography variant="h5">Загрузка медиафайла</Typography>
|
||||
// </div>
|
||||
|
||||
// <Paper
|
||||
// elevation={3}
|
||||
// className={`w-full h-[60vh] flex flex-col items-center justify-center p-8 transition-colors ${
|
||||
// isDragging ? "bg-blue-50 border-2 border-blue-500" : "bg-gray-50"
|
||||
// }`}
|
||||
// onDrop={handleDrop}
|
||||
// onDragOver={handleDragOver}
|
||||
// onDragLeave={handleDragLeave}
|
||||
// >
|
||||
// <input
|
||||
// type="file"
|
||||
// ref={fileInputRef}
|
||||
// className="hidden"
|
||||
// onChange={handleFileSelect}
|
||||
// accept="image/*,video/*,.glb,.gltf"
|
||||
// />
|
||||
|
||||
// <Box className="flex flex-col items-center gap-4 text-center">
|
||||
// <Upload size={48} className="text-gray-400" />
|
||||
// <Typography variant="h6" className="text-gray-600">
|
||||
// Перетащите файл сюда или
|
||||
// </Typography>
|
||||
// <Button
|
||||
// variant="contained"
|
||||
// onClick={() => fileInputRef.current?.click()}
|
||||
// startIcon={<Upload size={20} />}
|
||||
// >
|
||||
// Выберите файл
|
||||
// </Button>
|
||||
// <Typography variant="body2" className="text-gray-500 mt-4">
|
||||
// Поддерживаемые форматы: JPG, PNG, GIF, MP4, WebM, GLB, GLTF
|
||||
// </Typography>
|
||||
// </Box>
|
||||
// </Paper>
|
||||
|
||||
// <UploadMediaDialog
|
||||
// open={uploadDialogOpen}
|
||||
// onClose={() => setUploadDialogOpen(false)}
|
||||
// afterUpload={handleUploadSuccess}
|
||||
// />
|
||||
|
||||
// <Snackbar
|
||||
// open={success}
|
||||
// autoHideDuration={2000}
|
||||
// onClose={() => setSuccess(false)}
|
||||
// >
|
||||
// <Alert severity="success" onClose={() => setSuccess(false)}>
|
||||
// Медиафайл успешно загружен
|
||||
// </Alert>
|
||||
// </Snackbar>
|
||||
|
||||
// <Snackbar
|
||||
// open={!!error}
|
||||
// autoHideDuration={6000}
|
||||
// onClose={() => setError(null)}
|
||||
// >
|
||||
// <Alert severity="error" onClose={() => setError(null)}>
|
||||
// {error}
|
||||
// </Alert>
|
||||
// </Snackbar>
|
||||
// </div>
|
||||
// );
|
||||
// });
|
255
src/pages/EditMediaPage/index.tsx
Normal file
255
src/pages/EditMediaPage/index.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
Alert,
|
||||
Snackbar,
|
||||
} from "@mui/material";
|
||||
import { Save, ArrowLeft } from "lucide-react";
|
||||
import { authInstance, mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||
import { MediaViewer } from "@widgets";
|
||||
|
||||
export const EditMediaPage = observer(() => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [newFile, setNewFile] = useState<File | null>(null);
|
||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false); // State for the upload dialog
|
||||
|
||||
const media = id ? mediaStore.media.find((m) => m.id === id) : null;
|
||||
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
|
||||
const [mediaFilename, setMediaFilename] = useState(media?.filename ?? "");
|
||||
const [mediaType, setMediaType] = useState(media?.media_type ?? 1);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
mediaStore.getOneMedia(id);
|
||||
}
|
||||
console.log(newFile);
|
||||
console.log(uploadDialogOpen);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (media) {
|
||||
setMediaName(media.media_name);
|
||||
setMediaFilename(media.filename);
|
||||
setMediaType(media.media_type);
|
||||
}
|
||||
}, [media]);
|
||||
|
||||
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
// setIsDragging(false);
|
||||
|
||||
// const files = Array.from(e.dataTransfer.files);
|
||||
// if (files.length > 0) {
|
||||
// setNewFile(files[0]);
|
||||
// setMediaFilename(files[0].name);
|
||||
// setUploadDialogOpen(true); // Open dialog on file drop
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// setIsDragging(true);
|
||||
// };
|
||||
|
||||
// const handleDragLeave = () => {
|
||||
// setIsDragging(false);
|
||||
// };
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
setNewFile(files[0]);
|
||||
setMediaFilename(files[0].name);
|
||||
setUploadDialogOpen(true); // Open dialog on file selection
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await authInstance.patch(`/media/${id}`, {
|
||||
media_name: mediaName,
|
||||
filename: mediaFilename,
|
||||
type: mediaType,
|
||||
});
|
||||
|
||||
// If a new file was selected, the actual file upload will happen
|
||||
// via the UploadMediaDialog. We just need to make sure the metadata
|
||||
// is updated correctly before or after.
|
||||
// Since the dialog handles the actual upload, we don't call updateMediaFile here.
|
||||
|
||||
setSuccess(true);
|
||||
handleUploadSuccess();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to save media");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
// After successful upload in the dialog, refresh media data if needed
|
||||
if (id) {
|
||||
mediaStore.getOneMedia(id);
|
||||
}
|
||||
setNewFile(null); // Clear the new file state after successful upload
|
||||
setUploadDialogOpen(false);
|
||||
setSuccess(true);
|
||||
};
|
||||
|
||||
if (!media && id) {
|
||||
// Only show loading if an ID is present and media is not yet loaded
|
||||
return (
|
||||
<Box className="flex justify-center items-center h-screen">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="p-6 max-w-4xl mx-auto">
|
||||
<Box className="flex items-center gap-4 mb-6">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ArrowLeft size={20} />}
|
||||
onClick={() => navigate("/media")}
|
||||
>
|
||||
Назад
|
||||
</Button>
|
||||
<Typography variant="h5">Редактирование медиа</Typography>
|
||||
</Box>
|
||||
|
||||
<Paper className="p-6">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
accept="image/*,video/*,.glb,.gltf"
|
||||
/>
|
||||
<Box className="flex flex-col gap-6">
|
||||
<Box className="flex gap-4">
|
||||
<TextField
|
||||
fullWidth
|
||||
value={mediaName}
|
||||
onChange={(e) => setMediaName(e.target.value)}
|
||||
label="Название медиа"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={mediaFilename}
|
||||
onChange={(e) => setMediaFilename(e.target.value)}
|
||||
label="Название файла"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<FormControl fullWidth sx={{ width: "50%" }}>
|
||||
<InputLabel>Тип медиа</InputLabel>
|
||||
<Select
|
||||
value={mediaType}
|
||||
label="Тип медиа"
|
||||
onChange={(e) => setMediaType(Number(e.target.value))}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{Object.entries(MEDIA_TYPE_LABELS).map(([type, label]) => (
|
||||
<MenuItem key={type} value={Number(type)}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Box className="flex gap-6">
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
flex: 1,
|
||||
p: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 400,
|
||||
}}
|
||||
>
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: id || "",
|
||||
media_type: mediaType,
|
||||
filename: mediaFilename,
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Box className="flex flex-col gap-4 self-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || (!mediaName && !mediaFilename)}
|
||||
>
|
||||
{isLoading ? <CircularProgress size={20} /> : "Сохранить"}
|
||||
</Button>
|
||||
{/* Only show "Replace file" button if no new file is currently selected */}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Typography color="error" className="mt-2">
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
{success && (
|
||||
<Typography color="success.main" className="mt-2">
|
||||
Медиа успешно сохранено
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Snackbar
|
||||
open={!!error}
|
||||
autoHideDuration={6000}
|
||||
onClose={() => setError(null)}
|
||||
>
|
||||
<Alert severity="error" onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
<Snackbar
|
||||
open={success}
|
||||
autoHideDuration={2000}
|
||||
onClose={() => setSuccess(false)}
|
||||
>
|
||||
<Alert severity="success" onClose={() => setSuccess(false)}>
|
||||
Медиа успешно сохранено
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
});
|
1594
src/pages/MapPage/index.tsx
Normal file
1594
src/pages/MapPage/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
76
src/pages/MediaListPage/index.tsx
Normal file
76
src/pages/MediaListPage/index.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Button, TableBody } from "@mui/material";
|
||||
import { TableRow, TableCell } from "@mui/material";
|
||||
import { Table, TableHead } from "@mui/material";
|
||||
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Pencil, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const rows = (media: any[]) => {
|
||||
return media.map((row) => ({
|
||||
id: row.id,
|
||||
media_name: row.media_name,
|
||||
media_type:
|
||||
MEDIA_TYPE_LABELS[row.media_type as keyof typeof MEDIA_TYPE_LABELS],
|
||||
}));
|
||||
};
|
||||
|
||||
export const MediaListPage = observer(() => {
|
||||
const { media, getMedia, deleteMedia } = mediaStore;
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
getMedia();
|
||||
}, []);
|
||||
|
||||
const currentRows = rows(media);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-end p-3 gap-5">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/sight/create")}
|
||||
>
|
||||
Создать
|
||||
</Button>
|
||||
</div>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center">Название</TableCell>
|
||||
<TableCell align="center">Тип</TableCell>
|
||||
<TableCell align="center">Действия</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{currentRows.map((row) => (
|
||||
<TableRow
|
||||
key={row.media_name}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell align="center">{row.media_name}</TableCell>
|
||||
<TableCell align="center">{row.media_type}</TableCell>
|
||||
<TableCell align="center">
|
||||
<div className="flex gap-7 justify-center">
|
||||
<button onClick={() => navigate(`/media/${row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
|
||||
<button onClick={() => navigate(`/media/${row.id}/edit`)}>
|
||||
<Pencil size={20} className="text-blue-500" />
|
||||
</button>
|
||||
|
||||
<button onClick={() => deleteMedia(row.id)}>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
});
|
44
src/pages/PreviewMediaPage/index.tsx
Normal file
44
src/pages/PreviewMediaPage/index.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { mediaStore } from "@shared";
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { MediaViewer } from "../../widgets/MediaViewer/index";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Download } from "lucide-react";
|
||||
import { Button } from "@mui/material";
|
||||
|
||||
export const PreviewMediaPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { oneMedia, getOneMedia } = mediaStore;
|
||||
|
||||
useEffect(() => {
|
||||
getOneMedia(id!);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full h-[80vh] flex flex-col justify-center items-center gap-4">
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<MediaViewer className="w-full h-full" media={oneMedia!} />
|
||||
</div>
|
||||
|
||||
{oneMedia && (
|
||||
<div className="flex flex-col items-center gap-4 bg-[#998879] p-4 rounded-md">
|
||||
<p className="text-white text-center">
|
||||
Чтобы скачать файл, нажмите на кнопку ниже
|
||||
</p>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Download size={16} />}
|
||||
component="a"
|
||||
href={`${
|
||||
import.meta.env.VITE_KRBL_MEDIA
|
||||
}${id}/download?token=${localStorage.getItem("token")}`}
|
||||
target="_blank"
|
||||
>
|
||||
Скачать
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -4,3 +4,8 @@ export * from "./LoginPage";
|
||||
export * from "./DevicesPage";
|
||||
export * from "./SightPage";
|
||||
export * from "./CreateSightPage";
|
||||
export * from "./MapPage";
|
||||
export * from "./MediaListPage";
|
||||
export * from "./PreviewMediaPage";
|
||||
export * from "./EditMediaPage";
|
||||
// export * from "./CreateMediaPage";
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { authStore } from "@shared";
|
||||
import { Power, LucideIcon, Building2, MonitorSmartphone } from "lucide-react";
|
||||
import {
|
||||
Power,
|
||||
LucideIcon,
|
||||
Building2,
|
||||
MonitorSmartphone,
|
||||
Map,
|
||||
BookImage,
|
||||
Newspaper,
|
||||
} from "lucide-react";
|
||||
export const DRAWER_WIDTH = 300;
|
||||
|
||||
interface NavigationItem {
|
||||
@ -21,12 +29,30 @@ export const NAVIGATION_ITEMS: {
|
||||
icon: Building2,
|
||||
path: "/sight",
|
||||
},
|
||||
{
|
||||
id: "map",
|
||||
label: "Карта",
|
||||
icon: Map,
|
||||
path: "/map",
|
||||
},
|
||||
{
|
||||
id: "devices",
|
||||
label: "Устройства",
|
||||
icon: MonitorSmartphone,
|
||||
path: "/devices",
|
||||
},
|
||||
{
|
||||
id: "media",
|
||||
label: "Медиа",
|
||||
icon: BookImage,
|
||||
path: "/media",
|
||||
},
|
||||
{
|
||||
id: "articles",
|
||||
label: "Статьи",
|
||||
icon: Newspaper,
|
||||
path: "/articles",
|
||||
},
|
||||
],
|
||||
secondary: [
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ type Media = {
|
||||
|
||||
class MediaStore {
|
||||
media: Media[] = [];
|
||||
|
||||
oneMedia: Media | null = null;
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
@ -22,6 +22,60 @@ class MediaStore {
|
||||
this.media = [...response.data];
|
||||
});
|
||||
};
|
||||
|
||||
deleteMedia = async (id: string) => {
|
||||
await authInstance.delete(`/media/${id}`);
|
||||
this.media = this.media.filter((media) => media.id !== id);
|
||||
};
|
||||
|
||||
getOneMedia = async (id: string) => {
|
||||
this.oneMedia = null;
|
||||
const response = await authInstance.get(`/media/${id}`);
|
||||
runInAction(() => {
|
||||
this.oneMedia = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
updateMedia = async (id: string, data: Partial<Media>) => {
|
||||
const response = await authInstance.patch(`/media/${id}`, data);
|
||||
runInAction(() => {
|
||||
// Update in media array
|
||||
const index = this.media.findIndex((m) => m.id === id);
|
||||
if (index !== -1) {
|
||||
this.media[index] = { ...this.media[index], ...response.data };
|
||||
}
|
||||
// Update oneMedia if it's the current media being viewed
|
||||
if (this.oneMedia?.id === id) {
|
||||
this.oneMedia = { ...this.oneMedia, ...response.data };
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
updateMediaFile = async (id: string, file: File, filename: string) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("filename", filename);
|
||||
|
||||
const response = await authInstance.patch(`/media/${id}/file`, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
// Update in media array
|
||||
const index = this.media.findIndex((m) => m.id === id);
|
||||
if (index !== -1) {
|
||||
this.media[index] = { ...this.media[index], ...response.data };
|
||||
}
|
||||
// Update oneMedia if it's the current media being viewed
|
||||
if (this.oneMedia?.id === id) {
|
||||
this.oneMedia = { ...this.oneMedia, ...response.data };
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
}
|
||||
|
||||
export const mediaStore = new MediaStore();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { API_URL, authInstance } from "@shared";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
type Vehicle = {
|
||||
export type Vehicle = {
|
||||
vehicle: {
|
||||
id: number;
|
||||
tail_number: number;
|
||||
|
@ -11,15 +11,25 @@ import {
|
||||
devicesStore,
|
||||
Modal,
|
||||
snapshotStore,
|
||||
vehicleStore,
|
||||
} from "@shared";
|
||||
vehicleStore, // Not directly used in this component's rendering logic anymore
|
||||
} from "@shared"; // Assuming @shared exports these
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Button, Checkbox, Typography } from "@mui/material"; // Import Typography for the modal message
|
||||
import { Button, Checkbox, Typography } from "@mui/material";
|
||||
import { Vehicle } from "@shared";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const formatDate = (dateString: string | undefined) => {
|
||||
export type ConnectedDevice = string;
|
||||
|
||||
interface Snapshot {
|
||||
ID: string; // Assuming ID is string based on usage
|
||||
Name: string;
|
||||
// Add other snapshot properties if needed
|
||||
}
|
||||
|
||||
// --- HELPER FUNCTIONS ---
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return "Нет данных";
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("ru-RU", {
|
||||
@ -32,244 +42,368 @@ const formatDate = (dateString: string | undefined) => {
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
} catch (error) {
|
||||
console.error("Error formatting date:", error);
|
||||
return "Некорректная дата";
|
||||
}
|
||||
};
|
||||
|
||||
type TableRowData = {
|
||||
tail_number: number;
|
||||
online: boolean;
|
||||
lastUpdate: string | null;
|
||||
gps: boolean;
|
||||
media: boolean;
|
||||
connection: boolean;
|
||||
device_uuid: string | null;
|
||||
};
|
||||
function createData(
|
||||
uuid: string,
|
||||
tail_number: number,
|
||||
online: boolean,
|
||||
lastUpdate: string,
|
||||
lastUpdate: string | null,
|
||||
gps: boolean,
|
||||
media: boolean,
|
||||
connection: boolean
|
||||
) {
|
||||
return { uuid, online, lastUpdate, gps, media, connection };
|
||||
connection: boolean,
|
||||
device_uuid: string | null
|
||||
): TableRowData {
|
||||
return {
|
||||
tail_number,
|
||||
online,
|
||||
lastUpdate,
|
||||
gps,
|
||||
media,
|
||||
connection,
|
||||
device_uuid,
|
||||
};
|
||||
}
|
||||
|
||||
// Keep the rows function as you provided it, without additional filters
|
||||
const rows = (vehicles: any[]) => {
|
||||
// This function transforms the raw device data (which includes vehicle and device_status)
|
||||
// into the format expected by the table. It now filters for devices that have a UUID.
|
||||
const transformDevicesToRows = (
|
||||
vehicles: Vehicle[]
|
||||
// devices: ConnectedDevice[]
|
||||
): TableRowData[] => {
|
||||
return vehicles.map((vehicle) => {
|
||||
const uuid = vehicle.vehicle.uuid;
|
||||
if (!uuid)
|
||||
return {
|
||||
tail_number: vehicle.vehicle.tail_number,
|
||||
online: false,
|
||||
lastUpdate: null,
|
||||
gps: false,
|
||||
media: false,
|
||||
connection: false,
|
||||
device_uuid: null,
|
||||
};
|
||||
return createData(
|
||||
vehicle?.vehicle?.tail_number ?? "1243000", // Using tail_number as UUID, as in your original code
|
||||
vehicle?.device_status?.online ?? false,
|
||||
vehicle?.device_status?.last_update,
|
||||
vehicle?.device_status?.gps_ok,
|
||||
vehicle?.device_status?.media_service_ok,
|
||||
vehicle?.device_status?.is_connected
|
||||
vehicle.vehicle.tail_number,
|
||||
vehicle.device_status?.online ?? false,
|
||||
vehicle.device_status?.last_update ?? null,
|
||||
vehicle.device_status?.gps_ok ?? false,
|
||||
vehicle.device_status?.media_service_ok ?? false,
|
||||
vehicle.device_status?.is_connected ?? false,
|
||||
uuid
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const DevicesTable = observer(() => {
|
||||
const {
|
||||
devices,
|
||||
getDevices,
|
||||
// uuid, // This 'uuid' from devicesStore refers to a *single* selected device, not for batch actions.
|
||||
setSelectedDevice, // Useful for individual device actions like 'Reload Status'
|
||||
setSelectedDevice,
|
||||
sendSnapshotModalOpen,
|
||||
toggleSendSnapshotModal,
|
||||
} = devicesStore;
|
||||
const { snapshots, getSnapshots } = snapshotStore;
|
||||
const { getVehicles } = vehicleStore;
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||
|
||||
// Get the current list of rows displayed in the table
|
||||
const currentRows = rows(devices);
|
||||
const { snapshots, getSnapshots } = snapshotStore;
|
||||
const { getVehicles, vehicles } = vehicleStore; // Removed as devicesStore.devices should be the source of truth
|
||||
const { devices } = devicesStore;
|
||||
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
||||
|
||||
// Transform the raw devices data into rows suitable for the table
|
||||
// This will also filter out devices without a UUID, as those cannot be acted upon.
|
||||
const currentTableRows = transformDevicesToRows(
|
||||
vehicles as Vehicle[]
|
||||
// devices as ConnectedDevice[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
await getVehicles();
|
||||
await getDevices();
|
||||
await getVehicles(); // Not strictly needed if devicesStore.devices is populated correctly by getDevices
|
||||
await getDevices(); // This should fetch the combined vehicle/device_status data
|
||||
await getSnapshots();
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [getDevices, getSnapshots]); // Added dependencies
|
||||
|
||||
// Determine if all visible devices are selected
|
||||
const isAllSelected =
|
||||
currentRows.length > 0 && selectedDevices.length === currentRows.length;
|
||||
currentTableRows.length > 0 &&
|
||||
selectedDeviceUuids.length === currentTableRows.length;
|
||||
|
||||
const handleSelectAllDevices = () => {
|
||||
if (isAllSelected) {
|
||||
// If all are currently selected, deselect all
|
||||
setSelectedDevices([]);
|
||||
setSelectedDeviceUuids([]);
|
||||
} else {
|
||||
// Otherwise, select all device UUIDs from the current rows
|
||||
setSelectedDevices(currentRows.map((row) => row.uuid));
|
||||
// Select all device UUIDs from the *currently visible and selectable* rows
|
||||
setSelectedDeviceUuids(
|
||||
currentTableRows.map((row) => row.device_uuid ?? "")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectDevice = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const deviceUuid = event.target.value;
|
||||
const handleSelectDevice = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
deviceUuid: string
|
||||
) => {
|
||||
if (event.target.checked) {
|
||||
setSelectedDevices((prevSelected) => [...prevSelected, deviceUuid]);
|
||||
setSelectedDeviceUuids((prevSelected) => [...prevSelected, deviceUuid]);
|
||||
} else {
|
||||
setSelectedDevices((prevSelected) =>
|
||||
setSelectedDeviceUuids((prevSelected) =>
|
||||
prevSelected.filter((uuid) => uuid !== deviceUuid)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// This function now opens the modal for selected devices
|
||||
const handleOpenSendSnapshotModal = () => {
|
||||
if (selectedDevices.length > 0) {
|
||||
if (selectedDeviceUuids.length > 0) {
|
||||
toggleSendSnapshotModal();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReloadStatus = async (uuid: string) => {
|
||||
setSelectedDevice(uuid); // Set the active device in store for context if needed
|
||||
await authInstance.post(`/devices/${uuid}/request-status`);
|
||||
await getDevices(); // Refresh devices after status request
|
||||
setSelectedDevice(uuid); // Sets the device in the store, useful for context elsewhere
|
||||
try {
|
||||
await authInstance.post(`/devices/${uuid}/request-status`);
|
||||
await getDevices(); // Refresh devices to show updated status
|
||||
} catch (error) {
|
||||
console.error(`Error requesting status for device ${uuid}:`, error);
|
||||
// Optionally: show a user-facing error message
|
||||
}
|
||||
};
|
||||
|
||||
// This function now handles sending snapshots to ALL selected devices
|
||||
const handleSendSnapshotAction = async (snapshotId: string) => {
|
||||
if (selectedDeviceUuids.length === 0) return;
|
||||
|
||||
try {
|
||||
for (const deviceUuid of selectedDevices) {
|
||||
// Create an array of promises for all snapshot requests
|
||||
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
||||
console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`);
|
||||
// Ensure you are using the correct API endpoint for force-snapshot
|
||||
await authInstance.post(`/devices/${deviceUuid}/force-snapshot`, {
|
||||
return authInstance.post(`/devices/${deviceUuid}/force-snapshot`, {
|
||||
snapshot_id: snapshotId,
|
||||
});
|
||||
}
|
||||
// After all requests are sent
|
||||
await getDevices(); // Refresh the device list to show updated status
|
||||
setSelectedDevices([]); // Clear the selection
|
||||
});
|
||||
|
||||
// Wait for all promises to settle (either resolve or reject)
|
||||
await Promise.allSettled(snapshotPromises);
|
||||
|
||||
// After all requests are attempted
|
||||
await getDevices(); // Refresh the device list
|
||||
setSelectedDeviceUuids([]); // Clear the selection
|
||||
toggleSendSnapshotModal(); // Close the modal
|
||||
} catch (error) {
|
||||
console.error("Error sending snapshots:", error);
|
||||
// You might want to show an error notification to the user here
|
||||
// This catch block might not be hit if Promise.allSettled is used,
|
||||
// as it doesn't reject on individual promise failures.
|
||||
// Individual errors should be handled if needed within the .map or by checking results.
|
||||
console.error("Error in snapshot sending process:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<div className="flex justify-end p-3 gap-3">
|
||||
{" "}
|
||||
{/* Changed gap to 3 for slightly less space */}
|
||||
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
||||
<div className="flex justify-end p-3 gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
variant="outlined" // Changed to outlined for distinction
|
||||
onClick={handleSelectAllDevices}
|
||||
size="small"
|
||||
>
|
||||
{isAllSelected ? "Снять выбор со всех" : "Выбрать все"}
|
||||
{isAllSelected ? "Снять выбор" : "Выбрать все"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={selectedDevices.length === 0}
|
||||
onClick={handleOpenSendSnapshotModal} // Call the new handler
|
||||
disabled={selectedDeviceUuids.length === 0}
|
||||
onClick={handleOpenSendSnapshotModal}
|
||||
size="small"
|
||||
>
|
||||
Отправить снапшот ({selectedDevices.length})
|
||||
Отправить снапшот ({selectedDeviceUuids.length})
|
||||
</Button>
|
||||
</div>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<Table sx={{ minWidth: 650 }} aria-label="devices table" size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="center" padding="checkbox">
|
||||
{" "}
|
||||
{/* Added padding="checkbox" */}
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
indeterminate={
|
||||
selectedDeviceUuids.length > 0 &&
|
||||
selectedDeviceUuids.length < currentTableRows.length
|
||||
}
|
||||
checked={isAllSelected}
|
||||
onChange={handleSelectAllDevices}
|
||||
inputProps={{ "aria-label": "select all devices" }}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">Бортовой номер</TableCell>
|
||||
<TableCell align="center">Борт. номер</TableCell>
|
||||
<TableCell align="center">Онлайн</TableCell>
|
||||
<TableCell align="center">Последнее обновление</TableCell>
|
||||
<TableCell align="center">ГПС</TableCell>
|
||||
<TableCell align="center">Медиа-данные</TableCell>
|
||||
<TableCell align="center">Подключение</TableCell>
|
||||
<TableCell align="center">Перезапросить</TableCell>
|
||||
<TableCell align="center">Обновлено</TableCell>
|
||||
<TableCell align="center">GPS</TableCell>
|
||||
<TableCell align="center">Медиа</TableCell>
|
||||
<TableCell align="center">Связь</TableCell>
|
||||
<TableCell align="center">Действия</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{currentRows.map((row) => (
|
||||
{currentTableRows.map((row) => (
|
||||
<TableRow
|
||||
key={row.uuid} // Use row.uuid as key for consistent rendering
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
key={row.tail_number}
|
||||
hover
|
||||
role="checkbox"
|
||||
aria-checked={selectedDeviceUuids.includes(
|
||||
row.device_uuid ?? ""
|
||||
)}
|
||||
selected={selectedDeviceUuids.includes(row.device_uuid ?? "")}
|
||||
onClick={(event) => {
|
||||
// Allow clicking row to toggle checkbox, if not clicking on button
|
||||
if (
|
||||
(event.target as HTMLElement).closest("button") === null &&
|
||||
(event.target as HTMLElement).closest(
|
||||
'input[type="checkbox"]'
|
||||
) === null
|
||||
) {
|
||||
handleSelectDevice(
|
||||
{
|
||||
target: {
|
||||
checked: !selectedDeviceUuids.includes(
|
||||
row.device_uuid ?? ""
|
||||
),
|
||||
},
|
||||
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
|
||||
row.device_uuid ?? ""
|
||||
);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:last-child td, &:last-child th": { border: 0 },
|
||||
}}
|
||||
>
|
||||
<TableCell align="center" padding="checkbox">
|
||||
{" "}
|
||||
{/* Added padding="checkbox" */}
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
className="h-full"
|
||||
onChange={handleSelectDevice}
|
||||
value={row.uuid}
|
||||
checked={selectedDevices.includes(row.uuid)} // THIS IS THE KEY CHANGE
|
||||
checked={selectedDeviceUuids.includes(
|
||||
row.device_uuid ?? ""
|
||||
)}
|
||||
onChange={(event) =>
|
||||
handleSelectDevice(event, row.device_uuid ?? "")
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center" component="th" scope="row">
|
||||
{row.uuid}
|
||||
<TableCell
|
||||
align="center"
|
||||
component="th"
|
||||
scope="row"
|
||||
id={`device-label-${row.device_uuid}`}
|
||||
>
|
||||
{row.tail_number}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.online ? (
|
||||
<Check className="m-auto text-green-500" />
|
||||
) : (
|
||||
<X className="m-auto text-red-500" />
|
||||
)}
|
||||
<div className="flex items-center justify-center">
|
||||
{row.online ? (
|
||||
<Check size={18} className="text-green-600" />
|
||||
) : (
|
||||
<X size={18} className="text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{formatDate(row.lastUpdate)}
|
||||
<div className="flex items-center justify-center">
|
||||
{formatDate(row.lastUpdate)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.gps ? (
|
||||
<Check className="m-auto text-green-500" />
|
||||
) : (
|
||||
<X className="m-auto text-red-500" />
|
||||
)}
|
||||
<div className="flex items-center justify-center">
|
||||
{row.gps ? (
|
||||
<Check size={18} className="text-green-600" />
|
||||
) : (
|
||||
<X size={18} className="text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.media ? (
|
||||
<Check className="m-auto text-green-500" />
|
||||
) : (
|
||||
<X className="m-auto text-red-500" />
|
||||
)}
|
||||
<div className="flex items-center justify-center">
|
||||
{row.media ? (
|
||||
<Check size={18} className="text-green-600" />
|
||||
) : (
|
||||
<X size={18} className="text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{row.connection ? (
|
||||
<Check className="m-auto text-green-500" />
|
||||
) : (
|
||||
<X className="m-auto text-red-500" />
|
||||
)}
|
||||
<div className="flex items-center justify-center">
|
||||
{row.connection ? (
|
||||
<Check size={18} className="text-green-600" />
|
||||
) : (
|
||||
<X size={18} className="text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<button onClick={() => handleReloadStatus(row.uuid)}>
|
||||
<RotateCcw className="m-auto" />
|
||||
</button>
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
if (
|
||||
row.device_uuid &&
|
||||
devices.find((device) => device === row.device_uuid)
|
||||
) {
|
||||
await handleReloadStatus(row.device_uuid);
|
||||
await getDevices();
|
||||
toast.success("Статус устройства обновлен");
|
||||
} else {
|
||||
toast.error("Нет связи с устройством");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Ошибка сервера");
|
||||
}
|
||||
}}
|
||||
title="Перезапросить статус"
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{currentTableRows.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} align="center">
|
||||
Нет устройств для отображения.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Modal open={sendSnapshotModalOpen} onClose={toggleSendSnapshotModal}>
|
||||
<Typography variant="h6" component="p" sx={{ mb: 2 }}>
|
||||
Выбрать снапшот для{" "}
|
||||
<strong className="text-blue-600">{selectedDevices.length}</strong>{" "}
|
||||
устройств
|
||||
<Typography variant="h6" component="h2" sx={{ mb: 1 }}>
|
||||
Отправить снапшот
|
||||
</Typography>
|
||||
<div className="mt-5 flex flex-col gap-2 max-h-[300px] overflow-y-auto">
|
||||
{snapshots && snapshots.length > 0 ? (
|
||||
snapshots.map((snapshot) => (
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
Выбрано устройств:{" "}
|
||||
<strong className="text-blue-600">
|
||||
{selectedDeviceUuids.length}
|
||||
</strong>
|
||||
</Typography>
|
||||
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
|
||||
{snapshots && (snapshots as Snapshot[]).length > 0 ? ( // Cast snapshots
|
||||
(snapshots as Snapshot[]).map((snapshot) => (
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={() => handleSendSnapshotAction(snapshot.ID)}
|
||||
sx={{
|
||||
p: 1.5, // Adjust padding
|
||||
borderRadius: 2, // Adjust border radius
|
||||
backgroundColor: "white", // Ensure background is white
|
||||
"&:hover": {
|
||||
backgroundColor: "grey.100", // Light hover effect
|
||||
},
|
||||
}}
|
||||
key={snapshot.ID}
|
||||
sx={{ justifyContent: "flex-start" }}
|
||||
>
|
||||
{snapshot.Name}
|
||||
</Button>
|
||||
@ -280,6 +414,15 @@ export const DevicesTable = observer(() => {
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={toggleSendSnapshotModal}
|
||||
color="inherit"
|
||||
variant="outlined"
|
||||
sx={{ mt: 3 }}
|
||||
fullWidth
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
@ -9,7 +9,10 @@ export interface MediaData {
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export function MediaViewer({ media }: Readonly<{ media?: MediaData }>) {
|
||||
export function MediaViewer({
|
||||
media,
|
||||
className,
|
||||
}: Readonly<{ media?: MediaData; className?: string }>) {
|
||||
const token = localStorage.getItem("token");
|
||||
return (
|
||||
<Box
|
||||
@ -22,6 +25,7 @@ export function MediaViewer({ media }: Readonly<{ media?: MediaData }>) {
|
||||
justifyContent: "center",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{media?.media_type === 1 && (
|
||||
<img
|
||||
|
@ -1 +1 @@
|
||||
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"}
|
||||
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createmediapage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editmediapage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/medialistpage/index.tsx","./src/pages/previewmediapage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"}
|
106
yarn.lock
106
yarn.lock
@ -684,6 +684,11 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@petamoriken/float16@^3.4.7":
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.9.2.tgz#217a5d349f3655b8e286be447e0ed1eae063a78f"
|
||||
integrity sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==
|
||||
|
||||
"@photo-sphere-viewer/core@^5.13.2":
|
||||
version "5.13.2"
|
||||
resolved "https://registry.yarnpkg.com/@photo-sphere-viewer/core/-/core-5.13.2.tgz#518f27a2b7ca5a80068d8922183a9999a1b33ad1"
|
||||
@ -1093,6 +1098,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz"
|
||||
integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==
|
||||
|
||||
"@types/rbush@4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-4.0.0.tgz#b327bf54952e9c924ea6702c36904c2ce1d47f35"
|
||||
integrity sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==
|
||||
|
||||
"@types/react-dom@^19.1.2":
|
||||
version "19.1.5"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz"
|
||||
@ -1637,6 +1647,11 @@ dunder-proto@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
earcut@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/earcut/-/earcut-3.0.1.tgz#f60b3f671c5657cca9d3e131c5527c5dde00ef38"
|
||||
integrity sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==
|
||||
|
||||
easymde@^2.20.0:
|
||||
version "2.20.0"
|
||||
resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz"
|
||||
@ -1983,6 +1998,20 @@ gensync@^1.0.0-beta.2:
|
||||
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
|
||||
geotiff@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/geotiff/-/geotiff-2.1.3.tgz#993f40f2aa6aa65fb1e0451d86dd22ca8e66910c"
|
||||
integrity sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==
|
||||
dependencies:
|
||||
"@petamoriken/float16" "^3.4.7"
|
||||
lerc "^3.0.0"
|
||||
pako "^2.0.4"
|
||||
parse-headers "^2.0.2"
|
||||
quick-lru "^6.1.1"
|
||||
web-worker "^1.2.0"
|
||||
xml-utils "^1.0.2"
|
||||
zstddec "^0.1.0"
|
||||
|
||||
get-intrinsic@^1.2.6:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||
@ -2365,6 +2394,11 @@ keyv@^4.5.4:
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
lerc@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lerc/-/lerc-3.0.0.tgz#36f36fbd4ba46f0abf4833799fff2e7d6865f5cb"
|
||||
integrity sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==
|
||||
|
||||
levn@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz"
|
||||
@ -2923,6 +2957,17 @@ object-assign@^4.1.1:
|
||||
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
|
||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||
|
||||
ol@^10.5.0:
|
||||
version "10.5.0"
|
||||
resolved "https://registry.yarnpkg.com/ol/-/ol-10.5.0.tgz#5831dc55fe5eb5a09cb4193c9289c1b5a0a0f0ca"
|
||||
integrity sha512-nHFx8gkGmvYImsa7iKkwUnZidd5gn1XbMZd9GNOorvm9orjW9gQvT3Naw/MjIasVJ3cB9EJUdCGR2EFAulMHsQ==
|
||||
dependencies:
|
||||
"@types/rbush" "4.0.0"
|
||||
earcut "^3.0.0"
|
||||
geotiff "^2.1.3"
|
||||
pbf "4.0.1"
|
||||
rbush "^4.0.0"
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.4"
|
||||
resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz"
|
||||
@ -2949,6 +2994,11 @@ p-locate@^5.0.0:
|
||||
dependencies:
|
||||
p-limit "^3.0.2"
|
||||
|
||||
pako@^2.0.4:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
|
||||
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
|
||||
@ -2969,6 +3019,11 @@ parse-entities@^4.0.0:
|
||||
is-decimal "^2.0.0"
|
||||
is-hexadecimal "^2.0.0"
|
||||
|
||||
parse-headers@^2.0.2:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.6.tgz#7940f0abe5fe65df2dd25d4ce8800cb35b49d01c"
|
||||
integrity sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==
|
||||
|
||||
parse-json@^5.0.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz"
|
||||
@ -3014,6 +3069,13 @@ path@^0.12.7:
|
||||
process "^0.11.1"
|
||||
util "^0.10.3"
|
||||
|
||||
pbf@4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pbf/-/pbf-4.0.1.tgz#ad9015e022b235dcdbe05fc468a9acadf483f0d4"
|
||||
integrity sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==
|
||||
dependencies:
|
||||
resolve-protobuf-schema "^2.1.0"
|
||||
|
||||
picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
|
||||
@ -3088,6 +3150,11 @@ property-information@^7.0.0:
|
||||
resolved "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz"
|
||||
integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==
|
||||
|
||||
protocol-buffers-schema@^3.3.1:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03"
|
||||
integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
@ -3103,6 +3170,23 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
quick-lru@^6.1.1:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-6.1.2.tgz#e9a90524108629be35287d0b864e7ad6ceb3659e"
|
||||
integrity sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==
|
||||
|
||||
quickselect@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-3.0.0.tgz#a37fc953867d56f095a20ac71c6d27063d2de603"
|
||||
integrity sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==
|
||||
|
||||
rbush@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rbush/-/rbush-4.0.1.tgz#1f55afa64a978f71bf9e9a99bc14ff84f3cb0d6d"
|
||||
integrity sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==
|
||||
dependencies:
|
||||
quickselect "^3.0.0"
|
||||
|
||||
react-dom@^19.1.0:
|
||||
version "19.1.0"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz"
|
||||
@ -3254,6 +3338,13 @@ resolve-from@^4.0.0:
|
||||
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
resolve-protobuf-schema@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758"
|
||||
integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==
|
||||
dependencies:
|
||||
protocol-buffers-schema "^3.3.1"
|
||||
|
||||
resolve@^1.19.0:
|
||||
version "1.22.10"
|
||||
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz"
|
||||
@ -3696,6 +3787,11 @@ web-namespaces@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz"
|
||||
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
|
||||
|
||||
web-worker@^1.2.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.5.0.tgz#71b2b0fbcc4293e8f0aa4f6b8a3ffebff733dcc5"
|
||||
integrity sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==
|
||||
|
||||
webgl-constants@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/webgl-constants/-/webgl-constants-1.1.1.tgz#f9633ee87fea56647a60b9ce735cbdfb891c6855"
|
||||
@ -3718,6 +3814,11 @@ word-wrap@^1.2.5:
|
||||
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
xml-utils@^1.0.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/xml-utils/-/xml-utils-1.10.2.tgz#436b39ccc25a663ce367ea21abb717afdea5d6b1"
|
||||
integrity sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==
|
||||
|
||||
yallist@^3.0.2:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
|
||||
@ -3738,6 +3839,11 @@ yocto-queue@^0.1.0:
|
||||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zstddec@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/zstddec/-/zstddec-0.1.0.tgz#7050f3f0e0c3978562d0c566b3e5a427d2bad7ec"
|
||||
integrity sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==
|
||||
|
||||
zustand@^4.3.2:
|
||||
version "4.5.7"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55"
|
||||
|
Loading…
Reference in New Issue
Block a user