feat: Delete user preview + snapshot preview + coordinates update
				
					
				
			This commit is contained in:
		| @@ -19,9 +19,7 @@ import { | ||||
|   VehicleListPage, | ||||
|   ArticleListPage, | ||||
|   CityPreviewPage, | ||||
|   UserPreviewPage, | ||||
|   CountryPreviewPage, | ||||
|   SnapshotPreviewPage, | ||||
|   VehiclePreviewPage, | ||||
|   CarrierPreviewPage, | ||||
|   SnapshotCreatePage, | ||||
| @@ -132,12 +130,10 @@ const router = createBrowserRouter([ | ||||
|  | ||||
|       // User | ||||
|       { path: "user", element: <UserListPage /> }, | ||||
|       { path: "user/:id", element: <UserPreviewPage /> }, | ||||
|  | ||||
|       // Snapshot | ||||
|       { path: "snapshot", element: <SnapshotListPage /> }, | ||||
|       { path: "snapshot/create", element: <SnapshotCreatePage /> }, | ||||
|       { path: "snapshot/:id", element: <SnapshotPreviewPage /> }, | ||||
|  | ||||
|       // Carrier | ||||
|       { path: "carrier", element: <CarrierListPage /> }, | ||||
|   | ||||
| @@ -3,3 +3,7 @@ | ||||
| button { | ||||
|   cursor: pointer; | ||||
| } | ||||
|  | ||||
| .mde-preview { | ||||
|   background-color: #f5f5f5; | ||||
| } | ||||
|   | ||||
| @@ -1135,7 +1135,7 @@ const MapControls: React.FC<MapControlsProps> = ({ | ||||
|     }, | ||||
|     { | ||||
|       mode: "statistics", | ||||
|       title: "Инфо", | ||||
|       title: "Информация", | ||||
|       longTitle: "Информация", | ||||
|       icon: <StatsIcon />, | ||||
|       action: () => mapService.activateStatisticsMode(), | ||||
|   | ||||
| @@ -53,9 +53,7 @@ export const SnapshotListPage = observer(() => { | ||||
|             > | ||||
|               <DatabaseBackup size={20} className="text-blue-500" /> | ||||
|             </button> | ||||
|             <button onClick={() => navigate(`/snapshot/${params.row.id}`)}> | ||||
|               <Eye size={20} className="text-green-500" /> | ||||
|             </button> | ||||
|  | ||||
|             <button | ||||
|               onClick={() => { | ||||
|                 setIsDeleteModalOpen(true); | ||||
|   | ||||
| @@ -1,58 +0,0 @@ | ||||
| import { Paper } from "@mui/material"; | ||||
| import { snapshotStore } from "@shared"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { ArrowLeft } from "lucide-react"; | ||||
| import { useEffect } from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
|  | ||||
| export const SnapshotPreviewPage = observer(() => { | ||||
|   const { id } = useParams(); | ||||
|   const { getSnapshot, snapshot } = snapshotStore; | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     (async () => { | ||||
|       await getSnapshot(id as string); | ||||
|     })(); | ||||
|   }, [id]); | ||||
|  | ||||
|   return ( | ||||
|     <Paper className="w-full h-full p-3 flex flex-col gap-10"> | ||||
|       <div className="flex justify-between items-center"> | ||||
|         <button | ||||
|           className="flex items-center gap-2" | ||||
|           onClick={() => navigate(-1)} | ||||
|         > | ||||
|           <ArrowLeft size={20} /> | ||||
|           Назад | ||||
|         </button> | ||||
|         {/* <div className="flex gap-2"> | ||||
|           <Button | ||||
|             variant="contained" | ||||
|             color="primary" | ||||
|             onClick={() => navigate(`/snapshot/${id}/edit`)} | ||||
|             startIcon={<Pencil size={20} />} | ||||
|           > | ||||
|             Редактировать | ||||
|           </Button> | ||||
|           <Button | ||||
|             variant="contained" | ||||
|             color="error" | ||||
|             onClick={() => navigate(`/snapshot/${id}/delete`)} | ||||
|             startIcon={<Trash2 size={20} />} | ||||
|           > | ||||
|             Удалить | ||||
|           </Button> | ||||
|         </div> */} | ||||
|       </div> | ||||
|       {snapshot && ( | ||||
|         <div className="flex flex-col gap-10 w-full"> | ||||
|           <div className="flex flex-col gap-2"> | ||||
|             <h1 className="text-lg font-bold">Название</h1> | ||||
|             <p>{snapshot?.Name}</p> | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|     </Paper> | ||||
|   ); | ||||
| }); | ||||
| @@ -1,3 +1,3 @@ | ||||
| export * from "./SnapshotListPage"; | ||||
| export * from "./SnapshotPreviewPage"; | ||||
|  | ||||
| export * from "./SnapshotCreatePage"; | ||||
|   | ||||
| @@ -56,9 +56,6 @@ export const UserListPage = observer(() => { | ||||
|       renderCell: (params: GridRenderCellParams) => { | ||||
|         return ( | ||||
|           <div className="flex h-full gap-7 justify-center items-center"> | ||||
|             <button onClick={() => navigate(`/user/${params.row.id}`)}> | ||||
|               <Eye size={20} className="text-green-500" /> | ||||
|             </button> | ||||
|             <button | ||||
|               onClick={() => { | ||||
|                 setIsDeleteModalOpen(true); | ||||
|   | ||||
| @@ -1,74 +0,0 @@ | ||||
| import { Paper } from "@mui/material"; | ||||
| import { userStore } from "@shared"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { ArrowLeft } from "lucide-react"; | ||||
| import { useEffect } from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
|  | ||||
| export const UserPreviewPage = observer(() => { | ||||
|   const { id } = useParams(); | ||||
|   const { getUser, user } = userStore; | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     (async () => { | ||||
|       await getUser(Number(id)); | ||||
|     })(); | ||||
|   }, [id]); | ||||
|  | ||||
|   return ( | ||||
|     <Paper className="w-full h-full p-3 flex flex-col gap-10"> | ||||
|       <div className="flex justify-between items-center"> | ||||
|         <button | ||||
|           className="flex items-center gap-2" | ||||
|           onClick={() => navigate(-1)} | ||||
|         > | ||||
|           <ArrowLeft size={20} /> | ||||
|           Назад | ||||
|         </button> | ||||
|         {/* <div className="flex gap-2"> | ||||
|           <Button | ||||
|             variant="contained" | ||||
|             color="primary" | ||||
|             onClick={() => navigate(`/user/${id}/edit`)} | ||||
|             startIcon={<Pencil size={20} />} | ||||
|           > | ||||
|             Редактировать | ||||
|           </Button> | ||||
|           <Button | ||||
|             variant="contained" | ||||
|             color="error" | ||||
|             onClick={() => navigate(`/user/${id}/delete`)} | ||||
|             startIcon={<Trash2 size={20} />} | ||||
|           > | ||||
|             Удалить | ||||
|           </Button> | ||||
|         </div> */} | ||||
|       </div> | ||||
|       {user && ( | ||||
|         <div className="flex flex-col gap-10 w-full"> | ||||
|           <div className="flex flex-col gap-2"> | ||||
|             <h1 className="text-lg font-bold">Название</h1> | ||||
|             <p>{user?.name}</p> | ||||
|           </div> | ||||
|  | ||||
|           <div className="flex flex-col gap-2"> | ||||
|             <h1 className="text-lg font-bold">Email</h1> | ||||
|             <p>{user?.email}</p> | ||||
|           </div> | ||||
|  | ||||
|           <div className="flex flex-col gap-2"> | ||||
|             <h1 className="text-lg font-bold">Роль</h1> | ||||
|             <p | ||||
|               className={ | ||||
|                 user?.is_admin === true ? "text-green-500" : "text-red-500" | ||||
|               } | ||||
|             > | ||||
|               {user?.is_admin ? "Администратор" : "Пользователь"} | ||||
|             </p> | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|     </Paper> | ||||
|   ); | ||||
| }); | ||||
| @@ -1,2 +1 @@ | ||||
| export * from "./UserListPage"; | ||||
| export * from "./UserPreviewPage"; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { useMemo } from "react"; | ||||
| import { styled } from "@mui/material/styles"; | ||||
| import SimpleMDE, { SimpleMDEReactProps } from "react-simplemde-editor"; | ||||
| import SimpleMDE from "react-simplemde-editor"; | ||||
| import "easymde/dist/easymde.min.css"; | ||||
|  | ||||
| const StyledMarkdownEditor = styled("div")(({ theme }) => ({ | ||||
| @@ -19,7 +20,6 @@ const StyledMarkdownEditor = styled("div")(({ theme }) => ({ | ||||
|   "& .editor-statusbar": { | ||||
|     display: "none", | ||||
|   }, | ||||
|   // Стили для самого редактора | ||||
|   "& .CodeMirror": { | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     color: theme.palette.text.primary, | ||||
| @@ -33,14 +33,12 @@ const StyledMarkdownEditor = styled("div")(({ theme }) => ({ | ||||
|     minHeight: "200px", | ||||
|     maxHeight: "500px", | ||||
|   }, | ||||
|   // Стили для текста в редакторе | ||||
|   "& .CodeMirror-selected": { | ||||
|     backgroundColor: `${theme.palette.action.selected} !important`, | ||||
|   }, | ||||
|   "& .CodeMirror-cursor": { | ||||
|     borderLeftColor: theme.palette.text.primary, | ||||
|   }, | ||||
|   // Стили для markdown разметки | ||||
|   "& .cm-header": { | ||||
|     color: theme.palette.primary.main, | ||||
|   }, | ||||
| @@ -57,13 +55,11 @@ const StyledMarkdownEditor = styled("div")(({ theme }) => ({ | ||||
|   "& .cm-formatting": { | ||||
|     color: theme.palette.text.secondary, | ||||
|   }, | ||||
|  | ||||
|   "& .CodeMirror .editor-preview-full": { | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     color: theme.palette.text.primary, | ||||
|     borderColor: theme.palette.divider, | ||||
|   }, | ||||
|  | ||||
|   "& .EasyMDEContainer": { | ||||
|     position: "relative", | ||||
|     zIndex: 1000, | ||||
| @@ -73,45 +69,53 @@ const StyledMarkdownEditor = styled("div")(({ theme }) => ({ | ||||
|   }, | ||||
| })); | ||||
|  | ||||
| export const ReactMarkdownEditor = (props: SimpleMDEReactProps) => { | ||||
|   if (props.options) | ||||
|     props.options.toolbar = [ | ||||
|       "bold", | ||||
|       "italic", | ||||
|       "strikethrough", | ||||
|       { | ||||
|         name: "Underline", | ||||
|         action: (editor: any) => { | ||||
|           const cm = editor.codemirror; | ||||
|           let output = ""; | ||||
|           const selectedText = cm.getSelection(); | ||||
|           const text = selectedText ?? "placeholder"; | ||||
|  | ||||
|           output = "<u>" + text + "</u>"; | ||||
|           cm.replaceSelection(output); | ||||
| export const ReactMarkdownEditor = ({ | ||||
|   options: incomingOptions, | ||||
|   ...restProps | ||||
| }: any) => { | ||||
|   const mergedOptions = useMemo(() => { | ||||
|     return { | ||||
|       ...incomingOptions, | ||||
|       forceSync: true, | ||||
|       spellChecker: false, | ||||
|       toolbar: [ | ||||
|         "bold", | ||||
|         "italic", | ||||
|         "strikethrough", | ||||
|         { | ||||
|           name: "Underline", | ||||
|           action: (editor: any) => { | ||||
|             const cm = editor.codemirror; | ||||
|             const selectedText = cm.getSelection(); | ||||
|             const text = selectedText || "placeholder"; | ||||
|             const output = `<u>${text}</u>`; | ||||
|             cm.replaceSelection(output); | ||||
|           }, | ||||
|           className: "fa fa-underline", | ||||
|           title: "Underline (Ctrl/Cmd-Alt-U)", | ||||
|         }, | ||||
|         className: "fa fa-underline", // Look for a suitable icon | ||||
|         title: "Underline (Ctrl/Cmd-Alt-U)", | ||||
|       }, | ||||
|       "heading", | ||||
|       "quote", | ||||
|       "unordered-list", | ||||
|       "ordered-list", | ||||
|       "link", | ||||
|       "image", | ||||
|       "code", | ||||
|       "table", | ||||
|       "horizontal-rule", | ||||
|       "preview", | ||||
|       "fullscreen", | ||||
|       "guide", | ||||
|     ]; | ||||
|         "heading", | ||||
|         "quote", | ||||
|         "unordered-list", | ||||
|         "ordered-list", | ||||
|         "link", | ||||
|         "image", | ||||
|         "code", | ||||
|         "table", | ||||
|         "horizontal-rule", | ||||
|         "preview", | ||||
|         "fullscreen", | ||||
|         "guide", | ||||
|       ], | ||||
|     }; | ||||
|   }, []); // создаётся один раз | ||||
|  | ||||
|   return ( | ||||
|     <StyledMarkdownEditor | ||||
|       className="my-markdown-editor" | ||||
|       sx={{ marginTop: 1.5, marginBottom: 3 }} | ||||
|     > | ||||
|       <SimpleMDE {...props} /> | ||||
|       <SimpleMDE options={mergedOptions} {...restProps} /> | ||||
|     </StyledMarkdownEditor> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -40,7 +40,7 @@ export const CreateInformationTab = observer( | ||||
|     const data = sight[language]; | ||||
|  | ||||
|     const [, setCity] = useState<number>(sight.city_id ?? 0); | ||||
|     const [coordinates, setCoordinates] = useState<string>(`0 0`); | ||||
|     const [coordinates, setCoordinates] = useState<string>(`0, 0`); | ||||
|  | ||||
|     // Menu state for each media button | ||||
|     const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null); | ||||
| @@ -60,7 +60,7 @@ export const CreateInformationTab = observer( | ||||
|     useEffect(() => { | ||||
|       // Показывать только при инициализации (не менять при ошибках пользователя) | ||||
|       if (sight.latitude !== 0 || sight.longitude !== 0) { | ||||
|         setCoordinates(`${sight.latitude} ${sight.longitude}`); | ||||
|         setCoordinates(`${sight.latitude}, ${sight.longitude}`); | ||||
|       } | ||||
|       // если координаты обнулились — оставить поле как есть | ||||
|     }, [sight.latitude, sight.longitude]); | ||||
|   | ||||
| @@ -418,7 +418,7 @@ export const CreateRightTab = observer( | ||||
|                       <Box sx={{ minHeight: 200, flexGrow: 1 }}> | ||||
|                         <ReactMarkdownEditor | ||||
|                           value={currentRightArticle.body} | ||||
|                           onChange={(mdValue) => | ||||
|                           onChange={(mdValue: any) => | ||||
|                             activeArticleIndex !== null && | ||||
|                             updateRightArticleInfo( | ||||
|                               activeArticleIndex, | ||||
|   | ||||
| @@ -42,7 +42,7 @@ export const InformationTab = observer( | ||||
|     const { sight, updateSightInfo, updateSight } = editSightStore; | ||||
|  | ||||
|     const [, setCity] = useState<number>(sight.common.city_id ?? 0); | ||||
|     const [coordinates, setCoordinates] = useState<string>(`0 0`); | ||||
|     const [coordinates, setCoordinates] = useState<string>(`0, 0`); | ||||
|  | ||||
|     // Menu state for each media button | ||||
|     const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null); | ||||
| @@ -54,7 +54,7 @@ export const InformationTab = observer( | ||||
|     useEffect(() => { | ||||
|       // Показывать только при инициализации (не менять при ошибках пользователя) | ||||
|       if (sight.common.latitude !== 0 || sight.common.longitude !== 0) { | ||||
|         setCoordinates(`${sight.common.latitude} ${sight.common.longitude}`); | ||||
|         setCoordinates(`${sight.common.latitude}, ${sight.common.longitude}`); | ||||
|       } | ||||
|       // если координаты обнулились — оставить поле как есть | ||||
|     }, [sight.common.latitude, sight.common.longitude]); | ||||
| @@ -178,10 +178,12 @@ export const InformationTab = observer( | ||||
|                   label="Координаты" | ||||
|                   value={coordinates} | ||||
|                   onChange={(e) => { | ||||
|                     const input = e.target.value; | ||||
|                     setCoordinates(input); // показываем как есть | ||||
|                     const newValue = e.target.value; | ||||
|                     setCoordinates(newValue); // сохраняем ввод пользователя как есть | ||||
|  | ||||
|                     const [latStr, lonStr] = input.split(/\s+/); // учитываем любые пробелы | ||||
|                     // Обрабатываем значение для сохранения | ||||
|                     const input = newValue.replace(/,/g, " ").trim(); | ||||
|                     const [latStr, lonStr] = input.split(/\s+/); | ||||
|  | ||||
|                     const lat = parseFloat(latStr); | ||||
|                     const lon = parseFloat(lonStr); | ||||
| @@ -212,7 +214,7 @@ export const InformationTab = observer( | ||||
|                   }} | ||||
|                   fullWidth | ||||
|                   variant="outlined" | ||||
|                   placeholder="Введите координаты в формате: широта долгота" | ||||
|                   placeholder="Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)" | ||||
|                 /> | ||||
|               </Box> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user