feat: Sight Page update
This commit is contained in:
parent
87386c6a73
commit
a8777a974a
@ -25,6 +25,7 @@
|
||||
"path": "^0.12.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-sphere-viewer": "^6.2.3",
|
||||
"react-router-dom": "^7.6.1",
|
||||
|
@ -6,8 +6,9 @@ import {
|
||||
MainPage,
|
||||
SightPage,
|
||||
} from "@pages";
|
||||
import { authStore, editSightStore, sightsStore } from "@shared";
|
||||
import { authStore, createSightStore, editSightStore } from "@shared";
|
||||
import { Layout } from "@widgets";
|
||||
import { runInAction } from "mobx";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom";
|
||||
@ -34,10 +35,15 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
export const Router = () => {
|
||||
const pathname = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
editSightStore.clearSightInfo();
|
||||
sightsStore.clearCreateSight();
|
||||
createSightStore.clearCreateSight();
|
||||
runInAction(() => {
|
||||
editSightStore.hasLoadedCommon = false;
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Box, Tab, Tabs } from "@mui/material";
|
||||
import { articlesStore, cityStore, languageStore } from "@shared";
|
||||
import { InformationTab, RightWidgetTab } from "@widgets";
|
||||
import { LeftWidgetTab } from "@widgets";
|
||||
import { CreateInformationTab, CreateLeftTab, CreateRightTab } from "@widgets";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@ -21,8 +20,11 @@ export const CreateSightPage = observer(() => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getCities();
|
||||
getArticles(languageStore.language);
|
||||
const fetchData = async () => {
|
||||
await getCities();
|
||||
await getArticles(languageStore.language);
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -60,9 +62,9 @@ export const CreateSightPage = observer(() => {
|
||||
</Box>
|
||||
|
||||
<div className="flex-1">
|
||||
<InformationTab value={value} index={0} />
|
||||
<LeftWidgetTab value={value} index={1} />
|
||||
<RightWidgetTab value={value} index={2} />
|
||||
<CreateInformationTab value={value} index={0} />
|
||||
<CreateLeftTab value={value} index={1} />
|
||||
<CreateRightTab value={value} index={2} />
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
|
@ -20,7 +20,7 @@ function a11yProps(index: number) {
|
||||
|
||||
export const EditSightPage = observer(() => {
|
||||
const [value, setValue] = useState(0);
|
||||
const { getSightInfo } = editSightStore;
|
||||
const { sight, getSightInfo } = editSightStore;
|
||||
const { getArticles } = articlesStore;
|
||||
const { language } = languageStore;
|
||||
const { id } = useParams();
|
||||
@ -75,11 +75,13 @@ export const EditSightPage = observer(() => {
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<div className="flex-1">
|
||||
<InformationTab value={value} index={0} />
|
||||
<LeftWidgetTab value={value} index={1} />
|
||||
<RightWidgetTab value={value} index={2} />
|
||||
</div>
|
||||
{sight.common.id !== 0 && (
|
||||
<div className="flex-1">
|
||||
<InformationTab value={value} index={0} />
|
||||
<LeftWidgetTab value={value} index={1} />
|
||||
<RightWidgetTab value={value} index={2} />
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { articlesStore } from "@shared";
|
||||
import { articlesStore, authInstance, languageStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
@ -22,8 +22,13 @@ import { ReactMarkdownComponent } from "@widgets";
|
||||
interface SelectArticleModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSelectArticle: (articleId: string) => void;
|
||||
linkedArticleIds?: string[]; // Add optional prop for linked articles
|
||||
onSelectArticle: (
|
||||
articleId: number,
|
||||
heading: string,
|
||||
body: string,
|
||||
media: { id: string; media_type: number; filename: string }[]
|
||||
) => void;
|
||||
linkedArticleIds?: number[]; // Add optional prop for linked articles
|
||||
}
|
||||
|
||||
export const SelectArticleModal = observer(
|
||||
@ -35,7 +40,7 @@ export const SelectArticleModal = observer(
|
||||
}: SelectArticleModalProps) => {
|
||||
const { articles, getArticle, getArticleMedia } = articlesStore;
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
|
||||
const [selectedArticleId, setSelectedArticleId] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -50,12 +55,21 @@ export const SelectArticleModal = observer(
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
const handleKeyPress = async (event: KeyboardEvent) => {
|
||||
if (event.key.toLowerCase() === "enter") {
|
||||
event.preventDefault();
|
||||
if (selectedArticleId) {
|
||||
onSelectArticle(selectedArticleId);
|
||||
const media = await authInstance.get(
|
||||
`/article/${selectedArticleId}/media`
|
||||
);
|
||||
onSelectArticle(
|
||||
selectedArticleId,
|
||||
articlesStore.articleData?.heading || "",
|
||||
articlesStore.articleData?.body || "",
|
||||
media.data || []
|
||||
);
|
||||
onClose();
|
||||
setSelectedArticleId(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -66,9 +80,7 @@ export const SelectArticleModal = observer(
|
||||
};
|
||||
}, [selectedArticleId, onSelectArticle, onClose]);
|
||||
|
||||
const handleArticleClick = async (articleId: string) => {
|
||||
if (selectedArticleId === articleId) return;
|
||||
|
||||
const handleArticleClick = async (articleId: number) => {
|
||||
setSelectedArticleId(articleId);
|
||||
setIsLoading(true);
|
||||
|
||||
@ -86,14 +98,13 @@ export const SelectArticleModal = observer(
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
// @ts-ignore
|
||||
const filteredArticles = articles
|
||||
// @ts-ignore
|
||||
.filter((article) => !linkedArticleIds.includes(article.id))
|
||||
// @ts-ignore
|
||||
.filter((article) =>
|
||||
article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const filteredArticles = articles[languageStore.language].filter(
|
||||
(article) => !linkedArticleIds.includes(article.id)
|
||||
);
|
||||
// .filter((article) =>
|
||||
// article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
// );
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
return (
|
||||
@ -150,7 +161,17 @@ export const SelectArticleModal = observer(
|
||||
<ListItemButton
|
||||
key={article.id}
|
||||
onClick={() => handleArticleClick(article.id)}
|
||||
onDoubleClick={() => onSelectArticle(article.id)}
|
||||
onDoubleClick={async () => {
|
||||
const media = await authInstance.get(
|
||||
`/article/${article.id}/media`
|
||||
);
|
||||
onSelectArticle(
|
||||
article.id,
|
||||
article.heading,
|
||||
article.body,
|
||||
media.data
|
||||
);
|
||||
}}
|
||||
selected={selectedArticleId === article.id}
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
@ -288,9 +309,22 @@ export const SelectArticleModal = observer(
|
||||
<Button onClick={onClose}>Отмена</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
selectedArticleId && onSelectArticle(selectedArticleId)
|
||||
}
|
||||
onClick={async () => {
|
||||
if (selectedArticleId) {
|
||||
const media = await authInstance.get(
|
||||
`/article/${selectedArticleId}/media`
|
||||
);
|
||||
|
||||
onSelectArticle(
|
||||
selectedArticleId,
|
||||
articlesStore.articleData?.heading || "",
|
||||
articlesStore.articleData?.body || "",
|
||||
media.data
|
||||
);
|
||||
onClose();
|
||||
setSelectedArticleId(null);
|
||||
}
|
||||
}}
|
||||
disabled={!selectedArticleId || isLoading}
|
||||
>
|
||||
Выбрать
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { mediaStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
@ -21,7 +21,12 @@ import { MediaViewer } from "@widgets";
|
||||
interface SelectMediaDialogProps {
|
||||
open: boolean; // Corrected prop name
|
||||
onClose: () => void;
|
||||
onSelectMedia: (mediaId: string) => void; // Renamed from onSelectArticle
|
||||
onSelectMedia: (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}) => void; // Renamed from onSelectArticle
|
||||
linkedMediaIds?: string[]; // Renamed from linkedArticleIds, assuming it refers to media already in use
|
||||
}
|
||||
|
||||
@ -32,15 +37,14 @@ export const SelectMediaDialog = observer(
|
||||
onSelectMedia, // Renamed prop
|
||||
linkedMediaIds = [], // Default to empty array if not provided, renamed
|
||||
}: SelectMediaDialogProps) => {
|
||||
const { media, getMedia } = mediaStore; // Confirmed: using mediaStore for media
|
||||
const { media, getMedia } = mediaStore;
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [hoveredMediaId, setHoveredMediaId] = useState<string | null>(null);
|
||||
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Fetch media on component mount
|
||||
useEffect(() => {
|
||||
getMedia();
|
||||
}, [getMedia]); // getMedia should be a dependency to avoid lint warnings if it's not stable
|
||||
}, [getMedia]);
|
||||
|
||||
// Keyboard event listener for "Enter" key to select hovered media
|
||||
useEffect(() => {
|
||||
@ -49,7 +53,10 @@ export const SelectMediaDialog = observer(
|
||||
event.preventDefault(); // Prevent browser default action (e.g., form submission)
|
||||
|
||||
if (hoveredMediaId) {
|
||||
onSelectMedia(hoveredMediaId); // Call onSelectMedia
|
||||
const mediaItem = media.find((m) => m.id === hoveredMediaId);
|
||||
if (mediaItem) {
|
||||
onSelectMedia(mediaItem);
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
@ -61,26 +68,6 @@ export const SelectMediaDialog = observer(
|
||||
};
|
||||
}, [hoveredMediaId, onSelectMedia, onClose]); // Dependencies for keyboard listener
|
||||
|
||||
// Effect for handling hover timeout (if you want to clear the preview after a delay)
|
||||
// Based on the original code, it seemed like you wanted a delay for showing,
|
||||
// but typically for a preview, it's immediate on hover and cleared on mouse leave.
|
||||
// I've removed the 5-second timeout for setting the ID as it's counter-intuitive for a live preview.
|
||||
// If you intend for the preview to disappear after a short while *after* the mouse leaves,
|
||||
// you would implement a mouseleave timer. For now, it will clear on mouseleave.
|
||||
|
||||
const handleMouseEnter = (mediaId: string) => {
|
||||
if (hoverTimerRef.current) {
|
||||
clearTimeout(hoverTimerRef.current);
|
||||
}
|
||||
setHoveredMediaId(mediaId);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
// You can add a small delay here if you want the preview to linger for a moment
|
||||
// before disappearing, e.g., setTimeout(() => setHoveredMediaId(null), 200);
|
||||
setHoveredMediaId(null);
|
||||
};
|
||||
|
||||
const filteredMedia = media
|
||||
.filter((mediaItem) => !linkedMediaIds.includes(mediaItem.id)) // Use mediaItem to avoid name collision
|
||||
.filter((mediaItem) =>
|
||||
@ -125,9 +112,11 @@ export const SelectMediaDialog = observer(
|
||||
) => (
|
||||
<ListItemButton
|
||||
key={mediaItem.id}
|
||||
onClick={() => onSelectMedia(mediaItem.id)} // Call onSelectMedia
|
||||
onMouseEnter={() => handleMouseEnter(mediaItem.id)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={() => setHoveredMediaId(mediaItem.id)} // Call onSelectMedia
|
||||
onDoubleClick={() => {
|
||||
onSelectMedia(mediaItem);
|
||||
onClose();
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
mb: 0.5,
|
||||
|
259
src/shared/modals/UploadMediaDialog/index.tsx
Normal file
259
src/shared/modals/UploadMediaDialog/index.tsx
Normal file
@ -0,0 +1,259 @@
|
||||
import { MEDIA_TYPE_LABELS, editSightStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Paper,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Snackbar,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from "@mui/material";
|
||||
import { Save } from "lucide-react";
|
||||
import { ModelViewer3D } from "@widgets";
|
||||
|
||||
interface UploadMediaDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
afterUpload: (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const UploadMediaDialog = observer(
|
||||
({ open, onClose, afterUpload }: UploadMediaDialogProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [mediaName, setMediaName] = useState("");
|
||||
const [mediaFilename, setMediaFilename] = useState("");
|
||||
const [mediaType, setMediaType] = useState(0);
|
||||
const [mediaFile, setMediaFile] = useState<File | null>(null);
|
||||
const { fileToUpload, uploadMedia } = editSightStore;
|
||||
const [mediaUrl, setMediaUrl] = useState<string | null>(null);
|
||||
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (fileToUpload) {
|
||||
setMediaFile(fileToUpload);
|
||||
setMediaFilename(fileToUpload.name);
|
||||
// Try to determine media type from file extension
|
||||
const extension = fileToUpload.name.split(".").pop()?.toLowerCase();
|
||||
if (extension) {
|
||||
if (["glb", "gltf"].includes(extension)) {
|
||||
setAvailableMediaTypes([6]);
|
||||
setMediaType(6);
|
||||
}
|
||||
if (["jpg", "jpeg", "png", "gif"].includes(extension)) {
|
||||
// Для изображений доступны все типы кроме видео
|
||||
setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель
|
||||
setMediaType(1); // По умолчанию Фото
|
||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||
// Для видео только тип Видео
|
||||
setAvailableMediaTypes([2]);
|
||||
setMediaType(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [fileToUpload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mediaFile) {
|
||||
setMediaUrl(URL.createObjectURL(mediaFile as Blob));
|
||||
}
|
||||
}, [mediaFile]);
|
||||
|
||||
// const fileFormat = useEffect(() => {
|
||||
// const handleKeyPress = (event: KeyboardEvent) => {
|
||||
// if (event.key.toLowerCase() === "enter" && !event.ctrlKey) {
|
||||
// event.preventDefault();
|
||||
// onClose();
|
||||
// }
|
||||
// };
|
||||
|
||||
// window.addEventListener("keydown", handleKeyPress);
|
||||
// return () => window.removeEventListener("keydown", handleKeyPress);
|
||||
// }, [onClose]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!mediaFile) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const media = await uploadMedia(
|
||||
mediaFilename,
|
||||
mediaType,
|
||||
mediaFile,
|
||||
mediaName
|
||||
);
|
||||
if (media) {
|
||||
await afterUpload(media);
|
||||
}
|
||||
setSuccess(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to save media");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Просмотр медиа</DialogTitle>
|
||||
<DialogContent
|
||||
className="flex gap-4"
|
||||
dividers
|
||||
sx={{
|
||||
height: "600px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
pt: 2,
|
||||
}}
|
||||
>
|
||||
<Box className="flex flex-col gap-4">
|
||||
<Box className="flex gap-2">
|
||||
<TextField
|
||||
fullWidth
|
||||
value={mediaName}
|
||||
onChange={(e) => setMediaName(e.target.value)}
|
||||
label="Название медиа"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={mediaFilename}
|
||||
onChange={(e) => setMediaFilename(e.target.value)}
|
||||
label="Название файла"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<FormControl fullWidth sx={{ width: "50%" }}>
|
||||
<InputLabel>Тип медиа</InputLabel>
|
||||
<Select
|
||||
value={mediaType}
|
||||
label="Тип медиа"
|
||||
onChange={(e) => setMediaType(Number(e.target.value))}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{availableMediaTypes.map((type) => (
|
||||
<MenuItem key={type} value={type}>
|
||||
{
|
||||
MEDIA_TYPE_LABELS[
|
||||
type as keyof typeof MEDIA_TYPE_LABELS
|
||||
]
|
||||
}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Box className="flex gap-4 h-full">
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
flex: 1,
|
||||
p: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 400,
|
||||
}}
|
||||
>
|
||||
{/* <MediaViewer
|
||||
media={{
|
||||
id: "",
|
||||
media_type: mediaType,
|
||||
filename: mediaFilename,
|
||||
}}
|
||||
/> */}
|
||||
{mediaType === 6 && mediaUrl && (
|
||||
<ModelViewer3D fileUrl={mediaUrl} height="100%" />
|
||||
)}
|
||||
{mediaType !== 6 && mediaType !== 2 && mediaUrl && (
|
||||
<img
|
||||
src={mediaUrl ?? ""}
|
||||
alt="Uploaded media"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Box className="flex flex-col gap-2 self-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
startIcon={
|
||||
isLoading ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<Save size={16} />
|
||||
)
|
||||
}
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || (!mediaName && !mediaFilename)}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} disabled={isLoading}>
|
||||
Отмена
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Snackbar
|
||||
open={!!error}
|
||||
autoHideDuration={6000}
|
||||
onClose={() => setError(null)}
|
||||
>
|
||||
<Alert severity="error" onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
<Snackbar
|
||||
open={success}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setSuccess(false)}
|
||||
>
|
||||
<Alert severity="success" onClose={() => setSuccess(false)}>
|
||||
Медиа успешно сохранено
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
@ -1,3 +1,4 @@
|
||||
export * from "./SelectArticleDialog";
|
||||
export * from "./SelectMediaDialog";
|
||||
export * from "./PreviewMediaDialog";
|
||||
export * from "./UploadMediaDialog";
|
||||
|
@ -17,6 +17,8 @@ class CityStore {
|
||||
}
|
||||
|
||||
getCities = async () => {
|
||||
if (this.cities.length !== 0) return;
|
||||
|
||||
const response = await authInstance.get("/city");
|
||||
runInAction(() => {
|
||||
this.cities = response.data;
|
||||
|
449
src/shared/store/CreateSightStore/index.tsx
Normal file
449
src/shared/store/CreateSightStore/index.tsx
Normal file
@ -0,0 +1,449 @@
|
||||
// @shared/stores/createSightStore.ts
|
||||
import {
|
||||
Language,
|
||||
authInstance,
|
||||
languageInstance,
|
||||
articlesStore,
|
||||
languageStore,
|
||||
mediaStore,
|
||||
} from "@shared";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
type SightLanguageInfo = {
|
||||
name: string;
|
||||
address: string;
|
||||
left: {
|
||||
heading: string;
|
||||
body: string;
|
||||
media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}[];
|
||||
};
|
||||
right: { heading: string; body: string }[];
|
||||
};
|
||||
|
||||
type SightCommonInfo = {
|
||||
id: number;
|
||||
city_id: number;
|
||||
city: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
thumbnail: string | null;
|
||||
watermark_lu: string | null;
|
||||
watermark_rd: string | null;
|
||||
left_article: number;
|
||||
preview_media: string | null;
|
||||
video_preview: string | null;
|
||||
};
|
||||
|
||||
type SightBaseInfo = SightCommonInfo & {
|
||||
[key in Language]: SightLanguageInfo;
|
||||
};
|
||||
|
||||
class CreateSightStore {
|
||||
sight: SightBaseInfo = {
|
||||
id: 0,
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
thumbnail: null,
|
||||
watermark_lu: null,
|
||||
watermark_rd: null,
|
||||
left_article: 0,
|
||||
preview_media: null,
|
||||
video_preview: null,
|
||||
|
||||
ru: {
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
en: {
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
zh: {
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
|
||||
uploadMediaOpen = false;
|
||||
setUploadMediaOpen = (open: boolean) => {
|
||||
this.uploadMediaOpen = open;
|
||||
};
|
||||
fileToUpload: File | null = null;
|
||||
setFileToUpload = (file: File | null) => {
|
||||
this.fileToUpload = file;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
createNewRightArticle = () => {
|
||||
this.sight.ru.right.push({
|
||||
heading: "Введите русский заголовок",
|
||||
body: "Введите русский текст",
|
||||
});
|
||||
this.sight.en.right.push({
|
||||
heading: "Enter the English heading",
|
||||
body: "Enter the English text",
|
||||
});
|
||||
this.sight.zh.right.push({
|
||||
heading: "Введите китайский заголовок",
|
||||
body: "Введите китайский текст",
|
||||
});
|
||||
};
|
||||
|
||||
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
||||
this.sight[language].left.heading = heading;
|
||||
this.sight[language].left.body = body;
|
||||
};
|
||||
|
||||
clearCreateSight = () => {
|
||||
this.sight = {
|
||||
id: 0,
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
thumbnail: null,
|
||||
watermark_lu: null,
|
||||
watermark_rd: null,
|
||||
left_article: 0,
|
||||
preview_media: null,
|
||||
video_preview: null,
|
||||
|
||||
ru: {
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
|
||||
en: {
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
|
||||
zh: {
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
updateSightInfo = (
|
||||
content: Partial<SightLanguageInfo | SightCommonInfo>,
|
||||
language?: Language
|
||||
) => {
|
||||
if (language) {
|
||||
this.sight[language] = {
|
||||
...this.sight[language],
|
||||
...content,
|
||||
};
|
||||
} else {
|
||||
this.sight = {
|
||||
...this.sight,
|
||||
...content,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
unlinkLeftArticle = () => {
|
||||
this.sight.left_article = 0;
|
||||
this.sight.ru.left.heading = "";
|
||||
this.sight.en.left.heading = "";
|
||||
this.sight.zh.left.heading = "";
|
||||
this.sight.ru.left.body = "";
|
||||
this.sight.en.left.body = "";
|
||||
this.sight.zh.left.body = "";
|
||||
};
|
||||
|
||||
updateLeftArticle = async (articleId: number) => {
|
||||
this.sight.left_article = articleId;
|
||||
|
||||
if (articleId) {
|
||||
const ruArticleData = await languageInstance("ru").get(
|
||||
`/article/${articleId}`
|
||||
);
|
||||
const enArticleData = await languageInstance("en").get(
|
||||
`/article/${articleId}`
|
||||
);
|
||||
const zhArticleData = await languageInstance("zh").get(
|
||||
`/article/${articleId}`
|
||||
);
|
||||
|
||||
this.sight.ru.left.heading = ruArticleData.data.heading;
|
||||
this.sight.en.left.heading = enArticleData.data.heading;
|
||||
this.sight.zh.left.heading = zhArticleData.data.heading;
|
||||
|
||||
this.sight.ru.left.body = ruArticleData.data.body;
|
||||
this.sight.en.left.body = enArticleData.data.body;
|
||||
this.sight.zh.left.body = zhArticleData.data.body;
|
||||
} else {
|
||||
this.sight.left_article = 0;
|
||||
this.sight.ru.left.heading = "";
|
||||
this.sight.en.left.heading = "";
|
||||
this.sight.zh.left.heading = "";
|
||||
this.sight.ru.left.body = "";
|
||||
this.sight.en.left.body = "";
|
||||
this.sight.zh.left.body = "";
|
||||
}
|
||||
};
|
||||
|
||||
deleteLeftArticle = async (articleId: number) => {
|
||||
await authInstance.delete(`/article/${articleId}`);
|
||||
articlesStore.getArticles(languageStore.language);
|
||||
this.sight.left_article = 0;
|
||||
this.sight.ru.left.heading = "";
|
||||
this.sight.en.left.heading = "";
|
||||
this.sight.zh.left.heading = "";
|
||||
this.sight.ru.left.body = "";
|
||||
};
|
||||
|
||||
createLeftArticle = async () => {
|
||||
const response = await languageInstance("ru").post("/article", {
|
||||
heading: "Новая статья",
|
||||
body: "Заполните статью контентом",
|
||||
});
|
||||
|
||||
this.sight.left_article = response.data.id;
|
||||
|
||||
this.sight.ru.left.heading = "Новая статья ";
|
||||
this.sight.en.left.heading = "";
|
||||
this.sight.zh.left.heading = "";
|
||||
this.sight.ru.left.body = "Заполните статью контентом";
|
||||
this.sight.en.left.body = "";
|
||||
this.sight.zh.left.body = "";
|
||||
};
|
||||
|
||||
createSight = async (language: Language) => {
|
||||
const rightArticles: number[] = [];
|
||||
|
||||
if (this.sight.left_article !== 0) {
|
||||
if (this.sight.left_article == 10000000) {
|
||||
const response = await languageInstance("ru").post("/article", {
|
||||
heading: this.sight.ru.left.heading,
|
||||
body: this.sight.ru.left.body,
|
||||
});
|
||||
const { id } = response.data;
|
||||
await languageInstance("en").patch(`/article/${id}`, {
|
||||
heading: this.sight.en.left.heading,
|
||||
body: this.sight.en.left.body,
|
||||
});
|
||||
|
||||
await languageInstance("zh").patch(`/article/${id}`, {
|
||||
heading: this.sight.zh.left.heading,
|
||||
body: this.sight.zh.left.body,
|
||||
});
|
||||
this.sight.left_article = id;
|
||||
} else {
|
||||
await languageInstance("ru").patch(
|
||||
`/article/${this.sight.left_article}`,
|
||||
{
|
||||
heading: this.sight.ru.left.heading,
|
||||
body: this.sight.ru.left.body,
|
||||
}
|
||||
);
|
||||
|
||||
await languageInstance("en").patch(
|
||||
`/article/${this.sight.left_article}`,
|
||||
{
|
||||
heading: this.sight.en.left.heading,
|
||||
body: this.sight.en.left.body,
|
||||
}
|
||||
);
|
||||
|
||||
await languageInstance("zh").patch(
|
||||
`/article/${this.sight.left_article}`,
|
||||
{
|
||||
heading: this.sight.zh.left.heading,
|
||||
body: this.sight.zh.left.body,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.sight[language].right.map(async (article, index) => {
|
||||
try {
|
||||
const response = await languageInstance(language).post("/article", {
|
||||
heading: article.heading,
|
||||
body: article.body,
|
||||
});
|
||||
const { id } = response.data;
|
||||
const anotherLanguages = ["en", "zh", "ru"].filter(
|
||||
(lang) => lang !== language
|
||||
);
|
||||
await languageInstance(anotherLanguages[0] as Language).patch(
|
||||
`/article/${id}`,
|
||||
{
|
||||
heading:
|
||||
this.sight[anotherLanguages[0] as Language].right[index].heading,
|
||||
body: this.sight[anotherLanguages[0] as Language].right[index].body,
|
||||
}
|
||||
);
|
||||
await languageInstance(anotherLanguages[1] as Language).patch(
|
||||
`/article/${id}`,
|
||||
{
|
||||
heading:
|
||||
this.sight[anotherLanguages[1] as Language].right[index].heading,
|
||||
body: this.sight[anotherLanguages[1] as Language].right[index].body,
|
||||
}
|
||||
);
|
||||
rightArticles.push(id);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
const response = await languageInstance(language).post("/sight", {
|
||||
city_id: this.sight.city_id,
|
||||
city: this.sight.city,
|
||||
latitude: this.sight.latitude,
|
||||
longitude: this.sight.longitude,
|
||||
name: this.sight[language].name,
|
||||
address: this.sight[language].address,
|
||||
thumbnail: this.sight.thumbnail ?? null,
|
||||
watermark_lu: this.sight.watermark_lu,
|
||||
watermark_rd: this.sight.watermark_rd,
|
||||
left_article: this.sight.left_article,
|
||||
preview_media: this.sight.preview_media,
|
||||
video_preview: this.sight.video_preview,
|
||||
});
|
||||
|
||||
const { id } = response.data;
|
||||
const anotherLanguages = ["en", "zh", "ru"].filter(
|
||||
(lang) => lang !== language
|
||||
);
|
||||
|
||||
await languageInstance(anotherLanguages[0] as Language).patch(
|
||||
`/sight/${id}`,
|
||||
{
|
||||
city_id: this.sight.city_id,
|
||||
city: this.sight.city,
|
||||
latitude: this.sight.latitude,
|
||||
longitude: this.sight.longitude,
|
||||
name: this.sight[anotherLanguages[0] as Language as Language].name,
|
||||
address:
|
||||
this.sight[anotherLanguages[0] as Language as Language].address,
|
||||
thumbnail: this.sight.thumbnail ?? null,
|
||||
watermark_lu: this.sight.watermark_lu,
|
||||
watermark_rd: this.sight.watermark_rd,
|
||||
left_article: this.sight.left_article,
|
||||
preview_media: this.sight.preview_media,
|
||||
video_preview: this.sight.video_preview,
|
||||
}
|
||||
);
|
||||
await languageInstance(anotherLanguages[1] as Language).patch(
|
||||
`/sight/${id}`,
|
||||
{
|
||||
city_id: this.sight.city_id,
|
||||
city: this.sight.city,
|
||||
latitude: this.sight.latitude,
|
||||
longitude: this.sight.longitude,
|
||||
name: this.sight[anotherLanguages[1] as Language].name,
|
||||
address: this.sight[anotherLanguages[1] as Language].address,
|
||||
thumbnail: this.sight.thumbnail ?? null,
|
||||
watermark_lu: this.sight.watermark_lu,
|
||||
watermark_rd: this.sight.watermark_rd,
|
||||
left_article: this.sight.left_article,
|
||||
preview_media: this.sight.preview_media,
|
||||
video_preview: this.sight.video_preview,
|
||||
}
|
||||
);
|
||||
|
||||
rightArticles.map(async (article, index) => {
|
||||
await authInstance.post(`/sight/${id}/article`, {
|
||||
article_id: article,
|
||||
page_num: index + 1,
|
||||
});
|
||||
});
|
||||
console.log("created");
|
||||
};
|
||||
|
||||
updateRightArticleInfo = (
|
||||
index: number,
|
||||
language: Language,
|
||||
heading: string,
|
||||
body: string
|
||||
) => {
|
||||
this.sight[language].right[index].heading = heading;
|
||||
this.sight[language].right[index].body = body;
|
||||
};
|
||||
|
||||
uploadMedia = async (
|
||||
filename: string,
|
||||
type: number,
|
||||
file: File,
|
||||
media_name?: string
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("filename", filename);
|
||||
if (media_name) {
|
||||
formData.append("media_name", media_name);
|
||||
}
|
||||
formData.append("type", type.toString());
|
||||
try {
|
||||
const response = await authInstance.post(`/media`, formData);
|
||||
this.fileToUpload = null;
|
||||
this.uploadMediaOpen = false;
|
||||
mediaStore.getMedia();
|
||||
return {
|
||||
id: response.data.id,
|
||||
filename: filename,
|
||||
media_name: media_name,
|
||||
media_type: type,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
createLinkWithArticle = async (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}) => {
|
||||
await authInstance.post(`/article/${this.sight.left_article}/media`, {
|
||||
media_id: media.id,
|
||||
media_order: 1,
|
||||
});
|
||||
|
||||
this.sight.ru.left.media.unshift({
|
||||
id: media.id,
|
||||
media_type: media.media_type,
|
||||
filename: media.filename,
|
||||
});
|
||||
|
||||
this.sight.en.left.media.unshift({
|
||||
id: media.id,
|
||||
media_type: media.media_type,
|
||||
filename: media.filename,
|
||||
});
|
||||
|
||||
this.sight.zh.left.media.unshift({
|
||||
id: media.id,
|
||||
media_type: media.media_type,
|
||||
filename: media.filename,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const createSightStore = new CreateSightStore();
|
@ -1,26 +1,31 @@
|
||||
// @shared/stores/editSightStore.ts
|
||||
import { authInstance, Language } from "@shared";
|
||||
import { authInstance, Language, languageInstance, mediaStore } from "@shared";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
export type SightLanguageInfo = {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
left: { heading: string; body: string };
|
||||
left: {
|
||||
heading: string;
|
||||
body: string;
|
||||
media: { id: string; media_type: number; filename: string }[];
|
||||
};
|
||||
right: { heading: string; body: string }[];
|
||||
};
|
||||
|
||||
export type SightCommonInfo = {
|
||||
id: number;
|
||||
city_id: number;
|
||||
city: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
thumbnail: string;
|
||||
watermark_lu: string;
|
||||
watermark_rd: string;
|
||||
thumbnail: string | null;
|
||||
watermark_lu: string | null;
|
||||
watermark_rd: string | null;
|
||||
left_article: number;
|
||||
preview_media: string;
|
||||
video_preview: string;
|
||||
preview_media: string | null;
|
||||
video_preview: string | null;
|
||||
};
|
||||
|
||||
export type SightBaseInfo = {
|
||||
@ -31,36 +36,37 @@ export type SightBaseInfo = {
|
||||
class EditSightStore {
|
||||
sight: SightBaseInfo = {
|
||||
common: {
|
||||
id: 0,
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
thumbnail: "",
|
||||
watermark_lu: "",
|
||||
watermark_rd: "",
|
||||
thumbnail: null,
|
||||
watermark_lu: null,
|
||||
watermark_rd: null,
|
||||
left_article: 0,
|
||||
preview_media: "",
|
||||
video_preview: "",
|
||||
preview_media: null,
|
||||
video_preview: null,
|
||||
},
|
||||
ru: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
en: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
zh: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
@ -77,6 +83,9 @@ class EditSightStore {
|
||||
|
||||
const response = await authInstance.get(`/sight/${id}`);
|
||||
const data = response.data;
|
||||
if (data.left_article != 0) {
|
||||
await this.getLeftArticle(data.left_article);
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
// Обновляем языковую часть
|
||||
@ -101,25 +110,62 @@ class EditSightStore {
|
||||
this.sight[language].left.body = body;
|
||||
};
|
||||
|
||||
getRightArticles = async (id: number) => {
|
||||
const responseRu = await languageInstance("ru").get(`/sight/${id}/article`);
|
||||
const responseEn = await languageInstance("en").get(`/sight/${id}/article`);
|
||||
const responseZh = await languageInstance("zh").get(`/sight/${id}/article`);
|
||||
|
||||
const data = {
|
||||
ru: {
|
||||
right: responseRu.data,
|
||||
},
|
||||
en: {
|
||||
right: responseEn.data,
|
||||
},
|
||||
zh: {
|
||||
right: responseZh.data,
|
||||
},
|
||||
};
|
||||
runInAction(() => {
|
||||
this.sight = {
|
||||
...this.sight,
|
||||
ru: {
|
||||
...this.sight.ru,
|
||||
right: data.ru.right,
|
||||
},
|
||||
en: {
|
||||
...this.sight.en,
|
||||
right: data.en.right,
|
||||
},
|
||||
|
||||
zh: {
|
||||
...this.sight.zh,
|
||||
right: data.zh.right,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
clearSightInfo = () => {
|
||||
this.sight = {
|
||||
common: {
|
||||
id: 0,
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
thumbnail: "",
|
||||
watermark_lu: "",
|
||||
watermark_rd: "",
|
||||
thumbnail: null,
|
||||
watermark_lu: null,
|
||||
watermark_rd: null,
|
||||
left_article: 0,
|
||||
preview_media: "",
|
||||
video_preview: "",
|
||||
preview_media: null,
|
||||
video_preview: null,
|
||||
},
|
||||
ru: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
|
||||
@ -127,7 +173,7 @@ class EditSightStore {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
|
||||
@ -135,7 +181,7 @@ class EditSightStore {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
left: { heading: "", body: "", media: [] },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
@ -158,6 +204,244 @@ class EditSightStore {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
unlinkLeftArticle = async () => {
|
||||
this.sight.common.left_article = 0;
|
||||
this.sight.ru.left.heading = "";
|
||||
this.sight.en.left.heading = "";
|
||||
this.sight.zh.left.heading = "";
|
||||
this.sight.ru.left.body = "";
|
||||
this.sight.en.left.body = "";
|
||||
this.sight.zh.left.body = "";
|
||||
};
|
||||
|
||||
updateSight = async () => {
|
||||
let createdLeftArticleId = this.sight.common.left_article;
|
||||
|
||||
if (this.sight.common.left_article == 10000000) {
|
||||
const response = await languageInstance("ru").post(`/article`, {
|
||||
heading: this.sight.ru.left.heading,
|
||||
body: this.sight.ru.left.body,
|
||||
});
|
||||
createdLeftArticleId = response.data.id;
|
||||
await languageInstance("en").patch(`/article/${createdLeftArticleId}`, {
|
||||
heading: this.sight.en.left.heading,
|
||||
body: this.sight.en.left.body,
|
||||
});
|
||||
|
||||
await languageInstance("zh").patch(`/article/${createdLeftArticleId}`, {
|
||||
heading: this.sight.zh.left.heading,
|
||||
body: this.sight.zh.left.body,
|
||||
});
|
||||
|
||||
this.sight.common.left_article = createdLeftArticleId;
|
||||
} else if (this.sight.common.left_article != 0) {
|
||||
await languageInstance("ru").patch(
|
||||
`/article/${this.sight.common.left_article}`,
|
||||
{
|
||||
heading: this.sight.ru.left.heading,
|
||||
body: this.sight.ru.left.body,
|
||||
}
|
||||
);
|
||||
await languageInstance("en").patch(
|
||||
`/article/${this.sight.common.left_article}`,
|
||||
{
|
||||
heading: this.sight.en.left.heading,
|
||||
body: this.sight.en.left.body,
|
||||
}
|
||||
);
|
||||
|
||||
await languageInstance("zh").patch(
|
||||
`/article/${this.sight.common.left_article}`,
|
||||
{
|
||||
heading: this.sight.zh.left.heading,
|
||||
body: this.sight.zh.left.body,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await languageInstance("ru").patch(`/sight/${this.sight.common.id}`, {
|
||||
...this.sight.common,
|
||||
name: this.sight.ru.name,
|
||||
address: this.sight.ru.address,
|
||||
left_article: createdLeftArticleId,
|
||||
});
|
||||
await languageInstance("en").patch(`/sight/${this.sight.common.id}`, {
|
||||
...this.sight.common,
|
||||
name: this.sight.en.name,
|
||||
address: this.sight.en.address,
|
||||
left_article: createdLeftArticleId,
|
||||
});
|
||||
await languageInstance("zh").patch(`/sight/${this.sight.common.id}`, {
|
||||
...this.sight.common,
|
||||
name: this.sight.zh.name,
|
||||
address: this.sight.zh.address,
|
||||
left_article: createdLeftArticleId,
|
||||
});
|
||||
|
||||
if (this.sight.common.left_article == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// await languageInstance("ru").patch(
|
||||
// `/sight/${this.sight.common.left_article}/article`,
|
||||
// {
|
||||
// heading: this.sight.ru.left.heading,
|
||||
// body: this.sight.ru.left.body,
|
||||
// }
|
||||
// );
|
||||
// await languageInstance("en").patch(
|
||||
// `/sight/${this.sight.common.left_article}/article`,
|
||||
// {
|
||||
// heading: this.sight.en.left.heading,
|
||||
// body: this.sight.en.left.body,
|
||||
// }
|
||||
// );
|
||||
// await languageInstance("zh").patch(
|
||||
// `/sight/${this.sight.common.left_article}/article`,
|
||||
// {
|
||||
// heading: this.sight.zh.left.heading,
|
||||
// body: this.sight.zh.left.body,
|
||||
// }
|
||||
// );
|
||||
};
|
||||
|
||||
getLeftArticle = async (id: number) => {
|
||||
const response = await languageInstance("ru").get(`/article/${id}`);
|
||||
const responseEn = await languageInstance("en").get(`/article/${id}`);
|
||||
const responseZh = await languageInstance("zh").get(`/article/${id}`);
|
||||
const mediaIds = await authInstance.get(`/article/${id}/media`);
|
||||
runInAction(() => {
|
||||
this.sight.ru.left = {
|
||||
heading: response.data.heading,
|
||||
body: response.data.body,
|
||||
media: mediaIds.data,
|
||||
};
|
||||
this.sight.en.left = {
|
||||
heading: responseEn.data.heading,
|
||||
body: responseEn.data.body,
|
||||
media: mediaIds.data,
|
||||
};
|
||||
this.sight.zh.left = {
|
||||
heading: responseZh.data.heading,
|
||||
body: responseZh.data.body,
|
||||
media: mediaIds.data,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
deleteLeftArticle = async (id: number) => {
|
||||
await authInstance.delete(`/article/${id}`);
|
||||
this.sight.common.left_article = 0;
|
||||
this.sight.ru.left.heading = "";
|
||||
this.sight.en.left.heading = "";
|
||||
this.sight.zh.left.heading = "";
|
||||
this.sight.ru.left.body = "";
|
||||
this.sight.en.left.body = "";
|
||||
this.sight.zh.left.body = "";
|
||||
};
|
||||
|
||||
createLeftArticle = async () => {
|
||||
const response = await languageInstance("ru").post(`/article`, {
|
||||
heading: "",
|
||||
body: "",
|
||||
});
|
||||
|
||||
this.sight.common.left_article = response.data.id;
|
||||
|
||||
this.sight.ru.left.heading = "";
|
||||
this.sight.en.left.heading = "";
|
||||
this.sight.zh.left.heading = "";
|
||||
this.sight.ru.left.body = "";
|
||||
this.sight.en.left.body = "";
|
||||
this.sight.zh.left.body = "";
|
||||
};
|
||||
|
||||
deleteMedia = async (article_id: number, media_id: string) => {
|
||||
await authInstance.delete(`/article/${article_id}/media`, {
|
||||
data: {
|
||||
media_id: media_id,
|
||||
},
|
||||
});
|
||||
|
||||
this.sight.ru.left.media = this.sight.ru.left.media.filter(
|
||||
(media) => media.id !== media_id
|
||||
);
|
||||
this.sight.en.left.media = this.sight.en.left.media.filter(
|
||||
(media) => media.id !== media_id
|
||||
);
|
||||
this.sight.zh.left.media = this.sight.zh.left.media.filter(
|
||||
(media) => media.id !== media_id
|
||||
);
|
||||
};
|
||||
|
||||
uploadMediaOpen = false;
|
||||
setUploadMediaOpen = (open: boolean) => {
|
||||
this.uploadMediaOpen = open;
|
||||
};
|
||||
fileToUpload: File | null = null;
|
||||
setFileToUpload = (file: File | null) => {
|
||||
this.fileToUpload = file;
|
||||
};
|
||||
uploadMedia = async (
|
||||
filename: string,
|
||||
type: number,
|
||||
file: File,
|
||||
media_name?: string
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("filename", filename);
|
||||
if (media_name) {
|
||||
formData.append("media_name", media_name);
|
||||
}
|
||||
formData.append("type", type.toString());
|
||||
try {
|
||||
const response = await authInstance.post(`/media`, formData);
|
||||
this.fileToUpload = null;
|
||||
this.uploadMediaOpen = false;
|
||||
mediaStore.getMedia();
|
||||
return {
|
||||
id: response.data.id,
|
||||
filename: filename,
|
||||
media_name: media_name,
|
||||
media_type: type,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
createLinkWithArticle = async (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}) => {
|
||||
await authInstance.post(
|
||||
`/article/${this.sight.common.left_article}/media`,
|
||||
{
|
||||
media_id: media.id,
|
||||
media_order: 1,
|
||||
}
|
||||
);
|
||||
|
||||
this.sight.ru.left.media.unshift({
|
||||
id: media.id,
|
||||
media_type: media.media_type,
|
||||
filename: media.filename,
|
||||
});
|
||||
this.sight.en.left.media.unshift({
|
||||
id: media.id,
|
||||
media_type: media.media_type,
|
||||
filename: media.filename,
|
||||
});
|
||||
this.sight.zh.left.media.unshift({
|
||||
id: media.id,
|
||||
media_type: media.media_type,
|
||||
filename: media.filename,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const editSightStore = new EditSightStore();
|
||||
|
@ -8,3 +8,4 @@ export * from "./CityStore";
|
||||
export * from "./ArticlesStore";
|
||||
export * from "./EditSightStore";
|
||||
export * from "./MediaStore";
|
||||
export * from "./CreateSightStore";
|
||||
|
135
src/widgets/MediaArea/index.tsx
Normal file
135
src/widgets/MediaArea/index.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { PreviewMediaDialog } from "@shared";
|
||||
import { X, Upload } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState, DragEvent, useRef } from "react";
|
||||
|
||||
export const MediaArea = observer(
|
||||
({
|
||||
articleId,
|
||||
mediaIds,
|
||||
deleteMedia,
|
||||
onFilesDrop, // 👈 Проп для обработки загруженных файлов
|
||||
setSelectMediaDialogOpen,
|
||||
}: {
|
||||
articleId: number;
|
||||
mediaIds: { id: string; media_type: number; filename: string }[];
|
||||
deleteMedia: (id: number, media_id: string) => void;
|
||||
onFilesDrop?: (files: File[]) => void;
|
||||
setSelectMediaDialogOpen: (open: boolean) => void;
|
||||
}) => {
|
||||
const [mediaModal, setMediaModal] = useState<boolean>(false);
|
||||
const [mediaId, setMediaId] = useState<string>("");
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleMediaModal = (mediaId: string) => {
|
||||
setMediaModal(true);
|
||||
setMediaId(mediaId);
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length && onFilesDrop) {
|
||||
onFilesDrop(files);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
if (files.length && onFilesDrop) {
|
||||
onFilesDrop(files);
|
||||
}
|
||||
// Сбрасываем значение input, чтобы можно было выбрать тот же файл снова
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileSelect}
|
||||
accept="image/*,video/*,.glb,.gltf"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<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 ${
|
||||
isDragging ? "bg-blue-100 border-blue-400" : ""
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Upload size={32} className="mb-2" />
|
||||
Перетащите медиа файлы сюда или нажмите для выбора
|
||||
</div>
|
||||
<div>или</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setSelectMediaDialogOpen(true)}
|
||||
>
|
||||
Выбрать существующие медиа файлы
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-start flex-wrap gap-2 mt-4">
|
||||
{mediaIds.map((m) => (
|
||||
<button
|
||||
className="relative w-40 h-40"
|
||||
key={m.id}
|
||||
onClick={() => handleMediaModal(m.id)}
|
||||
>
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: m.id,
|
||||
media_type: m.media_type,
|
||||
filename: m.filename,
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="absolute top-2 right-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteMedia(articleId, m.id);
|
||||
}}
|
||||
>
|
||||
<X size={16} color="red" />
|
||||
</button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<PreviewMediaDialog
|
||||
open={mediaModal}
|
||||
onClose={() => setMediaModal(false)}
|
||||
mediaId={mediaId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
24
src/widgets/ModelViewer3D/index.tsx
Normal file
24
src/widgets/ModelViewer3D/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Stage, useGLTF } from "@react-three/drei";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
|
||||
export const ModelViewer3D = ({
|
||||
fileUrl,
|
||||
height = "100%",
|
||||
}: {
|
||||
fileUrl: string;
|
||||
height: string;
|
||||
}) => {
|
||||
const { scene } = useGLTF(fileUrl);
|
||||
|
||||
return (
|
||||
<Canvas style={{ width: "100%", height: height }}>
|
||||
<ambientLight />
|
||||
<directionalLight />
|
||||
<Stage environment="city" intensity={0.6}>
|
||||
<primitive object={scene} />
|
||||
</Stage>
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
};
|
@ -24,6 +24,14 @@ const StyledMarkdownEditor = styled("div")(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
borderColor: theme.palette.divider,
|
||||
height: "auto",
|
||||
minHeight: "200px",
|
||||
maxHeight: "500px",
|
||||
overflow: "auto",
|
||||
},
|
||||
"& .CodeMirror-scroll": {
|
||||
minHeight: "200px",
|
||||
maxHeight: "500px",
|
||||
},
|
||||
// Стили для текста в редакторе
|
||||
"& .CodeMirror-selected": {
|
||||
|
158
src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx
Normal file
158
src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
// import { Box, Button, Paper, Typography } from "@mui/material";
|
||||
// import { X, Upload } from "lucide-react";
|
||||
// import { useCallback, useState } from "react";
|
||||
// import { useDropzone } from "react-dropzone";
|
||||
// import { UploadMediaDialog } from "@shared";
|
||||
// import { createSightStore } from "@shared";
|
||||
|
||||
// interface MediaUploadBoxProps {
|
||||
// title: string;
|
||||
// tooltip?: string;
|
||||
// mediaId: string | null;
|
||||
// onMediaSelect: (mediaId: string) => void;
|
||||
// onMediaRemove: () => void;
|
||||
// onPreviewClick: (mediaId: string) => void;
|
||||
// token: string;
|
||||
// type: "thumbnail" | "watermark_lu" | "watermark_rd";
|
||||
// }
|
||||
|
||||
// export const MediaUploadBox = ({
|
||||
// title,
|
||||
// tooltip,
|
||||
// mediaId,
|
||||
// onMediaSelect,
|
||||
// onMediaRemove,
|
||||
// onPreviewClick,
|
||||
// token,
|
||||
// type,
|
||||
// }: MediaUploadBoxProps) => {
|
||||
// const [uploadMediaOpen, setUploadMediaOpen] = useState(false);
|
||||
// const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||
|
||||
// const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
// if (acceptedFiles.length > 0) {
|
||||
// setFileToUpload(acceptedFiles[0]);
|
||||
// setUploadMediaOpen(true);
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
// onDrop,
|
||||
// accept: {
|
||||
// "image/*": [".png", ".jpg", ".jpeg", ".gif"],
|
||||
// },
|
||||
// multiple: false,
|
||||
// });
|
||||
|
||||
// const handleUploadComplete = async (media: {
|
||||
// id: string;
|
||||
// filename: string;
|
||||
// media_name?: string;
|
||||
// media_type: number;
|
||||
// }) => {
|
||||
// onMediaSelect(media.id);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <Paper
|
||||
// elevation={2}
|
||||
// sx={{
|
||||
// padding: 2,
|
||||
// display: "flex",
|
||||
// flexDirection: "column",
|
||||
// alignItems: "center",
|
||||
// gap: 1,
|
||||
// flex: 1,
|
||||
// minWidth: 150,
|
||||
// }}
|
||||
// >
|
||||
// <Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
// <Typography variant="subtitle2" gutterBottom sx={{ mb: 0, mr: 0.5 }}>
|
||||
// {title}
|
||||
// </Typography>
|
||||
// </Box>
|
||||
// <Box
|
||||
// {...getRootProps()}
|
||||
// sx={{
|
||||
// position: "relative",
|
||||
// width: "200px",
|
||||
// height: "200px",
|
||||
// display: "flex",
|
||||
// alignItems: "center",
|
||||
// justifyContent: "center",
|
||||
// borderRadius: 1,
|
||||
// mb: 1,
|
||||
// cursor: mediaId ? "pointer" : "default",
|
||||
// border: isDragActive ? "2px dashed #1976d2" : "none",
|
||||
// backgroundColor: isDragActive
|
||||
// ? "rgba(25, 118, 210, 0.04)"
|
||||
// : "transparent",
|
||||
// transition: "all 0.2s ease",
|
||||
// }}
|
||||
// >
|
||||
// <input {...getInputProps()} />
|
||||
// {mediaId && (
|
||||
// <button
|
||||
// className="absolute top-2 right-2 z-10"
|
||||
// onClick={(e) => {
|
||||
// e.stopPropagation();
|
||||
// onMediaRemove();
|
||||
// }}
|
||||
// >
|
||||
// <X color="red" />
|
||||
// </button>
|
||||
// )}
|
||||
// {mediaId ? (
|
||||
// <img
|
||||
// src={`${
|
||||
// import.meta.env.VITE_KRBL_MEDIA
|
||||
// }${mediaId}/download?token=${token}`}
|
||||
// alt={title}
|
||||
// style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
// onClick={(e) => {
|
||||
// e.stopPropagation();
|
||||
// onPreviewClick(mediaId);
|
||||
// }}
|
||||
// />
|
||||
// ) : (
|
||||
// <div className="w-full flex flex-col items-center justify-center gap-3">
|
||||
// <div
|
||||
// className={`w-full h-20 border rounded-md flex flex-col items-center transition-all duration-300 justify-center border-dashed ${
|
||||
// isDragActive
|
||||
// ? "border-blue-500 bg-blue-50"
|
||||
// : "border-gray-300"
|
||||
// } cursor-pointer hover:bg-gray-100`}
|
||||
// >
|
||||
// <Upload size={24} className="mb-2" />
|
||||
// <p>
|
||||
// {isDragActive ? "Отпустите файл здесь" : "Перетащите файл"}
|
||||
// </p>
|
||||
// </div>
|
||||
// <p>или</p>
|
||||
// <Button
|
||||
// variant="contained"
|
||||
// color="primary"
|
||||
// onClick={(e) => {
|
||||
// e.stopPropagation();
|
||||
// onMediaSelect("");
|
||||
// }}
|
||||
// >
|
||||
// Выбрать файл
|
||||
// </Button>
|
||||
// </div>
|
||||
// )}
|
||||
// </Box>
|
||||
// </Paper>
|
||||
|
||||
// <UploadMediaDialog
|
||||
// open={uploadMediaOpen}
|
||||
// onClose={() => {
|
||||
// setUploadMediaOpen(false);
|
||||
// setFileToUpload(null);
|
||||
// }}
|
||||
// afterUpload={handleUploadComplete}
|
||||
// />
|
||||
// </>
|
||||
// );
|
||||
// };
|
582
src/widgets/SightTabs/CreateInformationTab/index.tsx
Normal file
582
src/widgets/SightTabs/CreateInformationTab/index.tsx
Normal file
@ -0,0 +1,582 @@
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
Box,
|
||||
Autocomplete,
|
||||
Typography,
|
||||
Paper,
|
||||
Tooltip,
|
||||
MenuItem,
|
||||
Menu as MuiMenu,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
BackButton,
|
||||
TabPanel,
|
||||
languageStore,
|
||||
Language,
|
||||
cityStore,
|
||||
SelectMediaDialog,
|
||||
PreviewMediaDialog,
|
||||
SightLanguageInfo,
|
||||
SightCommonInfo,
|
||||
createSightStore,
|
||||
} from "@shared";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { Info, X } from "lucide-react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
// Мокап для всплывающей подсказки
|
||||
|
||||
export const CreateInformationTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const { cities } = cityStore;
|
||||
const [, setIsMediaModalOpen] = useState(false);
|
||||
const [mediaId, setMediaId] = useState<string>("");
|
||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||
|
||||
const { language } = languageStore;
|
||||
const { sight, updateSightInfo, createSight } = createSightStore;
|
||||
|
||||
const data = sight[language];
|
||||
|
||||
const [, setCity] = useState<number>(sight.city_id ?? 0);
|
||||
const [coordinates, setCoordinates] = useState<string>(`0 0`);
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
// Menu state for each media button
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [activeMenuType, setActiveMenuType] = useState<
|
||||
"thumbnail" | "watermark_lu" | "watermark_rd" | null
|
||||
>(null);
|
||||
const [isAddMediaOpen, setIsAddMediaOpen] = useState(false);
|
||||
|
||||
// const handleMenuOpen = (
|
||||
// event: React.MouseEvent<HTMLElement>,
|
||||
// type: "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
// ) => {
|
||||
// setMenuAnchorEl(event.currentTarget);
|
||||
// setActiveMenuType(type);
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
// Показывать только при инициализации (не менять при ошибках пользователя)
|
||||
if (sight.latitude !== 0 || sight.longitude !== 0) {
|
||||
setCoordinates(`${sight.latitude} ${sight.longitude}`);
|
||||
}
|
||||
// если координаты обнулились — оставить поле как есть
|
||||
}, [sight.latitude, sight.longitude]);
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setMenuAnchorEl(null);
|
||||
setActiveMenuType(null);
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleAddMedia = () => {
|
||||
setIsAddMediaOpen(true);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
content: Partial<SightLanguageInfo | SightCommonInfo>,
|
||||
language?: Language
|
||||
) => {
|
||||
if (language) {
|
||||
updateSightInfo(content, language);
|
||||
} else {
|
||||
updateSightInfo(content);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMediaSelect = (
|
||||
media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
},
|
||||
type: "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
) => {
|
||||
handleChange({
|
||||
[type]: media.id,
|
||||
});
|
||||
setActiveMenuType(null);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TabPanel value={value} index={index}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 3,
|
||||
position: "relative",
|
||||
paddingBottom: "70px" /* Space for save button */,
|
||||
}}
|
||||
>
|
||||
<BackButton />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
||||
gap: 4, // Added gap between the two main columns
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Left column with main fields */}
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
width: "80%",
|
||||
flexDirection: "column",
|
||||
gap: 2.5,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label={`Название (${language.toUpperCase()})`}
|
||||
value={data.name}
|
||||
onChange={(e) => {
|
||||
handleChange(
|
||||
{
|
||||
name: e.target.value,
|
||||
},
|
||||
language
|
||||
);
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Адрес"
|
||||
value={data.address}
|
||||
onChange={(e) => {
|
||||
handleChange(
|
||||
{
|
||||
address: e.target.value,
|
||||
},
|
||||
language
|
||||
);
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<Autocomplete
|
||||
options={cities ?? []}
|
||||
value={
|
||||
cities.find((city) => city.id === sight.city_id) ?? null
|
||||
}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(_, value) => {
|
||||
setCity(value?.id ?? 0);
|
||||
handleChange({
|
||||
city_id: value?.id ?? 0,
|
||||
});
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Город" />
|
||||
)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Координаты"
|
||||
value={coordinates}
|
||||
onChange={(e) => {
|
||||
const input = e.target.value;
|
||||
setCoordinates(input); // показываем как есть
|
||||
|
||||
const [latStr, lonStr] = input.split(/\s+/); // учитываем любые пробелы
|
||||
|
||||
const lat = parseFloat(latStr);
|
||||
const lon = parseFloat(lonStr);
|
||||
|
||||
// Проверка, что обе координаты валидные числа
|
||||
const isValidLat = !isNaN(lat);
|
||||
const isValidLon = !isNaN(lon);
|
||||
|
||||
if (isValidLat && isValidLon) {
|
||||
handleChange({
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
});
|
||||
} else {
|
||||
handleChange(
|
||||
{
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
},
|
||||
language
|
||||
);
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
placeholder="Введите координаты в формате: широта долгота"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-around",
|
||||
width: "80%",
|
||||
gap: 2,
|
||||
flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
padding: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
flex: 1,
|
||||
minWidth: 150, // Ensure a minimum width
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
gutterBottom
|
||||
sx={{ mb: 0, mr: 0.5 }}
|
||||
>
|
||||
Логотип
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "200px",
|
||||
height: "200px",
|
||||
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
cursor: sight.thumbnail ? "pointer" : "default",
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsMediaModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{sight.thumbnail && (
|
||||
<button
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => {
|
||||
handleChange({
|
||||
thumbnail: null,
|
||||
});
|
||||
setActiveMenuType(null);
|
||||
}}
|
||||
>
|
||||
<X color="red" />
|
||||
</button>
|
||||
)}
|
||||
{sight.thumbnail ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
sight.thumbnail
|
||||
}/download?token=${token}`}
|
||||
alt="Логотип"
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onClick={() => {
|
||||
setIsPreviewMediaOpen(true);
|
||||
setMediaId(sight.thumbnail ?? "");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full flex flex-col items-center justify-center gap-3">
|
||||
<div className="w-full h-20 border rounded-md flex flex-col items-center transition-all duration-300 justify-center border-dashed border-gray-300 cursor-pointer hover:bg-gray-100">
|
||||
<p>Перетащите файл</p>
|
||||
</div>
|
||||
<p>или</p>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setIsAddMediaOpen(true);
|
||||
setActiveMenuType("thumbnail");
|
||||
}}
|
||||
>
|
||||
Выбрать файл
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
padding: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
flex: 1,
|
||||
minWidth: 150, // Ensure a minimum width
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
gutterBottom
|
||||
sx={{ mb: 0, mr: 0.5 }}
|
||||
>
|
||||
Водяной знак (л.в)
|
||||
</Typography>
|
||||
<Tooltip title={"asf"}>
|
||||
<Info
|
||||
size={16}
|
||||
color="gray"
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "200px",
|
||||
height: "200px",
|
||||
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
cursor: sight.watermark_lu ? "pointer" : "default",
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsMediaModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{sight.watermark_lu && (
|
||||
<button
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => {
|
||||
handleChange({
|
||||
watermark_lu: null,
|
||||
});
|
||||
setActiveMenuType(null);
|
||||
}}
|
||||
>
|
||||
<X color="red" />
|
||||
</button>
|
||||
)}
|
||||
{sight.watermark_lu ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
sight.watermark_lu
|
||||
}/download?token=${token}`}
|
||||
alt="Логотип"
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onClick={() => {
|
||||
setIsPreviewMediaOpen(true);
|
||||
setMediaId(sight.watermark_lu ?? "");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full flex flex-col items-center justify-center gap-3">
|
||||
<div className="w-full h-20 border rounded-md flex flex-col items-center transition-all duration-300 justify-center border-dashed border-gray-300 cursor-pointer hover:bg-gray-100">
|
||||
<p>Перетащите файл</p>
|
||||
</div>
|
||||
<p>или</p>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setActiveMenuType("watermark_lu");
|
||||
setIsAddMediaOpen(true);
|
||||
}}
|
||||
>
|
||||
Выбрать файл
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
padding: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
flex: 1,
|
||||
minWidth: 150, // Ensure a minimum width
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
gutterBottom
|
||||
sx={{ mb: 0, mr: 0.5 }}
|
||||
>
|
||||
Водяной знак (п.в)
|
||||
</Typography>
|
||||
<Tooltip title={"asfaf"}>
|
||||
<Info
|
||||
size={16}
|
||||
color="gray"
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "200px",
|
||||
height: "200px",
|
||||
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
cursor: sight.watermark_rd ? "pointer" : "default",
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsMediaModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{sight.watermark_rd && (
|
||||
<button
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => {
|
||||
handleChange({
|
||||
watermark_rd: null,
|
||||
});
|
||||
setActiveMenuType(null);
|
||||
}}
|
||||
>
|
||||
<X color="red" />
|
||||
</button>
|
||||
)}
|
||||
{sight.watermark_rd ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
sight.watermark_rd
|
||||
}/download?token=${token}`}
|
||||
alt="Логотип"
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onClick={() => {
|
||||
setIsPreviewMediaOpen(true);
|
||||
setMediaId(sight.watermark_rd ?? "");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full flex flex-col items-center justify-center gap-3">
|
||||
<div className="w-full h-20 border rounded-md flex flex-col items-center transition-all duration-300 justify-center border-dashed border-gray-300 cursor-pointer hover:bg-gray-100">
|
||||
<p>Перетащите файл</p>
|
||||
</div>
|
||||
<p>или</p>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setActiveMenuType("watermark_rd");
|
||||
setIsAddMediaOpen(true);
|
||||
}}
|
||||
>
|
||||
Выбрать файл
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* LanguageSwitcher positioned at the top right */}
|
||||
|
||||
<LanguageSwitcher />
|
||||
|
||||
{/* Save Button fixed at the bottom right */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: 2,
|
||||
backgroundColor: "background.paper", // To ensure it stands out over content
|
||||
width: "100%", // Take full width to cover content below it
|
||||
display: "flex",
|
||||
justifyContent: "flex-end", // Align to the right
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={async () => {
|
||||
await createSight(language);
|
||||
toast.success("Достопримечательность создана");
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* Media Menu */}
|
||||
<MuiMenu
|
||||
anchorEl={menuAnchorEl}
|
||||
open={Boolean(menuAnchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={handleCreateNew}>Создать новую</MenuItem>
|
||||
<MenuItem onClick={handleAddMedia}>Выбрать существующую</MenuItem>
|
||||
</MuiMenu>
|
||||
|
||||
<SelectMediaDialog
|
||||
open={isAddMediaOpen}
|
||||
onClose={() => {
|
||||
setIsAddMediaOpen(false);
|
||||
setActiveMenuType(null);
|
||||
}}
|
||||
onSelectMedia={(media) => {
|
||||
handleMediaSelect(media, activeMenuType ?? "thumbnail");
|
||||
}}
|
||||
/>
|
||||
|
||||
<PreviewMediaDialog
|
||||
open={isPreviewMediaOpen}
|
||||
onClose={() => setIsPreviewMediaOpen(false)}
|
||||
mediaId={mediaId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
451
src/widgets/SightTabs/CreateLeftTab/index.tsx
Normal file
451
src/widgets/SightTabs/CreateLeftTab/index.tsx
Normal file
@ -0,0 +1,451 @@
|
||||
// @widgets/LeftWidgetTab.tsx
|
||||
import { Box, Button, TextField, Paper, Typography } from "@mui/material";
|
||||
import {
|
||||
BackButton,
|
||||
TabPanel,
|
||||
languageStore,
|
||||
SelectMediaDialog,
|
||||
editSightStore,
|
||||
createSightStore,
|
||||
SelectArticleModal,
|
||||
UploadMediaDialog,
|
||||
} from "@shared";
|
||||
import {
|
||||
LanguageSwitcher,
|
||||
MediaArea,
|
||||
ReactMarkdownComponent,
|
||||
ReactMarkdownEditor,
|
||||
MediaViewer,
|
||||
} from "@widgets";
|
||||
import { Trash2, ImagePlus } from "lucide-react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const CreateLeftTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const {
|
||||
sight,
|
||||
updateSightInfo,
|
||||
updateLeftArticle,
|
||||
createSight,
|
||||
deleteLeftArticle,
|
||||
createLeftArticle,
|
||||
unlinkLeftArticle,
|
||||
createLinkWithArticle,
|
||||
} = createSightStore;
|
||||
const {
|
||||
deleteMedia,
|
||||
setFileToUpload,
|
||||
uploadMediaOpen,
|
||||
setUploadMediaOpen,
|
||||
} = editSightStore;
|
||||
|
||||
const { language } = languageStore;
|
||||
|
||||
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
|
||||
useState(false);
|
||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
// const handleMediaSelected = useCallback(() => {
|
||||
// // При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА
|
||||
// // сохраняя текущие heading и body.
|
||||
// updateSightInfo(language, {
|
||||
// left: {
|
||||
// heading: data.left.heading,
|
||||
// body: data.left.body,
|
||||
// },
|
||||
// });
|
||||
// setIsSelectMediaDialogOpen(false);
|
||||
// }, [language, data.left.heading, data.left.body]);
|
||||
|
||||
const handleCloseArticleDialog = useCallback(() => {
|
||||
setIsSelectArticleDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleCloseMediaDialog = useCallback(() => {
|
||||
setIsSelectMediaDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleMediaSelected = useCallback(
|
||||
async (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}) => {
|
||||
await createLinkWithArticle(media);
|
||||
setIsSelectMediaDialogOpen(false);
|
||||
},
|
||||
[createLinkWithArticle]
|
||||
);
|
||||
|
||||
const handleArticleSelect = useCallback(
|
||||
(articleId: number) => {
|
||||
updateLeftArticle(articleId);
|
||||
},
|
||||
[updateLeftArticle]
|
||||
);
|
||||
|
||||
return (
|
||||
<TabPanel value={value} index={index}>
|
||||
<LanguageSwitcher />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 3,
|
||||
paddingBottom: "70px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<BackButton />
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingX: 2.5,
|
||||
paddingY: 1.5,
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Левая статья</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
{sight.left_article ? (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
style={{ transition: "0" }}
|
||||
onClick={() => {
|
||||
unlinkLeftArticle();
|
||||
toast.success("Статья откреплена");
|
||||
}}
|
||||
>
|
||||
Открепить
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
style={{ transition: "0" }}
|
||||
startIcon={<Trash2 size={18} />}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
deleteLeftArticle(sight.left_article);
|
||||
toast.success("Статья откреплена");
|
||||
}}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||
>
|
||||
Выбрать статью
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
style={{ transition: "0" }}
|
||||
onClick={createLeftArticle}
|
||||
>
|
||||
Создать статью
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
{sight.left_article > 0 && (
|
||||
<>
|
||||
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
|
||||
{/* Левая колонка: Редактирование */}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Название информации"
|
||||
value={sight[language].left.heading}
|
||||
onChange={(e) =>
|
||||
updateSightInfo(
|
||||
{
|
||||
left: {
|
||||
heading: e.target.value,
|
||||
body: sight[language].left.body,
|
||||
media: sight[language].left.media,
|
||||
},
|
||||
},
|
||||
language
|
||||
)
|
||||
}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<ReactMarkdownEditor
|
||||
value={sight[language].left.body}
|
||||
onChange={(value) =>
|
||||
updateSightInfo(
|
||||
{
|
||||
left: {
|
||||
heading: sight[language].left.heading,
|
||||
body: value,
|
||||
media: sight[language].left.media,
|
||||
},
|
||||
},
|
||||
language
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<MediaArea
|
||||
articleId={sight.left_article}
|
||||
mediaIds={sight[language].left.media}
|
||||
deleteMedia={deleteMedia}
|
||||
setSelectMediaDialogOpen={setIsSelectMediaDialogOpen}
|
||||
onFilesDrop={(files) => {
|
||||
setFileToUpload(files[0]);
|
||||
setUploadMediaOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Блок МЕДИА для статьи */}
|
||||
{/* <Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
МЕДИА
|
||||
</Typography>
|
||||
{data.left.media ? (
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<img
|
||||
src={data.left.media.filename}
|
||||
alt="Selected media"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "150px",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 100,
|
||||
backgroundColor: "grey.100",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
border: "2px dashed",
|
||||
borderColor: "grey.300",
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary">Нет медиа</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<ImagePlus size={18} />}
|
||||
onClick={handleOpenMediaDialog}
|
||||
>
|
||||
Выбрать/Загрузить медиа
|
||||
</Button>
|
||||
{data.left.media && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
onClick={() =>
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: data.left.body,
|
||||
media: null,
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
>
|
||||
Удалить медиа
|
||||
</Button>
|
||||
)}
|
||||
</Paper> */}
|
||||
</Box>
|
||||
|
||||
{/* Правая колонка: Предпросмотр */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
width: "100%",
|
||||
minWidth: 320,
|
||||
maxWidth: 400,
|
||||
height: "auto",
|
||||
minHeight: 500,
|
||||
backgroundColor: "#877361",
|
||||
overflowY: "auto",
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* {data.left.media?.filename ? (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
backgroundColor: "grey.300",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={data.left.media?.filename ?? ""}
|
||||
alt="Превью медиа"
|
||||
style={{
|
||||
objectFit: "cover",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
)} */}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
backgroundColor: "grey.300",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{sight[language].left.media.length > 0 ? (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: sight[language].left.media[0].id,
|
||||
media_type:
|
||||
sight[language].left.media[0].media_type,
|
||||
filename: sight[language].left.media[0].filename,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ImagePlus size={48} color="grey" />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Заголовок в превью */}
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#877361",
|
||||
color: "white",
|
||||
padding: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="h2"
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{sight[language].left.heading || "Название информации"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Текст статьи в превью */}
|
||||
<Box
|
||||
sx={{
|
||||
padding: 2,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<ReactMarkdownComponent
|
||||
value={sight[language].left.body}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{ position: "absolute", bottom: 0, right: 0, padding: 2 }}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await createSight(language);
|
||||
toast.success("Странца создана");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* <SelectMediaDialog
|
||||
open={isSelectMediaDialogOpen}
|
||||
onClose={handleCloseMediaDialog}
|
||||
onSelectMedia={handleArticleSelect}
|
||||
/> */}
|
||||
<SelectMediaDialog
|
||||
open={isSelectMediaDialogOpen}
|
||||
onClose={handleCloseMediaDialog}
|
||||
onSelectMedia={handleMediaSelected}
|
||||
/>
|
||||
<UploadMediaDialog
|
||||
open={uploadMediaOpen}
|
||||
onClose={() => setUploadMediaOpen(false)}
|
||||
afterUpload={async (media) => {
|
||||
setUploadMediaOpen(false);
|
||||
setFileToUpload(null);
|
||||
await createLinkWithArticle(media);
|
||||
}}
|
||||
/>
|
||||
<SelectArticleModal
|
||||
open={isSelectArticleDialogOpen}
|
||||
onClose={handleCloseArticleDialog}
|
||||
onSelectArticle={handleArticleSelect}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
);
|
374
src/widgets/SightTabs/CreateRightTab/index.tsx
Normal file
374
src/widgets/SightTabs/CreateRightTab/index.tsx
Normal file
@ -0,0 +1,374 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Paper,
|
||||
Typography,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { BackButton, createSightStore, languageStore, TabPanel } from "@shared";
|
||||
import {
|
||||
LanguageSwitcher,
|
||||
ReactMarkdownComponent,
|
||||
ReactMarkdownEditor,
|
||||
} from "@widgets";
|
||||
import { ImagePlus, Plus } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState } from "react";
|
||||
|
||||
// --- RightWidgetTab (Parent) Component ---
|
||||
export const CreateRightTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const { sight, createNewRightArticle, updateRightArticleInfo } =
|
||||
createSightStore;
|
||||
const { language } = languageStore;
|
||||
|
||||
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const handleSave = () => {
|
||||
console.log("Saving right widget...");
|
||||
};
|
||||
|
||||
const handleSelectArticle = (index: number) => {
|
||||
setActiveArticleIndex(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<TabPanel value={value} index={index}>
|
||||
<LanguageSwitcher />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
minHeight: "calc(100vh - 200px)", // Adjust as needed
|
||||
gap: 2,
|
||||
paddingBottom: "70px", // Space for the save button
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<BackButton />
|
||||
|
||||
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
||||
<Box className="flex flex-col w-[75%] gap-2">
|
||||
<Box className="w-full flex gap-2 ">
|
||||
<Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl overflow-y-auto gap-3 border border-gray-300 p-3">
|
||||
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto">
|
||||
<Box
|
||||
onClick={() => {
|
||||
// setMediaType("preview");
|
||||
}}
|
||||
className="w-full bg-gray-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300"
|
||||
>
|
||||
<Typography>Предпросмотр медиа</Typography>
|
||||
</Box>
|
||||
|
||||
{sight[language].right.map((article, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300"
|
||||
onClick={() => {
|
||||
handleSelectArticle(index);
|
||||
}}
|
||||
>
|
||||
<Typography>{article.heading}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<button
|
||||
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Plus size={20} color="white" />
|
||||
</button>
|
||||
<Menu
|
||||
id="basic-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "basic-button",
|
||||
}}
|
||||
sx={{
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
createNewRightArticle();
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<Typography>Создать новую</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<Typography>Выбрать существующую статью</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3">
|
||||
{activeArticleIndex !== null && (
|
||||
<>
|
||||
<Box className="flex justify-end gap-2 mb-3">
|
||||
<Button variant="contained" color="primary">
|
||||
Открепить
|
||||
</Button>
|
||||
|
||||
<Button variant="contained" color="success">
|
||||
Удалить
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
|
||||
{/* Левая колонка: Редактирование */}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Название информации"
|
||||
value={
|
||||
sight[language].right[activeArticleIndex].heading
|
||||
}
|
||||
onChange={(e) =>
|
||||
updateRightArticleInfo(
|
||||
activeArticleIndex,
|
||||
language,
|
||||
e.target.value,
|
||||
sight[language].right[activeArticleIndex].body
|
||||
)
|
||||
}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<ReactMarkdownEditor
|
||||
value={
|
||||
sight[language].right[activeArticleIndex].body
|
||||
}
|
||||
onChange={(value) =>
|
||||
updateRightArticleInfo(
|
||||
activeArticleIndex,
|
||||
language,
|
||||
sight[language].right[activeArticleIndex]
|
||||
.heading,
|
||||
value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{/* Блок МЕДИА для статьи */}
|
||||
{/* <Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
МЕДИА
|
||||
</Typography>
|
||||
{data.left.media ? (
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<img
|
||||
src={data.left.media.filename}
|
||||
alt="Selected media"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "150px",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 100,
|
||||
backgroundColor: "grey.100",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
border: "2px dashed",
|
||||
borderColor: "grey.300",
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary">Нет медиа</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<ImagePlus size={18} />}
|
||||
onClick={handleOpenMediaDialog}
|
||||
>
|
||||
Выбрать/Загрузить медиа
|
||||
</Button>
|
||||
{data.left.media && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
onClick={() =>
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: data.left.body,
|
||||
media: null,
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
>
|
||||
Удалить медиа
|
||||
</Button>
|
||||
)}
|
||||
</Paper> */}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className="w-[25%] mr-10">
|
||||
{activeArticleIndex !== null && (
|
||||
<Paper
|
||||
className="flex-1 flex flex-col rounded-2xl"
|
||||
elevation={2}
|
||||
>
|
||||
<Box
|
||||
className="rounded-2xl overflow-hidden"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "75vh",
|
||||
background: "#877361",
|
||||
borderColor: "grey.300",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{false ? (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography color="white">Загрузка...</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<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">
|
||||
{sight[language].right[activeArticleIndex]
|
||||
.heading || "Выберите статью"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
px: 2,
|
||||
flexGrow: 1,
|
||||
|
||||
overflowY: "auto",
|
||||
backgroundColor: "#877361",
|
||||
color: "white",
|
||||
py: 1,
|
||||
}}
|
||||
>
|
||||
{sight[language].right[activeArticleIndex].body ? (
|
||||
<ReactMarkdownComponent
|
||||
value={
|
||||
sight[language].right[activeArticleIndex].body
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
color="rgba(255,255,255,0.7)"
|
||||
sx={{ textAlign: "center", mt: 4 }}
|
||||
>
|
||||
Предпросмотр статьи появится здесь
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: 2,
|
||||
backgroundColor: "background.paper", // Ensure button is visible
|
||||
width: "100%", // Cover the full width to make it a sticky footer
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button variant="contained" color="success" onClick={handleSave}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{/*
|
||||
<SelectArticleModal
|
||||
open={openedType === "article"}
|
||||
onClose={handleCloseSelectModal}
|
||||
onSelectArticle={handleSelectArticle}
|
||||
linkedArticleIds={linkedArticleIds}
|
||||
/> */}
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
);
|
@ -27,6 +27,8 @@ import { Info, ImagePlus } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
// Мокап для всплывающей подсказки
|
||||
|
||||
export const InformationTab = observer(
|
||||
@ -37,12 +39,10 @@ export const InformationTab = observer(
|
||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||
|
||||
const { language } = languageStore;
|
||||
const { sight, updateSightInfo } = editSightStore;
|
||||
|
||||
const data = sight[language];
|
||||
const common = sight.common;
|
||||
const { sight, updateSightInfo, updateSight } = editSightStore;
|
||||
|
||||
const [, setCity] = useState<number>(common.city_id ?? 0);
|
||||
const [, setCity] = useState<number>(sight.common.city_id ?? 0);
|
||||
const [coordinates, setCoordinates] = useState<string>(`0 0`);
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
@ -54,21 +54,13 @@ export const InformationTab = observer(
|
||||
>(null);
|
||||
const [isAddMediaOpen, setIsAddMediaOpen] = useState(false);
|
||||
|
||||
const handleMenuOpen = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
type: "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
) => {
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
setActiveMenuType(type);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Показывать только при инициализации (не менять при ошибках пользователя)
|
||||
if (common.latitude !== 0 || common.longitude !== 0) {
|
||||
setCoordinates(`${common.latitude} ${common.longitude}`);
|
||||
if (sight.common.latitude !== 0 || sight.common.longitude !== 0) {
|
||||
setCoordinates(`${sight.common.latitude} ${sight.common.longitude}`);
|
||||
}
|
||||
// если координаты обнулились — оставить поле как есть
|
||||
}, [common.latitude, common.longitude]);
|
||||
}, [sight.common.latitude, sight.common.longitude]);
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setMenuAnchorEl(null);
|
||||
@ -135,7 +127,7 @@ export const InformationTab = observer(
|
||||
>
|
||||
<TextField
|
||||
label={`Название (${language.toUpperCase()})`}
|
||||
value={data.name}
|
||||
value={sight[language].name}
|
||||
onChange={(e) => {
|
||||
handleChange(language as Language, {
|
||||
name: e.target.value,
|
||||
@ -147,7 +139,7 @@ export const InformationTab = observer(
|
||||
|
||||
<TextField
|
||||
label="Адрес"
|
||||
value={data.address}
|
||||
value={sight[language].address}
|
||||
onChange={(e) => {
|
||||
handleChange(language as Language, {
|
||||
address: e.target.value,
|
||||
@ -160,18 +152,15 @@ export const InformationTab = observer(
|
||||
<Autocomplete
|
||||
options={cities ?? []}
|
||||
value={
|
||||
cities.find((city) => city.id === common.city_id) ?? null
|
||||
cities.find((city) => city.id === sight.common.city_id) ??
|
||||
null
|
||||
}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(_, value) => {
|
||||
setCity(value?.id ?? 0);
|
||||
handleChange(
|
||||
language as Language,
|
||||
{
|
||||
city_id: value?.id ?? 0,
|
||||
},
|
||||
true
|
||||
);
|
||||
handleChange(language as Language, {
|
||||
city_id: value?.id ?? 0,
|
||||
});
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Город" />
|
||||
@ -195,15 +184,23 @@ export const InformationTab = observer(
|
||||
const isValidLon = !isNaN(lon);
|
||||
|
||||
if (isValidLat && isValidLon) {
|
||||
handleChange(language as Language, {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
});
|
||||
handleChange(
|
||||
language as Language,
|
||||
{
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
},
|
||||
true
|
||||
);
|
||||
} else {
|
||||
handleChange(language as Language, {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
});
|
||||
handleChange(
|
||||
language as Language,
|
||||
{
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
@ -251,17 +248,18 @@ export const InformationTab = observer(
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
position: "relative",
|
||||
width: "200px",
|
||||
height: "200px",
|
||||
backgroundColor: "grey.200",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
cursor: common.thumbnail ? "pointer" : "default",
|
||||
cursor: sight.common.thumbnail ? "pointer" : "default",
|
||||
"&:hover": {
|
||||
backgroundColor: common.thumbnail
|
||||
backgroundColor: sight.common.thumbnail
|
||||
? "red.300"
|
||||
: "grey.200",
|
||||
},
|
||||
@ -270,29 +268,22 @@ export const InformationTab = observer(
|
||||
setIsMediaModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{common.thumbnail ? (
|
||||
{sight.common.thumbnail ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
common.thumbnail
|
||||
sight.common.thumbnail
|
||||
}/download?token=${token}`}
|
||||
alt="Логотип"
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onClick={() => {
|
||||
setIsPreviewMediaOpen(true);
|
||||
setMediaId(common.thumbnail);
|
||||
setMediaId(sight.common.thumbnail ?? "");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ImagePlus size={24} color="grey" />
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={(e) => handleMenuOpen(e, "thumbnail")}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Paper>
|
||||
<Paper
|
||||
elevation={2}
|
||||
@ -324,49 +315,45 @@ export const InformationTab = observer(
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
position: "relative",
|
||||
width: "200px",
|
||||
height: "200px",
|
||||
backgroundColor: "grey.200",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
cursor: common.watermark_lu ? "pointer" : "default",
|
||||
cursor: sight.common.watermark_lu
|
||||
? "pointer"
|
||||
: "default",
|
||||
"&:hover": {
|
||||
backgroundColor: common.watermark_lu
|
||||
backgroundColor: sight.common.watermark_lu
|
||||
? "grey.300"
|
||||
: "grey.200",
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsPreviewMediaOpen(true);
|
||||
setMediaId(common.watermark_lu);
|
||||
setMediaId(sight.common.watermark_lu ?? "");
|
||||
}}
|
||||
>
|
||||
{common.watermark_lu ? (
|
||||
{sight.common.watermark_lu ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
common.watermark_lu
|
||||
sight.common.watermark_lu
|
||||
}/download?token=${token}`}
|
||||
alt="Знак л.в"
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onClick={() => {
|
||||
setIsMediaModalOpen(true);
|
||||
setMediaId(common.watermark_lu);
|
||||
setMediaId(sight.common.watermark_lu ?? "");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ImagePlus size={24} color="grey" />
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={(e) => handleMenuOpen(e, "watermark_lu")}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
<Paper
|
||||
@ -399,49 +386,45 @@ export const InformationTab = observer(
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
position: "relative",
|
||||
width: "200px",
|
||||
height: "200px",
|
||||
backgroundColor: "grey.200",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
cursor: common.watermark_rd ? "pointer" : "default",
|
||||
cursor: sight.common.watermark_rd
|
||||
? "pointer"
|
||||
: "default",
|
||||
"&:hover": {
|
||||
backgroundColor: common.watermark_rd
|
||||
backgroundColor: sight.common.watermark_rd
|
||||
? "grey.300"
|
||||
: "grey.200",
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsMediaModalOpen(true);
|
||||
setMediaId(common.watermark_rd);
|
||||
setMediaId(sight.common.watermark_rd ?? "");
|
||||
}}
|
||||
>
|
||||
{common.watermark_rd ? (
|
||||
{sight.common.watermark_rd ? (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
common.watermark_rd
|
||||
sight.common.watermark_rd
|
||||
}/download?token=${token}`}
|
||||
alt="Знак п.в"
|
||||
style={{ maxWidth: "100%", maxHeight: "100%" }}
|
||||
onClick={() => {
|
||||
setIsPreviewMediaOpen(true);
|
||||
setMediaId(common.watermark_rd);
|
||||
setMediaId(sight.common.watermark_rd ?? "");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ImagePlus size={24} color="grey" />
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={(e) => handleMenuOpen(e, "watermark_rd")}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
@ -467,8 +450,9 @@ export const InformationTab = observer(
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={() => {
|
||||
console.log(sight);
|
||||
onClick={async () => {
|
||||
await updateSight();
|
||||
toast.success("Достопримечательность сохранена");
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
|
@ -1,326 +1,345 @@
|
||||
// @widgets/LeftWidgetTab.tsx
|
||||
import { Box, Button, TextField, Paper, Typography } from "@mui/material";
|
||||
import {
|
||||
articlesStore,
|
||||
BackButton,
|
||||
TabPanel,
|
||||
languageStore,
|
||||
SelectMediaDialog,
|
||||
editSightStore,
|
||||
SelectArticleModal,
|
||||
UploadMediaDialog,
|
||||
} from "@shared";
|
||||
import {
|
||||
LanguageSwitcher,
|
||||
ReactMarkdownComponent,
|
||||
ReactMarkdownEditor,
|
||||
MediaArea,
|
||||
MediaViewer,
|
||||
} from "@widgets";
|
||||
import { Unlink, Trash2, ImagePlus } from "lucide-react";
|
||||
import { Trash2, ImagePlus } from "lucide-react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const LeftWidgetTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const { sight, updateSightInfo } = editSightStore;
|
||||
const { getArticleByArticleId } = articlesStore;
|
||||
const {
|
||||
sight,
|
||||
updateSightInfo,
|
||||
unlinkLeftArticle,
|
||||
updateSight,
|
||||
deleteLeftArticle,
|
||||
createLeftArticle,
|
||||
deleteMedia,
|
||||
uploadMediaOpen,
|
||||
setUploadMediaOpen,
|
||||
|
||||
const linkedArticle = getArticleByArticleId.get(); // Получаем связанную статью
|
||||
const data = sight[languageStore.language]; // Получаем данные для текущего языка
|
||||
setFileToUpload,
|
||||
createLinkWithArticle,
|
||||
} = editSightStore;
|
||||
|
||||
const { language } = languageStore;
|
||||
const data = sight[language];
|
||||
|
||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||||
useState(false);
|
||||
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleMediaSelected = useCallback(() => {
|
||||
// При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА
|
||||
// сохраняя текущие heading и body.
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: data.left.body,
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
setIsSelectMediaDialogOpen(false);
|
||||
}, [
|
||||
languageStore.language,
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: data.left.body,
|
||||
},
|
||||
const handleMediaSelected = useCallback(
|
||||
async (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}) => {
|
||||
await createLinkWithArticle(media);
|
||||
setIsSelectMediaDialogOpen(false);
|
||||
},
|
||||
false,
|
||||
]);
|
||||
[createLinkWithArticle]
|
||||
);
|
||||
|
||||
const handleCloseMediaDialog = useCallback(() => {
|
||||
setIsSelectMediaDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
// ... (остальной JSX код остался почти без изменений)
|
||||
return (
|
||||
<TabPanel value={value} index={index}>
|
||||
<LanguageSwitcher />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 3,
|
||||
paddingBottom: "70px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<BackButton />
|
||||
const handleCloseArticleDialog = useCallback(() => {
|
||||
setIsSelectArticleDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
<Paper
|
||||
elevation={2}
|
||||
const handleSelectArticle = useCallback(
|
||||
(
|
||||
articleId: number,
|
||||
heading: string,
|
||||
body: string,
|
||||
media: { id: string; media_type: number; filename: string }[]
|
||||
) => {
|
||||
setIsSelectArticleDialogOpen(false);
|
||||
updateSightInfo(languageStore.language, {
|
||||
left: {
|
||||
heading,
|
||||
body,
|
||||
media,
|
||||
},
|
||||
});
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
{
|
||||
left_article: articleId,
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabPanel value={value} index={index}>
|
||||
<LanguageSwitcher />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingX: 2.5,
|
||||
paddingY: 1.5,
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
flexDirection: "column",
|
||||
gap: 3,
|
||||
paddingBottom: "70px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Левая статья</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
{linkedArticle && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<Unlink size={18} />}
|
||||
size="small"
|
||||
>
|
||||
Открепить
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<Trash2 size={18} />}
|
||||
size="small"
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
<BackButton />
|
||||
|
||||
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
|
||||
{/* Левая колонка: Редактирование */}
|
||||
<Box
|
||||
sx={{ flex: 2, display: "flex", flexDirection: "column", gap: 2 }}
|
||||
>
|
||||
<TextField
|
||||
label="Название информации"
|
||||
value={data?.left?.heading}
|
||||
onChange={(e) =>
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
{
|
||||
left: {
|
||||
heading: e.target.value,
|
||||
body: data.left.body,
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<ReactMarkdownEditor
|
||||
value={data?.left?.body}
|
||||
onChange={(value) =>
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: value,
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Блок МЕДИА для статьи */}
|
||||
{/* <Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
МЕДИА
|
||||
</Typography>
|
||||
{data.left.media ? (
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<img
|
||||
src={data.left.media.filename}
|
||||
alt="Selected media"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "150px",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 100,
|
||||
backgroundColor: "grey.100",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
border: "2px dashed",
|
||||
borderColor: "grey.300",
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary">Нет медиа</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<ImagePlus size={18} />}
|
||||
onClick={handleOpenMediaDialog}
|
||||
>
|
||||
Выбрать/Загрузить медиа
|
||||
</Button>
|
||||
{data.left.media && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
onClick={() =>
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
{
|
||||
left: {
|
||||
heading: data.left.heading,
|
||||
body: data.left.body,
|
||||
media: null,
|
||||
},
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
>
|
||||
Удалить медиа
|
||||
</Button>
|
||||
)}
|
||||
</Paper> */}
|
||||
</Box>
|
||||
|
||||
{/* Правая колонка: Предпросмотр */}
|
||||
<Box
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1.5,
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingX: 2.5,
|
||||
paddingY: 1.5,
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
width: "100%",
|
||||
minWidth: 320,
|
||||
maxWidth: 400,
|
||||
height: "auto",
|
||||
minHeight: 500,
|
||||
backgroundColor: "#877361",
|
||||
overflowY: "auto",
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* {data.left.media?.filename ? (
|
||||
<Box
|
||||
<Typography variant="h6">Левая статья</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
{sight.common.left_article ? (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
style={{ transition: "0" }}
|
||||
onClick={() => {
|
||||
unlinkLeftArticle();
|
||||
toast.success("Статья откреплена");
|
||||
}}
|
||||
>
|
||||
Открепить
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
style={{ transition: "0" }}
|
||||
startIcon={<Trash2 size={18} />}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
deleteLeftArticle(sight.common.left_article);
|
||||
toast.success("Статья откреплена");
|
||||
}}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||
>
|
||||
Выбрать статью
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
style={{ transition: "0" }}
|
||||
onClick={() => {
|
||||
createLeftArticle();
|
||||
toast.success("Статья создана");
|
||||
}}
|
||||
>
|
||||
Создать статью
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{sight.common.left_article > 0 && (
|
||||
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Название информации"
|
||||
value={data?.left?.heading}
|
||||
onChange={(e) =>
|
||||
updateSightInfo(languageStore.language, {
|
||||
left: {
|
||||
heading: e.target.value,
|
||||
body: sight[languageStore.language].left.body,
|
||||
media: data.left.media,
|
||||
},
|
||||
})
|
||||
}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<ReactMarkdownEditor
|
||||
value={data?.left?.body}
|
||||
onChange={(value) =>
|
||||
updateSightInfo(languageStore.language, {
|
||||
left: {
|
||||
heading: sight[languageStore.language].left.heading,
|
||||
body: value,
|
||||
media: data.left.media,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<MediaArea
|
||||
articleId={sight.common.left_article}
|
||||
mediaIds={data.left.media}
|
||||
deleteMedia={deleteMedia}
|
||||
setSelectMediaDialogOpen={setIsSelectMediaDialogOpen}
|
||||
onFilesDrop={(files) => {
|
||||
setFileToUpload(files[0]);
|
||||
setUploadMediaOpen(true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
backgroundColor: "grey.300",
|
||||
minWidth: 320,
|
||||
maxWidth: 400,
|
||||
height: "auto",
|
||||
minHeight: 500,
|
||||
backgroundColor: "#877361",
|
||||
overflowY: "auto",
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={data.left.media?.filename ?? ""}
|
||||
alt="Превью медиа"
|
||||
style={{
|
||||
objectFit: "cover",
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
height: 200,
|
||||
backgroundColor: "grey.300",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
)} */}
|
||||
>
|
||||
{data.left.media.length > 0 ? (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: data.left.media[0].id,
|
||||
media_type: data.left.media[0].media_type,
|
||||
filename: data.left.media[0].filename,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ImagePlus size={48} color="grey" />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
backgroundColor: "grey.300",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<ImagePlus size={48} color="grey" />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#877361",
|
||||
color: "white",
|
||||
padding: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="h2"
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{data?.left?.heading || "Название информации"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Заголовок в превью */}
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#877361",
|
||||
color: "white",
|
||||
padding: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="h2"
|
||||
sx={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{data?.left?.heading || "Название информации"}
|
||||
</Typography>
|
||||
{data?.left?.body && (
|
||||
<Box
|
||||
sx={{
|
||||
padding: 2,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<ReactMarkdownComponent value={data?.left?.body} />
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Текст статьи в превью */}
|
||||
<Box
|
||||
sx={{
|
||||
padding: 2,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<ReactMarkdownComponent value={data?.left?.body} />
|
||||
</Box>
|
||||
</Paper>
|
||||
<Box sx={{ position: "absolute", bottom: 0, right: 0, padding: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={async () => {
|
||||
await updateSight();
|
||||
toast.success("Достопримечательность сохранена");
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ position: "absolute", bottom: 0, right: 0, padding: 2 }}>
|
||||
<Button variant="contained" color="success">
|
||||
Сохранить
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
</TabPanel>
|
||||
<UploadMediaDialog
|
||||
open={uploadMediaOpen}
|
||||
onClose={() => setUploadMediaOpen(false)}
|
||||
afterUpload={async (media) => {
|
||||
setUploadMediaOpen(false);
|
||||
setFileToUpload(null);
|
||||
await createLinkWithArticle(media);
|
||||
}}
|
||||
/>
|
||||
<SelectMediaDialog
|
||||
open={isSelectMediaDialogOpen}
|
||||
onClose={handleCloseMediaDialog}
|
||||
onSelectMedia={handleMediaSelected}
|
||||
/>
|
||||
</TabPanel>
|
||||
<SelectArticleModal
|
||||
open={isSelectArticleDialogOpen}
|
||||
onClose={handleCloseArticleDialog}
|
||||
onSelectArticle={handleSelectArticle}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,345 +1,288 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Typography,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
articlesStore,
|
||||
BackButton,
|
||||
createSightStore,
|
||||
editSightStore,
|
||||
languageStore,
|
||||
SelectArticleModal,
|
||||
TabPanel,
|
||||
} from "@shared";
|
||||
import { SightEdit } from "@widgets";
|
||||
import { Plus } from "lucide-react";
|
||||
import {
|
||||
LanguageSwitcher,
|
||||
ReactMarkdownComponent,
|
||||
ReactMarkdownEditor,
|
||||
} from "@widgets";
|
||||
import { ImagePlus, Plus } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
// --- Mock Data (can be moved to a separate file or fetched from an API) ---
|
||||
const mockRightWidgetBlocks = [
|
||||
{ id: "preview_media", name: "Превью-медиа", type: "special" },
|
||||
{ id: "article_1", name: "1. История", type: "article" },
|
||||
{ id: "article_2", name: "2. Факты", type: "article" },
|
||||
{
|
||||
id: "article_3",
|
||||
name: "3. Блокада (Пример длинного названия)",
|
||||
type: "article",
|
||||
},
|
||||
];
|
||||
|
||||
const mockSelectedBlockData = {
|
||||
id: "article_1",
|
||||
heading: "История основания Санкт-Петербурга",
|
||||
body: "## Начало\nГород был основан 27 мая 1703 года Петром I...",
|
||||
media: [],
|
||||
};
|
||||
|
||||
// --- ArticleListSidebar Component ---
|
||||
interface ArticleBlock {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
linkedArticleId?: string; // Added for linked articles
|
||||
}
|
||||
|
||||
interface ArticleListSidebarProps {
|
||||
blocks: ArticleBlock[];
|
||||
selectedBlockId: string | null;
|
||||
onSelectBlock: (blockId: string) => void;
|
||||
onCreateNew: () => void;
|
||||
onSelectExisting: () => void;
|
||||
}
|
||||
|
||||
const ArticleListSidebar = ({
|
||||
blocks,
|
||||
selectedBlockId,
|
||||
onSelectBlock,
|
||||
onCreateNew,
|
||||
onSelectExisting,
|
||||
}: ArticleListSidebarProps) => {
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setMenuAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
width: 260,
|
||||
minWidth: 240,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
padding: 1.5,
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<List
|
||||
dense
|
||||
sx={{
|
||||
overflowY: "auto",
|
||||
flexGrow: 1,
|
||||
maxHeight: "calc(100% - 60px)",
|
||||
}}
|
||||
>
|
||||
{blocks.map((block) => (
|
||||
<ListItemButton
|
||||
key={block.id}
|
||||
selected={selectedBlockId === block.id}
|
||||
onClick={() => onSelectBlock(block.id)}
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
mb: 0.5,
|
||||
backgroundColor:
|
||||
selectedBlockId === block.id ? "primary.light" : "transparent",
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
"&:hover": {
|
||||
backgroundColor: "primary.dark",
|
||||
},
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor:
|
||||
selectedBlockId !== block.id ? "action.hover" : undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={block.name}
|
||||
primaryTypographyProps={{
|
||||
fontWeight: selectedBlockId === block.id ? "bold" : "normal",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<button
|
||||
className="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center hover:bg-blue-600 transition-colors"
|
||||
onClick={handleMenuOpen}
|
||||
>
|
||||
<Plus color="white" />
|
||||
</button>
|
||||
<Menu
|
||||
anchorEl={menuAnchorEl}
|
||||
open={Boolean(menuAnchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={onCreateNew}>Создать новую</MenuItem>
|
||||
<MenuItem onClick={onSelectExisting}>Выбрать существующую</MenuItem>
|
||||
</Menu>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// --- ArticleEditorPane Component ---
|
||||
interface ArticleData {
|
||||
id: string;
|
||||
heading: string;
|
||||
body: string;
|
||||
media: any[]; // Define a proper type for media if available
|
||||
}
|
||||
|
||||
interface ArticleEditorPaneProps {
|
||||
articleData: ArticleData | null;
|
||||
}
|
||||
|
||||
const ArticleEditorPane = ({ articleData }: ArticleEditorPaneProps) => {
|
||||
if (!articleData) {
|
||||
return (
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
padding: 2.5,
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Выберите блок для редактирования
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
padding: 2.5,
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<SightEdit />
|
||||
<Paper elevation={1} sx={{ padding: 2, mt: 1, width: "75%" }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
МЕДИА
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 100,
|
||||
backgroundColor: "grey.100",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
border: "2px dashed",
|
||||
borderColor: "grey.300",
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary">Нет медиа</Typography>
|
||||
</Box>
|
||||
<Button variant="contained">Выбрать/Загрузить медиа</Button>
|
||||
</Paper>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// --- RightWidgetTab (Parent) Component ---
|
||||
export const RightWidgetTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const [rightWidgetBlocks, setRightWidgetBlocks] = useState<ArticleBlock[]>(
|
||||
mockRightWidgetBlocks
|
||||
);
|
||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(
|
||||
mockRightWidgetBlocks[1]?.id || null
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const { createNewRightArticle, updateRightArticleInfo } = createSightStore;
|
||||
const { sight, getRightArticles, updateSight } = editSightStore;
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
if (sight.common.id) {
|
||||
getRightArticles(sight.common.id);
|
||||
}
|
||||
}, [sight.common.id]);
|
||||
|
||||
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
|
||||
|
||||
const handleSelectBlock = (blockId: string) => {
|
||||
setSelectedBlockId(blockId);
|
||||
console.log("Selected block:", blockId);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleSelectArticle = (index: number) => {
|
||||
setActiveArticleIndex(index);
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
const newBlockId = `article_${Date.now()}`;
|
||||
setRightWidgetBlocks((prevBlocks) => [
|
||||
...prevBlocks,
|
||||
{
|
||||
id: newBlockId,
|
||||
name: `${
|
||||
prevBlocks.filter((b) => b.type === "article").length + 1
|
||||
}. Новый блок`,
|
||||
type: "article",
|
||||
},
|
||||
]);
|
||||
setSelectedBlockId(newBlockId);
|
||||
createNewRightArticle();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleSelectExisting = () => {
|
||||
setIsSelectModalOpen(true);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleCloseSelectModal = () => {
|
||||
setIsSelectModalOpen(false);
|
||||
};
|
||||
|
||||
const handleSelectArticle = (articleId: string) => {
|
||||
// @ts-ignore
|
||||
const article = articlesStore.articles.find((a) => a.id === articleId);
|
||||
if (article) {
|
||||
const newBlockId = `article_linked_${article.id}_${Date.now()}`;
|
||||
setRightWidgetBlocks((prevBlocks) => [
|
||||
...prevBlocks,
|
||||
{
|
||||
id: newBlockId,
|
||||
name: `${
|
||||
prevBlocks.filter((b) => b.type === "article").length + 1
|
||||
}. ${article.service_name}`,
|
||||
type: "article",
|
||||
linkedArticleId: article.id,
|
||||
},
|
||||
]);
|
||||
setSelectedBlockId(newBlockId);
|
||||
}
|
||||
const handleArticleSelect = () => {
|
||||
// TODO: Implement article selection logic
|
||||
handleCloseSelectModal();
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log("Saving right widget...");
|
||||
// Implement save logic here, e.g., send data to an API
|
||||
const handleSave = async () => {
|
||||
await updateSight();
|
||||
toast.success("Достопримечательность сохранена");
|
||||
};
|
||||
|
||||
// Determine the current block data to pass to the editor pane
|
||||
const currentBlockToEdit = selectedBlockId
|
||||
? selectedBlockId === mockSelectedBlockData.id
|
||||
? mockSelectedBlockData
|
||||
: {
|
||||
id: selectedBlockId,
|
||||
heading:
|
||||
rightWidgetBlocks.find((b) => b.id === selectedBlockId)?.name ||
|
||||
"Заголовок...",
|
||||
body: "Содержимое...",
|
||||
media: [],
|
||||
}
|
||||
: null;
|
||||
|
||||
// Get list of already linked article IDs
|
||||
const linkedArticleIds = rightWidgetBlocks
|
||||
.filter((block) => block.linkedArticleId)
|
||||
.map((block) => block.linkedArticleId as string);
|
||||
|
||||
return (
|
||||
<TabPanel value={value} index={index}>
|
||||
<LanguageSwitcher />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
minHeight: "calc(100vh - 200px)", // Adjust as needed
|
||||
minHeight: "calc(100vh - 200px)",
|
||||
gap: 2,
|
||||
paddingBottom: "70px", // Space for the save button
|
||||
paddingBottom: "70px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<BackButton />
|
||||
|
||||
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
||||
<ArticleListSidebar
|
||||
blocks={rightWidgetBlocks}
|
||||
selectedBlockId={selectedBlockId}
|
||||
onSelectBlock={handleSelectBlock}
|
||||
onCreateNew={handleCreateNew}
|
||||
onSelectExisting={handleSelectExisting}
|
||||
/>
|
||||
<Box className="flex flex-col w-[75%] gap-2">
|
||||
<Box className="w-full flex gap-2">
|
||||
<Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl overflow-y-auto gap-3 border border-gray-300 p-3">
|
||||
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto">
|
||||
<Box
|
||||
// onClick={() => setMediaType("preview")}
|
||||
className="w-full bg-gray-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300"
|
||||
>
|
||||
<Typography>Предпросмотр медиа</Typography>
|
||||
</Box>
|
||||
|
||||
<ArticleEditorPane articleData={currentBlockToEdit} />
|
||||
{sight[language].right.map((article, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300"
|
||||
onClick={() => handleSelectArticle(index)}
|
||||
>
|
||||
<Typography>{article.heading}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<button
|
||||
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Plus size={20} color="white" />
|
||||
</button>
|
||||
<Menu
|
||||
id="basic-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "basic-button",
|
||||
}}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
<MenuItem onClick={handleCreateNew}>
|
||||
<Typography>Создать новую</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleSelectExisting}>
|
||||
<Typography>Выбрать существующую статью</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
<Box className="w-[80%] border border-gray-300 rounded-2xl p-3">
|
||||
{activeArticleIndex !== null && (
|
||||
<>
|
||||
<Box className="flex justify-end gap-2 mb-3">
|
||||
<Button variant="contained" color="primary">
|
||||
Открепить
|
||||
</Button>
|
||||
<Button variant="contained" color="success">
|
||||
Удалить
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
maxHeight: "70%",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Название информации"
|
||||
value={
|
||||
sight[language].right[activeArticleIndex].heading
|
||||
}
|
||||
onChange={(e) =>
|
||||
updateRightArticleInfo(
|
||||
activeArticleIndex,
|
||||
language,
|
||||
e.target.value,
|
||||
sight[language].right[activeArticleIndex].body
|
||||
)
|
||||
}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<ReactMarkdownEditor
|
||||
value={
|
||||
sight[language].right[activeArticleIndex].body
|
||||
}
|
||||
onChange={(value) =>
|
||||
updateRightArticleInfo(
|
||||
activeArticleIndex,
|
||||
language,
|
||||
sight[language].right[activeArticleIndex]
|
||||
.heading,
|
||||
value
|
||||
)
|
||||
}
|
||||
/>
|
||||
{/* <MediaArea
|
||||
articleId={1}
|
||||
mediaIds={[]}
|
||||
deleteMedia={() => {}}
|
||||
/> */}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="w-[25%] mr-10">
|
||||
{activeArticleIndex !== null && (
|
||||
<Paper
|
||||
className="flex-1 flex flex-col rounded-2xl"
|
||||
elevation={2}
|
||||
>
|
||||
<Box
|
||||
className="rounded-2xl overflow-hidden"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "75vh",
|
||||
background: "#877361",
|
||||
borderColor: "grey.300",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
{sight[language].right[activeArticleIndex].heading ||
|
||||
"Выберите статью"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
px: 2,
|
||||
flexGrow: 1,
|
||||
overflowY: "auto",
|
||||
backgroundColor: "#877361",
|
||||
color: "white",
|
||||
py: 1,
|
||||
}}
|
||||
>
|
||||
{sight[language].right[activeArticleIndex].body ? (
|
||||
<ReactMarkdownComponent
|
||||
value={sight[language].right[activeArticleIndex].body}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
color="rgba(255,255,255,0.7)"
|
||||
sx={{ textAlign: "center", mt: 4 }}
|
||||
>
|
||||
Предпросмотр статьи появится здесь
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
@ -348,8 +291,8 @@ export const RightWidgetTab = observer(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: 2,
|
||||
backgroundColor: "background.paper", // Ensure button is visible
|
||||
width: "100%", // Cover the full width to make it a sticky footer
|
||||
backgroundColor: "background.paper",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
@ -363,8 +306,7 @@ export const RightWidgetTab = observer(
|
||||
<SelectArticleModal
|
||||
open={isSelectModalOpen}
|
||||
onClose={handleCloseSelectModal}
|
||||
onSelectArticle={handleSelectArticle}
|
||||
linkedArticleIds={linkedArticleIds}
|
||||
onSelectArticle={handleArticleSelect}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
|
@ -1,3 +1,6 @@
|
||||
export * from "./InformationTab";
|
||||
export * from "./LeftWidgetTab";
|
||||
export * from "./RightWidgetTab";
|
||||
export * from "./CreateInformationTab";
|
||||
export * from "./CreateLeftTab";
|
||||
export * from "./CreateRightTab";
|
||||
|
@ -8,3 +8,5 @@ export * from "./LanguageSwitcher";
|
||||
export * from "./DevicesTable";
|
||||
export * from "./SightsTable";
|
||||
export * from "./MediaViewer";
|
||||
export * from "./MediaArea";
|
||||
export * from "./ModelViewer3D";
|
||||
|
@ -1 +1 @@
|
||||
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"errors":true,"version":"5.8.3"}
|
||||
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"}
|
23
yarn.lock
23
yarn.lock
@ -1323,6 +1323,11 @@ asynckit@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
attr-accept@^2.2.4:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e"
|
||||
integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==
|
||||
|
||||
axios@^1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.9.0.tgz#25534e3b72b54540077d33046f77e3b8d7081901"
|
||||
@ -1908,6 +1913,13 @@ file-entry-cache@^8.0.0:
|
||||
dependencies:
|
||||
flat-cache "^4.0.0"
|
||||
|
||||
file-selector@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4"
|
||||
integrity sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==
|
||||
dependencies:
|
||||
tslib "^2.7.0"
|
||||
|
||||
fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz"
|
||||
@ -3098,6 +3110,15 @@ react-dom@^19.1.0:
|
||||
dependencies:
|
||||
scheduler "^0.26.0"
|
||||
|
||||
react-dropzone@^14.3.8:
|
||||
version "14.3.8"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.8.tgz#a7eab118f8a452fe3f8b162d64454e81ba830582"
|
||||
integrity sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==
|
||||
dependencies:
|
||||
attr-accept "^2.2.4"
|
||||
file-selector "^2.1.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||
@ -3501,7 +3522,7 @@ ts-api-utils@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
|
||||
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
||||
|
||||
tslib@^2.4.0, tslib@^2.8.0:
|
||||
tslib@^2.4.0, tslib@^2.7.0, tslib@^2.8.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
Loading…
Reference in New Issue
Block a user