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