feat: Refactor old code with delete
modal and icons
for buttons
This commit is contained in:
@ -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>
|
||||
);
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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 }}>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
33
src/widgets/DeleteModal/index.tsx
Normal file
33
src/widgets/DeleteModal/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
21
src/widgets/LeaveAgree/index.tsx
Normal file
21
src/widgets/LeaveAgree/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -12,3 +12,5 @@ export * from "./MediaArea";
|
||||
export * from "./ModelViewer3D";
|
||||
export * from "./MediaAreaForSight";
|
||||
export * from "./ImageUploadCard";
|
||||
export * from "./LeaveAgree";
|
||||
export * from "./DeleteModal";
|
||||
|
Reference in New Issue
Block a user