Add Map page with basic logic

This commit is contained in:
2025-06-02 18:02:02 +03:00
parent a8777a974a
commit 5814e65953
15 changed files with 2576 additions and 132 deletions

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

View 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

File diff suppressed because it is too large Load Diff

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

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

View File

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