feat: Refactor old code with delete modal and icons for buttons

This commit is contained in:
2025-06-04 20:19:06 +03:00
parent 078f051e8a
commit 89488d9921
27 changed files with 2070 additions and 476 deletions

View File

@ -1,7 +1,5 @@
import * as React from "react";
import { BrowserRouter } from "react-router-dom";
import { Router } from "./router";
import { CustomTheme } from "@shared";
import { ThemeProvider } from "@mui/material/styles";
@ -10,8 +8,7 @@ import { ToastContainer } from "react-toastify";
export const App: React.FC = () => (
<ThemeProvider theme={CustomTheme.Light}>
<ToastContainer />
<BrowserRouter>
<Router />
</BrowserRouter>
<Router />
</ThemeProvider>
);

View File

@ -9,37 +9,45 @@ import {
MediaListPage,
PreviewMediaPage,
EditMediaPage,
// CreateMediaPage,
} from "@pages";
import { authStore, createSightStore, editSightStore } from "@shared";
import { Layout } from "@widgets";
import { runInAction } from "mobx";
import { useEffect } from "react";
import React, { useEffect } from "react";
import { Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom";
import {
createBrowserRouter,
RouterProvider,
Navigate,
Outlet,
useLocation,
} from "react-router-dom";
const PublicRoute = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated } = authStore;
if (isAuthenticated) {
return <Navigate to="/sight" />;
return <Navigate to="/sight" replace />;
}
return children;
return <>{children}</>;
};
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated } = authStore;
const pathname = useLocation();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" />;
return <Navigate to="/login" replace />;
}
if (pathname.pathname === "/") {
return <Navigate to="/sight" />;
if (location.pathname === "/") {
return <Navigate to="/sight" replace />;
}
return children;
return <>{children}</>;
};
export const Router = () => {
const pathname = useLocation();
// Чтобы очистка сторов происходила при смене локации
const ClearStoresWrapper: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const location = useLocation();
useEffect(() => {
editSightStore.clearSightInfo();
@ -47,41 +55,46 @@ export const Router = () => {
runInAction(() => {
editSightStore.hasLoadedCommon = false;
});
}, [pathname]);
}, [location]);
return (
<Routes>
<Route
path="/login"
element={
<PublicRoute>
<LoginPage />
</PublicRoute>
}
/>
{/* Protected routes with layout */}
<Route
path="/"
element={
<ProtectedRoute>
<Layout>
<Outlet />
</Layout>
</ProtectedRoute>
}
>
<Route index element={<MainPage />} />
<Route path="sight" element={<SightPage />} />
<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>
);
return <>{children}</>;
};
const router = createBrowserRouter([
{
path: "/login",
element: (
<PublicRoute>
<LoginPage />
</PublicRoute>
),
},
{
path: "/",
element: (
<ProtectedRoute>
<Layout>
<ClearStoresWrapper>
<Outlet />
</ClearStoresWrapper>
</Layout>
</ProtectedRoute>
),
children: [
{ index: true, element: <MainPage /> },
{ path: "sight", element: <SightPage /> },
{ path: "sight/create", element: <CreateSightPage /> },
{ path: "sight/:id", element: <EditSightPage /> },
{ path: "devices", element: <DevicesPage /> },
{ path: "map", element: <MapPage /> },
{ path: "media", element: <MediaListPage /> },
{ path: "media/:id", element: <PreviewMediaPage /> },
{ path: "media/:id/edit", element: <EditMediaPage /> },
// { path: "media/create", element: <CreateMediaPage /> },
],
},
]);
export const Router = () => {
return <RouterProvider router={router} />;
};

View File

@ -1,6 +1,11 @@
import { Box, Tab, Tabs } from "@mui/material";
import { Box, Button, Tab, Tabs } from "@mui/material";
import { articlesStore, cityStore, languageStore } from "@shared";
import { CreateInformationTab, CreateLeftTab, CreateRightTab } from "@widgets";
import {
CreateInformationTab,
CreateLeftTab,
CreateRightTab,
LeaveAgree,
} from "@widgets";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
@ -11,6 +16,8 @@ function a11yProps(index: number) {
};
}
import { useBlocker } from "react-router";
export const CreateSightPage = observer(() => {
const [value, setValue] = useState(0);
const { getCities } = cityStore;
@ -19,6 +26,11 @@ export const CreateSightPage = observer(() => {
setValue(newValue);
};
let blocker = useBlocker(
({ currentLocation, nextLocation }) =>
true && currentLocation.pathname !== nextLocation.pathname
);
useEffect(() => {
const fetchData = async () => {
await getCities();
@ -34,6 +46,7 @@ export const CreateSightPage = observer(() => {
display: "flex",
flexDirection: "column",
minHeight: "100vh",
z: 10,
}}
>
<Box
@ -66,6 +79,8 @@ export const CreateSightPage = observer(() => {
<CreateLeftTab value={value} index={1} />
<CreateRightTab value={value} index={2} />
</div>
{blocker.state === "blocked" ? <LeaveAgree blocker={blocker} /> : null}
</Box>
);
});

View File

@ -34,6 +34,7 @@ export const EditMediaPage = observer(() => {
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
const [mediaFilename, setMediaFilename] = useState(media?.filename ?? "");
const [mediaType, setMediaType] = useState(media?.media_type ?? 1);
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>([]);
useEffect(() => {
if (id) {
@ -48,6 +49,18 @@ export const EditMediaPage = observer(() => {
setMediaName(media.media_name);
setMediaFilename(media.filename);
setMediaType(media.media_type);
// Set available media types based on current file extension
const extension = media.filename.split(".").pop()?.toLowerCase();
if (extension) {
if (["glb", "gltf"].includes(extension)) {
setAvailableMediaTypes([6]); // 3D model
} else if (["jpg", "jpeg", "png", "gif"].includes(extension)) {
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
} else if (["mp4", "webm", "mov"].includes(extension)) {
setAvailableMediaTypes([2]); // Video
}
}
}
}, [media]);
@ -76,8 +89,25 @@ export const EditMediaPage = observer(() => {
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
setNewFile(files[0]);
setMediaFilename(files[0].name);
const file = files[0];
setNewFile(file);
setMediaFilename(file.name);
// Determine media type based on file extension
const extension = file.name.split(".").pop()?.toLowerCase();
if (extension) {
if (["glb", "gltf"].includes(extension)) {
setAvailableMediaTypes([6]); // 3D model
setMediaType(6);
} else if (["jpg", "jpeg", "png", "gif"].includes(extension)) {
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
setMediaType(1); // Default to Photo
} else if (["mp4", "webm", "mov"].includes(extension)) {
setAvailableMediaTypes([2]); // Video
setMediaType(2);
}
}
setUploadDialogOpen(true); // Open dialog on file selection
}
};
@ -175,11 +205,21 @@ export const EditMediaPage = observer(() => {
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>
))}
{availableMediaTypes.length > 0
? availableMediaTypes.map((type) => (
<MenuItem key={type} value={type}>
{
MEDIA_TYPE_LABELS[
type as keyof typeof MEDIA_TYPE_LABELS
]
}
</MenuItem>
))
: Object.entries(MEDIA_TYPE_LABELS).map(([type, label]) => (
<MenuItem key={type} value={Number(type)}>
{label}
</MenuItem>
))}
</Select>
</FormControl>

View File

@ -1,5 +1,5 @@
import { Box, Tab, Tabs } from "@mui/material";
import { InformationTab, RightWidgetTab } from "@widgets";
import { InformationTab, LeaveAgree, RightWidgetTab } from "@widgets";
import { LeftWidgetTab } from "@widgets";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
@ -9,7 +9,7 @@ import {
editSightStore,
languageStore,
} from "@shared";
import { useParams } from "react-router-dom";
import { useBlocker, useParams } from "react-router-dom";
function a11yProps(index: number) {
return {
@ -26,6 +26,11 @@ export const EditSightPage = observer(() => {
const { id } = useParams();
const { getCities } = cityStore;
let blocker = useBlocker(
({ currentLocation, nextLocation }) =>
true && currentLocation.pathname !== nextLocation.pathname
);
const handleChange = (_: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
@ -82,6 +87,8 @@ export const EditSightPage = observer(() => {
<RightWidgetTab value={value} index={2} />
</div>
)}
{blocker.state === "blocked" ? <LeaveAgree blocker={blocker} /> : null}
</Box>
);
});

View File

@ -28,8 +28,8 @@ import { FeatureLike } from "ol/Feature";
// --- CONFIGURATION ---
export const mapConfig = {
center: [37.6173, 55.7558] as [number, number],
zoom: 10,
center: [30.311, 59.94] as [number, number],
zoom: 13,
};
// --- SVG ICONS ---
@ -1128,7 +1128,7 @@ const MapControls: React.FC<MapControlsProps> = ({
const controls = [
{
mode: "edit",
title: "Редакт.",
title: "Редактировать",
longTitle: "Редактирование",
icon: <EditIcon />,
action: () => mapService.activateEditMode(),
@ -1156,7 +1156,7 @@ const MapControls: React.FC<MapControlsProps> = ({
},
];
return (
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-20 flex flex-wrap justify-center p-2 bg-white/90 backdrop-blur-sm rounded-lg shadow-xl space-x-1 sm:space-x-2">
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-20 flex flex-nowrap justify-center p-2 bg-white/90 backdrop-blur-sm rounded-lg shadow-xl space-x-1 sm:space-x-2">
{controls.map((c) => (
<button
key={c.mode}

View File

@ -2,10 +2,11 @@ import { 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 { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { DeleteModal } from "@widgets";
const rows = (media: any[]) => {
return media.map((row) => ({
@ -24,7 +25,8 @@ export const MediaListPage = observer(() => {
}, []);
const currentRows = rows(media);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState<number | null>(null);
return (
<>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
@ -53,7 +55,12 @@ export const MediaListPage = observer(() => {
<Pencil size={20} className="text-blue-500" />
</button>
<button onClick={() => deleteMedia(row.id)}>
<button
onClick={() => {
setIsDeleteModalOpen(true);
setRowId(row.id);
}}
>
<Trash2 size={20} className="text-red-500" />
</button>
</div>
@ -62,6 +69,20 @@ export const MediaListPage = observer(() => {
))}
</TableBody>
</Table>
<DeleteModal
open={isDeleteModalOpen}
onDelete={async () => {
if (rowId) {
await deleteMedia(rowId.toString());
}
setIsDeleteModalOpen(false);
setRowId(null);
}}
onCancel={() => {
setIsDeleteModalOpen(false);
setRowId(null);
}}
/>
</>
);
});

View File

@ -7,3 +7,14 @@ export const MEDIA_TYPE_LABELS = {
5: "Панорама",
6: "3Д-модель",
};
export const MEDIA_TYPE_VALUES = {
photo: 1,
video: 2,
icon: 3,
thumbnail: 3,
watermark_lu: 4,
watermark_rd: 4,
panorama: 5,
model: 6,
};

View File

@ -1,4 +1,4 @@
import { articlesStore, authInstance, languageStore } from "@shared";
import { articlesStore, authInstance, Language, languageStore } from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import {
@ -17,7 +17,7 @@ import {
InputAdornment,
} from "@mui/material";
import { ImagePlus, Search } from "lucide-react";
import { ReactMarkdownComponent } from "@widgets";
import { MediaViewer, ReactMarkdownComponent } from "@widgets";
interface SelectArticleModalProps {
open: boolean;
@ -128,10 +128,12 @@ export const SelectArticleModal = observer(
height: "600px",
display: "flex",
flexDirection: "row",
alignItems: "center",
p: 2,
}}
>
<Paper className="w-[66%] flex flex-col" elevation={2}>
<Paper className="w-[66%] flex flex-col h-full" elevation={2}>
<TextField
fullWidth
placeholder="Поиск статей..."
@ -201,108 +203,86 @@ export const SelectArticleModal = observer(
)}
</List>
</Paper>
<Paper className="flex-1 flex flex-col" elevation={2}>
<Paper
elevation={3}
sx={{
width: "100%",
minWidth: 320,
maxWidth: 310,
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
padding: 0,
margin: "0px auto",
display: "flex",
flexDirection: "column",
}}
>
<Box
className="rounded-2xl overflow-hidden"
sx={{
width: "100%",
height: "100%",
background: "#877361",
borderColor: "grey.300",
height: 175,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "3px",
}}
>
{isLoading ? (
<Box
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
{articlesStore.articleMedia ? (
<MediaViewer
media={{
id: articlesStore.articleMedia.id,
media_type: articlesStore.articleMedia.media_type,
filename: articlesStore.articleMedia.filename,
}}
>
<Typography color="white">Загрузка...</Typography>
</Box>
/>
) : (
<>
{articlesStore.articleMedia && (
<Box sx={{ p: 2, backgroundColor: "rgba(0,0,0,0.1)" }}>
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
articlesStore.articleMedia.id
}/download?token=${token}`}
alt={articlesStore.articleMedia.filename}
style={{
maxWidth: "100%",
height: "auto",
maxHeight: "300px",
objectFit: "contain",
borderRadius: 8,
}}
/>
</Box>
)}
{!articlesStore.articleMedia && (
<Box
sx={{
width: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<ImagePlus size={48} color="white" />
</Box>
)}
<Box
sx={{
width: "100%",
minHeight: "70px",
background: "#877361",
display: "flex",
flexShrink: 0,
alignItems: "center",
borderBottom: "1px solid rgba(255,255,255,0.1)",
px: 2,
}}
>
<Typography variant="h6" color="white">
{articlesStore.articleData?.heading || "Выберите статью"}
</Typography>
</Box>
<Box
sx={{
px: 2,
flexGrow: 1,
overflowY: "auto",
backgroundColor: "#877361",
color: "white",
py: 1,
}}
>
{articlesStore.articleData?.body ? (
<ReactMarkdownComponent
value={articlesStore.articleData.body}
/>
) : (
<Typography
color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }}
>
Предпросмотр статьи появится здесь
</Typography>
)}
</Box>
</>
<ImagePlus size={48} color="white" />
)}
</Box>
<Box
sx={{
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
color: "white",
margin: "5px 0px 5px 0px",
display: "flex",
flexDirection: "column",
gap: 1,
padding: 1,
}}
>
<Typography
variant="h5"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
}}
>
{articlesStore.articleData?.heading || "Название cтатьи"}
</Typography>
</Box>
{articlesStore.articleData?.body && (
<Box
sx={{
padding: 1,
maxHeight: "200px",
overflowY: "scroll",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
}}
>
<ReactMarkdownComponent
value={articlesStore.articleData?.body || "Описание"}
/>
</Box>
)}
</Paper>
</DialogContent>
<DialogActions sx={{ p: 2 }}>

View File

@ -1,4 +1,4 @@
import { mediaStore } from "@shared";
import { Media, mediaStore } from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import {
@ -29,6 +29,7 @@ interface SelectMediaDialogProps {
}) => void; // Renamed from onSelectArticle
onSelectForSightMedia?: (mediaId: string) => void;
linkedMediaIds?: string[]; // Renamed from linkedArticleIds, assuming it refers to media already in use
mediaType?: number;
}
export const SelectMediaDialog = observer(
@ -38,12 +39,29 @@ export const SelectMediaDialog = observer(
onSelectMedia, // Renamed prop
onSelectForSightMedia,
linkedMediaIds = [], // Default to empty array if not provided, renamed
mediaType,
}: SelectMediaDialogProps) => {
const { media, getMedia } = mediaStore;
const [searchQuery, setSearchQuery] = useState("");
const [hoveredMediaId, setHoveredMediaId] = useState<string | null>(null);
const [currentHoveredMedia, setCurrentHoveredMedia] =
useState<Media | null>(null);
useEffect(() => {
if (hoveredMediaId) {
setCurrentHoveredMedia(
media.find((m) => m.id === hoveredMediaId) ?? null
);
}
}, [hoveredMediaId]);
const handleClose = () => {
setHoveredMediaId(null);
setCurrentHoveredMedia(null);
onClose();
setSearchQuery("");
};
// Fetch media on component mount
useEffect(() => {
getMedia();
}, [getMedia]);
@ -63,7 +81,7 @@ export const SelectMediaDialog = observer(
onSelectMedia(mediaItem);
}
}
onClose();
handleClose();
}
}
};
@ -74,19 +92,21 @@ export const SelectMediaDialog = observer(
};
}, [hoveredMediaId, onSelectMedia, onClose]); // Dependencies for keyboard listener
const filteredMedia = media
let filteredMedia = media
.filter((mediaItem) => !linkedMediaIds.includes(mediaItem.id)) // Use mediaItem to avoid name collision
.filter((mediaItem) =>
mediaItem.media_name.toLowerCase().includes(searchQuery.toLowerCase())
);
// Find the currently hovered media object for MediaViewer
const currentHoveredMedia = hoveredMediaId
? media.find((m) => m.id === hoveredMediaId)
: null;
if (mediaType) {
filteredMedia = filteredMedia.filter(
(mediaItem) => mediaItem.media_type === mediaType
);
console.log(filteredMedia);
}
return (
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
<Dialog open={open} onClose={handleClose} maxWidth="lg" fullWidth>
<DialogTitle>Выберите существующее медиа</DialogTitle>
<DialogContent
className="flex gap-4"
@ -125,14 +145,22 @@ export const SelectMediaDialog = observer(
} else if (onSelectMedia) {
onSelectMedia(mediaItem);
}
onClose();
handleClose();
}}
selected={hoveredMediaId === mediaItem.id}
sx={{
borderRadius: 1,
mb: 0.5,
"&:hover": {
backgroundColor: "action.hover",
},
"&.Mui-selected": {
backgroundColor: "primary.main",
color: "primary.contrastText",
"&:hover": {
backgroundColor: "primary.dark",
},
},
}}
>
<ListItemText primary={mediaItem.media_name} />
@ -149,7 +177,7 @@ export const SelectMediaDialog = observer(
)}
</List>
</Paper>
{currentHoveredMedia ? ( // Only render MediaViewer if currentHoveredMedia is found
{currentHoveredMedia !== null && hoveredMediaId !== null ? ( // Only render MediaViewer if currentHoveredMedia is found
<Paper className="w-[33%] h-[100%] flex justify-center items-center">
<MediaViewer
media={{
@ -167,8 +195,28 @@ export const SelectMediaDialog = observer(
</Paper>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Отмена</Button>
<DialogActions sx={{ p: 2 }}>
<Button onClick={handleClose}>Отмена</Button>
<Button
variant="contained"
onClick={() => {
if (hoveredMediaId) {
const mediaItem = media.find((m) => m.id === hoveredMediaId);
if (mediaItem) {
if (onSelectForSightMedia) {
onSelectForSightMedia(mediaItem.id);
} else if (onSelectMedia) {
onSelectMedia(mediaItem);
}
}
handleClose();
}
}}
disabled={hoveredMediaId === null}
>
Выбрать
</Button>
</DialogActions>
</Dialog>
);

View File

@ -1,7 +1,7 @@
import { makeAutoObservable, runInAction } from "mobx";
import { authInstance } from "@shared";
type Media = {
export type Media = {
id: string;
filename: string;
media_name: string;

View File

@ -0,0 +1,33 @@
import { Button } from "@mui/material";
export const DeleteModal = ({
onDelete,
onCancel,
open,
}: {
onDelete: () => void;
onCancel: () => void;
open: boolean;
}) => {
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-10000 bg-black/30 transition-all duration-300 ${
open ? "block" : "hidden"
}`}
>
<div className="bg-white p-4 w-100 rounded-lg flex flex-col gap-4 items-center">
<p className="text-black w-100 text-center">
Вы уверены, что хотите удалить этот элемент?
</p>
<div className="flex gap-4 justify-center">
<Button variant="contained" color="error" onClick={onDelete}>
Да
</Button>
<Button variant="outlined" onClick={onCancel}>
Нет
</Button>
</div>
</div>
</div>
);
};

View File

@ -1,6 +1,6 @@
import React, { useRef, useState, DragEvent, useEffect } from "react";
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
import { X, Info } from "lucide-react"; // Assuming lucide-react for icons
import { X, Info, MousePointer } from "lucide-react"; // Assuming lucide-react for icons
import { editSightStore } from "@shared";
interface ImageUploadCardProps {
@ -159,6 +159,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
<Button
variant="contained"
color="primary"
startIcon={<MousePointer color="white" size={18} />}
onClick={(e) => {
e.stopPropagation(); // Prevent `handleZoneClick` from firing
onSelectFileClick(); // This button might trigger a different modal

View File

@ -0,0 +1,21 @@
import { Button } from "@mui/material";
export const LeaveAgree = ({ blocker }: { blocker: any }) => {
return (
<div className="fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-10000 bg-black/30">
<div className="bg-white p-4 w-100 rounded-lg flex flex-col gap-4 items-center">
<p className="text-black w-100 text-center">
При выходе со страницы, несохраненные данные будут потеряны.
</p>
<div className="flex gap-4 justify-center">
<Button variant="contained" onClick={() => blocker.proceed()}>
Да
</Button>
<Button variant="outlined" onClick={() => blocker.reset()}>
Нет
</Button>
</div>
</div>
</div>
);
};

View File

@ -69,7 +69,7 @@ export const MediaAreaForSight = observer(
<Box className="w-full flex flex-col items-center justify-center border rounded-md p-4">
<div className="w-full flex flex-col items-center justify-center">
<div
className={`w-full h-40 flex flex-col justify-center items-center text-gray-400 border-dashed border-2 rounded-md border-gray-400 p-4 cursor-pointer hover:bg-gray-50 ${
className={`w-full h-40 flex text-center flex-col justify-center items-center text-gray-400 border-dashed border-2 rounded-md border-gray-400 p-4 cursor-pointer hover:bg-gray-50 ${
isDragging ? "bg-blue-100 border-blue-400" : ""
}`}
onDrop={handleDrop}

View File

@ -17,13 +17,11 @@ export function MediaViewer({
return (
<Box
sx={{
width: "80%",
width: "100%",
height: "100%",
maxWidth: "600px",
display: "flex",
flexGrow: 1,
justifyContent: "center",
margin: "0 auto",
}}
className={className}
>
@ -34,10 +32,9 @@ export function MediaViewer({
}/download?token=${token}`}
alt={media?.filename}
style={{
maxWidth: "100%",
height: "auto",
objectFit: "contain",
borderRadius: 8,
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
)}
@ -48,9 +45,7 @@ export function MediaViewer({
media?.id
}/download?token=${token}`}
style={{
margin: "auto 0",
height: "fit-content",
width: "fit-content",
height: "100%",
objectFit: "contain",
borderRadius: 30,
}}
@ -66,7 +61,7 @@ export function MediaViewer({
}/download?token=${token}`}
alt={media?.filename}
style={{
maxWidth: "100%",
width: "100%",
height: "100%",
objectFit: "contain",
borderRadius: 8,
@ -80,7 +75,7 @@ export function MediaViewer({
}/download?token=${token}`}
alt={media?.filename}
style={{
maxWidth: "100%",
width: "100%",
height: "100%",
objectFit: "contain",
borderRadius: 8,

View File

@ -18,8 +18,10 @@ import {
SightCommonInfo,
createSightStore,
UploadMediaDialog,
MEDIA_TYPE_VALUES,
} from "@shared";
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
import { Save } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
@ -331,6 +333,7 @@ export const CreateInformationTab = observer(
<Button
variant="contained"
color="success"
startIcon={<Save color="white" size={18} />}
onClick={async () => {
await createSight(language);
toast.success("Достопримечательность создана");
@ -369,6 +372,13 @@ export const CreateInformationTab = observer(
onSelectMedia={(media) => {
handleMediaSelect(media, activeMenuType ?? "thumbnail");
}}
mediaType={
activeMenuType
? MEDIA_TYPE_VALUES[
activeMenuType as keyof typeof MEDIA_TYPE_VALUES
]
: undefined
}
/>
<PreviewMediaDialog

View File

@ -16,8 +16,16 @@ import {
ReactMarkdownComponent,
ReactMarkdownEditor,
MediaViewer,
DeleteModal,
} from "@widgets";
import { Trash2, ImagePlus } from "lucide-react";
import {
Trash2,
ImagePlus,
Unlink,
MousePointer,
Plus,
Save,
} from "lucide-react";
import { useState, useCallback } from "react";
import { observer } from "mobx-react-lite";
import { toast } from "react-toastify";
@ -47,7 +55,7 @@ export const CreateLeftTab = observer(
useState(false);
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
// const handleMediaSelected = useCallback(() => {
// // При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА
// // сохраняя текущие heading и body.
@ -123,6 +131,7 @@ export const CreateLeftTab = observer(
color="primary"
size="small"
style={{ transition: "0" }}
startIcon={<Unlink size={18} />}
onClick={() => {
unlinkLeftArticle();
toast.success("Статья откреплена");
@ -136,10 +145,7 @@ export const CreateLeftTab = observer(
style={{ transition: "0" }}
startIcon={<Trash2 size={18} />}
size="small"
onClick={() => {
deleteLeftArticle(sight.left_article);
toast.success("Статья откреплена");
}}
onClick={() => setIsDeleteModalOpen(true)}
>
Удалить
</Button>
@ -150,6 +156,7 @@ export const CreateLeftTab = observer(
variant="contained"
color="primary"
size="small"
startIcon={<MousePointer color="white" size={18} />}
onClick={() => setIsSelectArticleDialogOpen(true)}
>
Выбрать статью
@ -158,6 +165,7 @@ export const CreateLeftTab = observer(
variant="contained"
color="primary"
size="small"
startIcon={<Plus color="white" size={18} />}
style={{ transition: "0" }}
onClick={createLeftArticle}
>
@ -301,6 +309,7 @@ export const CreateLeftTab = observer(
display: "flex",
flexDirection: "column",
gap: 1.5,
maxWidth: "320px",
}}
>
<Paper
@ -405,10 +414,11 @@ export const CreateLeftTab = observer(
<Button
variant="contained"
color="success"
startIcon={<Save color="white" size={18} />}
onClick={async () => {
try {
await createSight(language);
toast.success("Странца создана");
toast.success("Страница создана");
} catch (error) {
console.error(error);
}
@ -445,6 +455,14 @@ export const CreateLeftTab = observer(
onClose={handleCloseArticleDialog}
onSelectArticle={handleArticleSelect}
/>
<DeleteModal
open={isDeleteModalOpen}
onDelete={() => {
deleteLeftArticle(sight.left_article);
toast.success("Статья откреплена");
}}
onCancel={() => setIsDeleteModalOpen(false)}
/>
</TabPanel>
);
}

View File

@ -14,7 +14,8 @@ import {
SelectArticleModal,
TabPanel,
SelectMediaDialog, // Import
UploadMediaDialog, // Import
UploadMediaDialog,
Media, // Import
} from "@shared";
import {
LanguageSwitcher,
@ -22,12 +23,14 @@ import {
MediaAreaForSight, // Import
ReactMarkdownComponent,
ReactMarkdownEditor,
DeleteModal,
} from "@widgets";
import { ImagePlus, Plus, X } from "lucide-react"; // Import X
import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react"; // Import X
import { observer } from "mobx-react-lite";
import { useState, useEffect } from "react"; // Added useEffect
import { MediaViewer } from "../../MediaViewer/index";
import { toast } from "react-toastify";
import { authInstance } from "@shared";
type MediaItemShared = {
// Define if not already available from @shared
@ -65,14 +68,27 @@ export const CreateRightTab = observer(
null
);
const [type, setType] = useState<"article" | "media">("media");
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
useState(false);
const [mediaTarget, setMediaTarget] = useState<
"sightPreview" | "rightArticle" | null
>(null);
const [previewMedia, setPreviewMedia] = useState<Media | null>(null);
// Reset activeArticleIndex if language changes and index is out of bounds
useEffect(() => {
if (sight.preview_media) {
const fetchMedia = async () => {
const response = await authInstance.get(
`/media/${sight.preview_media}`
);
setPreviewMedia(response.data);
};
fetchMedia();
}
}, [sight.preview_media]);
useEffect(() => {
if (
activeArticleIndex !== null &&
@ -168,6 +184,7 @@ export const CreateRightTab = observer(
const handleMediaSelectedFromDialog = async (media: MediaItemShared) => {
setIsSelectMediaDialogOpen(false);
if (mediaTarget === "sightPreview") {
await linkPreviewMedia(media.id);
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
@ -176,6 +193,11 @@ export const CreateRightTab = observer(
setMediaTarget(null);
};
const handleUnlinkPreviewMedia = async () => {
await unlinkPreviewMedia();
setPreviewMedia(null);
};
const handleMediaUploaded = async (media: MediaItemShared) => {
// After UploadMediaDialog finishes
setUploadMediaOpen(false);
@ -273,12 +295,13 @@ export const CreateRightTab = observer(
{/* Main content area: Article Editor or Sight Media Preview */}
{type === "article" && currentRightArticle ? (
<Box className="w-[80%] border border-gray-300 rounded-2xl p-3 flex flex-col gap-2 overflow-hidden">
<Box className="w-[80%] border border-gray-300 p-3 flex flex-col gap-2 overflow-hidden">
<Box className="flex justify-end gap-2 mb-1 flex-shrink-0">
<Button
variant="outlined"
color="warning"
color="primary"
size="small"
startIcon={<Unlink color="white" size={18} />}
onClick={() => {
if (currentRightArticle) {
unlinkRightAritcle(currentRightArticle.id); // Corrected function name
@ -293,22 +316,9 @@ export const CreateRightTab = observer(
variant="contained"
color="error"
size="small"
startIcon={<Trash2 size={18} />}
onClick={async () => {
if (
currentRightArticle &&
window.confirm(
`Удалить статью "${currentRightArticle.heading}" окончательно?`
)
) {
try {
await deleteRightArticle(currentRightArticle.id);
setActiveArticleIndex(null);
setType("media");
toast.success("Статья удалена");
} catch {
toast.error("Не удалось удалить статью");
}
}
setIsDeleteModalOpen(true);
}}
>
Удалить
@ -373,28 +383,30 @@ export const CreateRightTab = observer(
) : type === "media" ? (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 relative flex justify-center items-center">
{type === "media" && (
<Box className="w-[80%] border border-gray-300 rounded-2xl relative">
{sight.preview_media && (
<Box className="w-[80%] border border-gray-300 rounded-2xl relative flex items-center justify-center">
{previewMedia && (
<>
<Box className="absolute top-4 right-4">
<Box className="absolute top-4 right-4 z-10">
<button
className="w-10 h-10 flex items-center justify-center"
onClick={unlinkPreviewMedia}
className="w-10 h-10 flex items-center justify-center z-10"
onClick={handleUnlinkPreviewMedia}
>
<X size={20} color="red" />
</button>
</Box>
<MediaViewer
media={{
id: sight.preview_media || "",
media_type: 1,
filename: sight.preview_media || "",
}}
/>
<Box sx={{}}>
<MediaViewer
media={{
id: previewMedia.id || "",
media_type: previewMedia.media_type,
filename: previewMedia.filename || "",
}}
/>
</Box>
</>
)}
{!sight.preview_media && (
{!previewMedia && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
@ -505,33 +517,6 @@ export const CreateRightTab = observer(
</Box>
</Paper>
)}
{/* Optional: Preview for sight.preview_media when type === "media" */}
{type === "media" && sight.preview_media && (
<Paper
className="flex-1 flex flex-col rounded-2xl"
elevation={2}
sx={{ height: "75vh", overflow: "hidden" }}
>
<Box
sx={{
width: "100%",
height: "100%",
background: "#877361",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<MediaViewer
media={{
id: sight.preview_media,
filename: sight.preview_media,
media_type: 1,
}}
/>
</Box>
</Paper>
)}
</Box>
</Box>
@ -539,17 +524,15 @@ export const CreateRightTab = observer(
<Box
sx={{
position: "absolute",
bottom: 0,
bottom: "-20px",
left: 0, // ensure it spans from left
right: 0,
padding: 2,
backgroundColor: "background.paper",
borderTop: "1px solid", // Add a subtle top border
borderColor: "divider", // Use theme's divider color
width: "100%",
display: "flex",
justifyContent: "flex-end",
boxShadow: "0 -2px 5px rgba(0,0,0,0.1)", // Optional shadow
}}
>
<Button
@ -557,8 +540,9 @@ export const CreateRightTab = observer(
color="success"
onClick={handleSave}
size="large"
startIcon={<Save color="white" size={18} />}
>
Сохранить достопримечательность
Сохранить
</Button>
</Box>
</Box>
@ -588,6 +572,20 @@ export const CreateRightTab = observer(
}}
onSelectMedia={handleMediaSelectedFromDialog}
/>
<DeleteModal
open={isDeleteModalOpen}
onDelete={async () => {
try {
await deleteRightArticle(currentRightArticle?.id || 0);
setActiveArticleIndex(null);
setType("media");
toast.success("Статья удалена");
} catch {
toast.error("Не удалось удалить статью");
}
}}
onCancel={() => setIsDeleteModalOpen(false)}
/>
</TabPanel>
);
}

View File

@ -18,8 +18,10 @@ import {
SightLanguageInfo,
SightCommonInfo,
UploadMediaDialog,
MEDIA_TYPE_VALUES,
} from "@shared";
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
import { Save } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
@ -335,6 +337,7 @@ export const InformationTab = observer(
<Button
variant="contained"
color="success"
startIcon={<Save color="white" size={18} />}
onClick={async () => {
await updateSight();
toast.success("Достопримечательность сохранена");
@ -371,6 +374,13 @@ export const InformationTab = observer(
setActiveMenuType(null);
}}
onSelectMedia={handleMediaSelect}
mediaType={
activeMenuType
? MEDIA_TYPE_VALUES[
activeMenuType as keyof typeof MEDIA_TYPE_VALUES
]
: undefined
}
/>
<UploadMediaDialog

View File

@ -8,6 +8,7 @@ import {
editSightStore,
SelectArticleModal,
UploadMediaDialog,
Language,
} from "@shared";
import {
LanguageSwitcher,
@ -15,8 +16,16 @@ import {
ReactMarkdownEditor,
MediaArea,
MediaViewer,
DeleteModal,
} from "@widgets";
import { Trash2, ImagePlus } from "lucide-react";
import {
Trash2,
ImagePlus,
Unlink,
Plus,
MousePointer,
Save,
} from "lucide-react";
import { useState, useCallback } from "react";
import { observer } from "mobx-react-lite";
import { toast } from "react-toastify";
@ -40,12 +49,18 @@ export const LeftWidgetTab = observer(
const { language } = languageStore;
const data = sight[language];
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
useState(false);
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
useState(false);
const handleDeleteLeftArticle = useCallback(() => {
deleteLeftArticle(sight.common.left_article);
setIsDeleteModalOpen(false);
}, [deleteLeftArticle, sight.common.left_article]);
const handleMediaSelected = useCallback(
async (media: {
id: string;
@ -130,6 +145,7 @@ export const LeftWidgetTab = observer(
color="primary"
size="small"
style={{ transition: "0" }}
startIcon={<Unlink size={18} />}
onClick={() => {
unlinkLeftArticle();
toast.success("Статья откреплена");
@ -144,8 +160,7 @@ export const LeftWidgetTab = observer(
startIcon={<Trash2 size={18} />}
size="small"
onClick={() => {
deleteLeftArticle(sight.common.left_article);
toast.success("Статья откреплена");
setIsDeleteModalOpen(true);
}}
>
Удалить
@ -157,6 +172,7 @@ export const LeftWidgetTab = observer(
variant="contained"
color="primary"
size="small"
startIcon={<MousePointer color="white" size={18} />}
onClick={() => setIsSelectArticleDialogOpen(true)}
>
Выбрать статью
@ -166,6 +182,7 @@ export const LeftWidgetTab = observer(
color="primary"
size="small"
style={{ transition: "0" }}
startIcon={<Plus color="white" size={18} />}
onClick={() => {
createLeftArticle();
toast.success("Статья создана");
@ -234,7 +251,8 @@ export const LeftWidgetTab = observer(
flex: 1,
display: "flex",
flexDirection: "column",
gap: 1.5,
maxWidth: "320px",
gap: 0.5,
}}
>
<Paper
@ -242,10 +260,9 @@ export const LeftWidgetTab = observer(
sx={{
width: "100%",
minWidth: 320,
maxWidth: 400,
height: "auto",
minHeight: 500,
backgroundColor: "#877361",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
overflowY: "auto",
padding: 0,
display: "flex",
@ -255,11 +272,10 @@ export const LeftWidgetTab = observer(
<Box
sx={{
width: "100%",
height: 200,
backgroundColor: "grey.300",
height: 175,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "3px",
}}
>
{data.left.media.length > 0 ? (
@ -277,24 +293,50 @@ export const LeftWidgetTab = observer(
<Box
sx={{
backgroundColor: "#877361",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
color: "white",
padding: 1.5,
margin: "5px 0px 5px 0px",
display: "flex",
flexDirection: "column",
gap: 1,
padding: 1,
}}
>
<Typography
variant="h5"
component="h2"
sx={{ wordBreak: "break-word" }}
sx={{
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
}}
>
{data?.left?.heading || "Название информации"}
</Typography>
<Typography
variant="h6"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "18px",
lineHeight: "120%",
}}
>
{sight[language as Language].address}
</Typography>
</Box>
{data?.left?.body && (
<Box
sx={{
padding: 2,
padding: 1,
maxHeight: "300px",
overflowY: "scroll",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
}}
>
@ -310,6 +352,7 @@ export const LeftWidgetTab = observer(
<Button
variant="contained"
color="success"
startIcon={<Save color="white" size={18} />}
onClick={async () => {
await updateSight();
toast.success("Достопримечательность сохранена");
@ -339,6 +382,11 @@ export const LeftWidgetTab = observer(
onClose={handleCloseArticleDialog}
onSelectArticle={handleSelectArticle}
/>
<DeleteModal
open={isDeleteModalOpen}
onDelete={handleDeleteLeftArticle}
onCancel={() => setIsDeleteModalOpen(false)}
/>
</>
);
}

View File

@ -23,7 +23,7 @@ import {
ReactMarkdownComponent,
ReactMarkdownEditor,
} from "@widgets";
import { ImagePlus, Plus, X } from "lucide-react";
import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
@ -49,6 +49,19 @@ export const RightWidgetTab = observer(
createNewRightArticle,
} = editSightStore;
const [previewMedia, setPreviewMedia] = useState<any | null>(null);
useEffect(() => {
if (sight.common.preview_media) {
setPreviewMedia(sight.common.preview_media);
}
}, [sight.common.preview_media]);
const handleUnlinkPreviewMedia = () => {
unlinkPreviewMedia();
setPreviewMedia(null);
};
const [uploadMediaOpen, setUploadMediaOpen] = useState(false);
const { language } = languageStore;
const [type, setType] = useState<"article" | "media">("media");
@ -194,6 +207,7 @@ export const RightWidgetTab = observer(
<Button
variant="contained"
color="primary"
startIcon={<Unlink color="white" size={18} />}
onClick={() => {
unlinkRightArticle(
sight[language].right[activeArticleIndex].id
@ -205,7 +219,8 @@ export const RightWidgetTab = observer(
</Button>
<Button
variant="contained"
color="success"
color="error"
startIcon={<Trash2 size={18} />}
onClick={() => {
deleteRightArticle(
sight[language].right[activeArticleIndex].id
@ -285,31 +300,65 @@ export const RightWidgetTab = observer(
<Box className="w-[80%] border border-gray-300 rounded-2xl relative">
{sight.common.preview_media && (
<>
<Box className="absolute top-4 right-4">
<button
className="w-10 h-10 flex items-center justify-center"
onClick={unlinkPreviewMedia}
>
<X size={20} color="red" />
</button>
</Box>
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 relative flex justify-center items-center">
{type === "media" && (
<Box className="w-[80%] border border-gray-300 rounded-2xl relative flex items-center justify-center">
{previewMedia && (
<>
<Box className="absolute top-4 right-4 z-10">
<button
className="w-10 h-10 flex items-center justify-center z-10"
onClick={handleUnlinkPreviewMedia}
>
<X size={20} color="red" />
</button>
</Box>
<MediaViewer
media={{
id: sight.common.preview_media || "",
media_type: 1,
filename: sight.common.preview_media || "",
}}
/>
<Box sx={{}}>
<MediaViewer
media={{
id: previewMedia.id || "",
media_type: previewMedia.media_type,
filename: previewMedia.filename || "",
}}
/>
</Box>
</>
)}
{!previewMedia && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
)}
</Box>
)}
</Box>
</>
)}
{!sight.common.preview_media && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
<Box className="w-full h-full flex justify-center items-center">
<Box
sx={{
maxWidth: "500px",
maxHeight: "100%",
display: "flex",
flexGrow: 1,
margin: "0 auto",
justifyContent: "center",
}}
>
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
</Box>
</Box>
)}
</Box>
)}
@ -423,8 +472,13 @@ export const RightWidgetTab = observer(
justifyContent: "flex-end",
}}
>
<Button variant="contained" color="success" onClick={handleSave}>
Сохранить изменения
<Button
variant="contained"
startIcon={<Save color="white" size={18} />}
color="success"
onClick={handleSave}
>
Сохранить
</Button>
</Box>
</Box>

View File

@ -6,10 +6,10 @@ import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import { authInstance, cityStore, languageStore, sightsStore } from "@shared";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Button } from "@mui/material";
import { LanguageSwitcher } from "@widgets";
import { DeleteModal, LanguageSwitcher } from "@widgets";
import { Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
@ -29,6 +29,8 @@ export const SightsTable = observer(() => {
const { language } = languageStore;
const { sights, getSights } = sightsStore;
const { cities, getCities } = cityStore;
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState<number | null>(null);
useEffect(() => {
const fetchData = async () => {
@ -92,7 +94,10 @@ export const SightsTable = observer(() => {
</button>
<button
className="rounded-md px-3 py-1.5 transition-transform transform hover:scale-105"
onClick={() => handleDelete(row?.id)}
onClick={() => {
setIsDeleteModalOpen(true);
setRowId(row?.id);
}}
>
<Trash2 size={18} className="text-red-500" />
</button>
@ -103,6 +108,17 @@ export const SightsTable = observer(() => {
</TableBody>
</Table>
</TableContainer>
<DeleteModal
open={isDeleteModalOpen}
onDelete={async () => {
if (rowId) {
await handleDelete(rowId);
}
setIsDeleteModalOpen(false);
setRowId(null);
}}
onCancel={() => setIsDeleteModalOpen(false)}
/>
</>
);
});

View File

@ -12,3 +12,5 @@ export * from "./MediaArea";
export * from "./ModelViewer3D";
export * from "./MediaAreaForSight";
export * from "./ImageUploadCard";
export * from "./LeaveAgree";
export * from "./DeleteModal";