feat: cache delete + empty snapshot + route page
This commit is contained in:
@@ -38,6 +38,7 @@ import { Save } from "lucide-react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { SaveWithoutCityAgree } from "@widgets";
|
||||
|
||||
@@ -50,6 +51,7 @@ export const CreateInformationTab = observer(
|
||||
const { language } = languageStore;
|
||||
const { sight, updateSightInfo, createSight } = createSightStore;
|
||||
const data = sight[language];
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [, setCity] = useState<number>(sight.city_id ?? 0);
|
||||
const [coordinates, setCoordinates] = useState<string>(`0, 0`);
|
||||
@@ -173,14 +175,16 @@ export const CreateInformationTab = observer(
|
||||
return;
|
||||
}
|
||||
|
||||
await createSight(language);
|
||||
const newSightId = await createSight(language);
|
||||
toast.success("Достопримечательность создана");
|
||||
navigate(`/sight/${newSightId}/edit`);
|
||||
};
|
||||
|
||||
const handleConfirmSave = async () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
await createSight(language);
|
||||
const newSightId = await createSight(language);
|
||||
toast.success("Достопримечательность создана");
|
||||
navigate(`/sight/${newSightId}/edit`);
|
||||
};
|
||||
|
||||
const handleCancelSave = () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@widgets";
|
||||
import { Trash2, ImagePlus, Unlink, Plus, Save, Search } from "lucide-react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
@@ -41,6 +42,7 @@ export const CreateLeftTab = observer(
|
||||
uploadMediaOpen,
|
||||
setUploadMediaOpen,
|
||||
} = editSightStore;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { language } = languageStore;
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -449,8 +451,9 @@ export const CreateLeftTab = observer(
|
||||
startIcon={<Save color="white" size={18} />}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await createSight(language);
|
||||
const newSightId = await createSight(language);
|
||||
toast.success("Страница создана");
|
||||
navigate(`/sight/${newSightId}/edit`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Paper,
|
||||
Typography,
|
||||
TextField,
|
||||
Slider,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
BackButton,
|
||||
@@ -19,17 +20,18 @@ import {
|
||||
LanguageSwitcher,
|
||||
MediaArea,
|
||||
MediaAreaForSight,
|
||||
ReactMarkdownComponent,
|
||||
ReactMarkdownEditor,
|
||||
DeleteModal,
|
||||
} from "@widgets";
|
||||
import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react";
|
||||
import { Plus, Save, Trash2, Unlink, X } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { MediaViewer } from "../../MediaViewer/index";
|
||||
import { toast } from "react-toastify";
|
||||
import { authInstance } from "@shared";
|
||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||
import { SightFramePreview } from "../RightWidgetTab/SightFramePreview";
|
||||
|
||||
type MediaItemShared = {
|
||||
id: string;
|
||||
@@ -51,17 +53,19 @@ export const CreateRightTab = observer(
|
||||
unlinkRightAritcle,
|
||||
deleteRightArticle,
|
||||
createSight,
|
||||
clearCreateSight,
|
||||
updateRightArticles,
|
||||
updateSightInfo,
|
||||
} = createSightStore;
|
||||
const { language } = languageStore;
|
||||
const navigate = useNavigate();
|
||||
const { setFileToUpload, setUploadMediaOpen, uploadMediaOpen } =
|
||||
editSightStore;
|
||||
|
||||
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [type, setType] = useState<"article" | "media">("media");
|
||||
const [previewSection, setPreviewSection] = useState<number>(-1);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||||
useState(false);
|
||||
@@ -70,12 +74,34 @@ export const CreateRightTab = observer(
|
||||
>(null);
|
||||
|
||||
const [previewMedia, setPreviewMedia] = useState<Media | null>(null);
|
||||
const shortNameRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const insertNewline = () => {
|
||||
const input = shortNameRef.current;
|
||||
const currentValue = sight[language].name || "";
|
||||
if (!input) {
|
||||
updateSightInfo({ name: currentValue + "\n" }, language);
|
||||
return;
|
||||
}
|
||||
const start = input.selectionStart ?? currentValue.length;
|
||||
const end = input.selectionEnd ?? start;
|
||||
const newValue =
|
||||
currentValue.slice(0, start) + "\n" + currentValue.slice(end);
|
||||
updateSightInfo({ name: newValue }, language);
|
||||
requestAnimationFrame(() => {
|
||||
if (shortNameRef.current) {
|
||||
shortNameRef.current.selectionStart = start + 1;
|
||||
shortNameRef.current.selectionEnd = start + 1;
|
||||
shortNameRef.current.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (sight.preview_media) {
|
||||
const fetchMedia = async () => {
|
||||
const response = await authInstance.get(
|
||||
`/media/${sight.preview_media}`
|
||||
`/media/${sight.preview_media}`,
|
||||
);
|
||||
setPreviewMedia(response.data);
|
||||
};
|
||||
@@ -90,16 +116,17 @@ export const CreateRightTab = observer(
|
||||
) {
|
||||
setActiveArticleIndex(null);
|
||||
setType("media");
|
||||
setPreviewSection(-1);
|
||||
}
|
||||
}, [language, sight[language].right, activeArticleIndex]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await createSight(language);
|
||||
const newSightId = await createSight(language);
|
||||
console.log("[handleSave] newSightId:", newSightId, "needLeaveAgree:", createSightStore.needLeaveAgree);
|
||||
toast.success("Достопримечательность успешно создана!");
|
||||
clearCreateSight();
|
||||
setActiveArticleIndex(null);
|
||||
setType("media");
|
||||
navigate(`/sight/${newSightId}/edit`);
|
||||
console.log("[handleSave] navigate called to:", `/sight/${newSightId}/edit`);
|
||||
} catch (error) {
|
||||
console.error("Failed to save sight:", error);
|
||||
toast.error("Ошибка при создании достопримечательности.");
|
||||
@@ -109,6 +136,7 @@ export const CreateRightTab = observer(
|
||||
const handleDisplayArticleFromList = (idx: number) => {
|
||||
setActiveArticleIndex(idx);
|
||||
setType("article");
|
||||
setPreviewSection(idx);
|
||||
};
|
||||
|
||||
const handleCreateNewLocalArticle = async () => {
|
||||
@@ -116,15 +144,13 @@ export const CreateRightTab = observer(
|
||||
const newArticleId = await createNewRightArticle();
|
||||
|
||||
const newIndex = sight[language].right.findIndex(
|
||||
(a) => a.id === newArticleId
|
||||
(a) => a.id === newArticleId,
|
||||
);
|
||||
if (newIndex > -1) {
|
||||
setActiveArticleIndex(newIndex);
|
||||
setType("article");
|
||||
} else {
|
||||
setActiveArticleIndex(sight[language].right.length - 1);
|
||||
setType("article");
|
||||
}
|
||||
const resolvedIndex =
|
||||
newIndex > -1 ? newIndex : sight[language].right.length - 1;
|
||||
setActiveArticleIndex(resolvedIndex);
|
||||
setType("article");
|
||||
setPreviewSection(resolvedIndex);
|
||||
} catch (error) {
|
||||
toast.error("Не удалось создать новую статью.");
|
||||
}
|
||||
@@ -140,7 +166,7 @@ export const CreateRightTab = observer(
|
||||
};
|
||||
|
||||
const handleOpenSelectMediaDialog = (
|
||||
target: "sightPreview" | "rightArticle"
|
||||
target: "sightPreview" | "rightArticle",
|
||||
) => {
|
||||
setMediaTarget(target);
|
||||
setIsSelectMediaDialogOpen(true);
|
||||
@@ -184,11 +210,8 @@ export const CreateRightTab = observer(
|
||||
if (sourceIndex === destinationIndex) return;
|
||||
|
||||
const newRightArticles = [...sight[language].right];
|
||||
|
||||
const [movedArticle] = newRightArticles.splice(sourceIndex, 1);
|
||||
|
||||
newRightArticles.splice(destinationIndex, 0, movedArticle);
|
||||
|
||||
updateRightArticles(newRightArticles);
|
||||
};
|
||||
|
||||
@@ -212,18 +235,22 @@ export const CreateRightTab = observer(
|
||||
</div>
|
||||
|
||||
<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="flex flex-col gap-2"
|
||||
sx={{ flexGrow: 1, minWidth: 0 }}
|
||||
>
|
||||
<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={() => {
|
||||
setType("media");
|
||||
setPreviewSection(-1);
|
||||
}}
|
||||
className={`w-full p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300 ${
|
||||
className={`w-full p-4 rounded-2xl cursor-pointer text-sm transition-all duration-300 ${
|
||||
type === "media"
|
||||
? "bg-green-300 font-semibold"
|
||||
: "bg-green-200"
|
||||
? "bg-blue-400 text-white"
|
||||
: "bg-gray-200 hover:bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Typography>Предпросмотр медиа</Typography>
|
||||
@@ -249,16 +276,19 @@ export const CreateRightTab = observer(
|
||||
<Box
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
className={`w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 ${
|
||||
className={`w-full p-4 rounded-2xl text-sm cursor-pointer transition-all duration-300 ${
|
||||
snapshot.isDragging
|
||||
? "shadow-lg"
|
||||
: ""
|
||||
? "shadow-lg bg-gray-200"
|
||||
: activeArticleIndex ===
|
||||
index &&
|
||||
type === "article"
|
||||
? "bg-blue-400 text-white"
|
||||
: "bg-gray-200 hover:bg-gray-300"
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleDisplayArticleFromList(
|
||||
index
|
||||
index,
|
||||
);
|
||||
setType("article");
|
||||
}}
|
||||
>
|
||||
<Box {...provided.dragHandleProps}>
|
||||
@@ -269,7 +299,7 @@ export const CreateRightTab = observer(
|
||||
</Box>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
),
|
||||
)
|
||||
: null}
|
||||
{provided.placeholder}
|
||||
@@ -300,6 +330,7 @@ export const CreateRightTab = observer(
|
||||
unlinkRightAritcle(currentRightArticle.id);
|
||||
setActiveArticleIndex(null);
|
||||
setType("media");
|
||||
setPreviewSection(-1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -310,9 +341,7 @@ export const CreateRightTab = observer(
|
||||
color="error"
|
||||
size="small"
|
||||
startIcon={<Trash2 size={18} />}
|
||||
onClick={async () => {
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
@@ -336,7 +365,7 @@ export const CreateRightTab = observer(
|
||||
activeArticleIndex,
|
||||
language,
|
||||
e.target.value,
|
||||
currentRightArticle.body
|
||||
currentRightArticle.body,
|
||||
)
|
||||
}
|
||||
variant="outlined"
|
||||
@@ -351,7 +380,7 @@ export const CreateRightTab = observer(
|
||||
activeArticleIndex,
|
||||
language,
|
||||
currentRightArticle.heading,
|
||||
mdValue || ""
|
||||
mdValue || "",
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -373,225 +402,160 @@ export const CreateRightTab = observer(
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
) : type === "media" ? (
|
||||
<Box className="w-[80%] border border-gray-300 rounded-2xl relative flex items-center justify-center">
|
||||
<>
|
||||
{type === "media" && (
|
||||
<Box className="w-[80%] h-full rounded-2xl relative flex items-center justify-center">
|
||||
{previewMedia && (
|
||||
<>
|
||||
<Box className="absolute top-4 right-4 z-10">
|
||||
<button
|
||||
className="w-10 h-10 flex items-center justify-center z-10"
|
||||
onClick={handleUnlinkPreviewMedia}
|
||||
>
|
||||
<X size={20} color="red" />
|
||||
</button>
|
||||
</Box>
|
||||
|
||||
<Box className="w-1/2 h-1/2">
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: previewMedia.id || "",
|
||||
media_type: previewMedia.media_type,
|
||||
filename: previewMedia.filename || "",
|
||||
}}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!previewMedia && (
|
||||
<Box className="w-full h-full flex justify-center items-center">
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "500px",
|
||||
maxHeight: "100%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
margin: "0 auto",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<MediaAreaForSight
|
||||
onFinishUpload={(mediaId) => {
|
||||
linkPreviewMedia(mediaId);
|
||||
}}
|
||||
onFilesDrop={() => {}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={false}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center">
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Выберите статью слева или секцию "Предпросмотр медиа"
|
||||
</Typography>
|
||||
<Box className="w-[80%] border border-gray-300 rounded-2xl relative flex justify-center items-center">
|
||||
{sight.preview_media && (
|
||||
<Box className="w-full h-full rounded-2xl relative flex items-center justify-center">
|
||||
{previewMedia && (
|
||||
<>
|
||||
<Box className="absolute top-4 right-4 z-10">
|
||||
<button
|
||||
className="w-10 h-10 flex items-center justify-center z-10"
|
||||
onClick={handleUnlinkPreviewMedia}
|
||||
>
|
||||
<X size={20} color="red" />
|
||||
</button>
|
||||
</Box>
|
||||
<Box className="w-1/2 h-1/2 flex justify-center items-center">
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: previewMedia.id || "",
|
||||
media_type: previewMedia.media_type,
|
||||
filename: previewMedia.filename || "",
|
||||
}}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!sight.preview_media && (
|
||||
<Box className="w-full h-full flex justify-center items-center">
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "500px",
|
||||
maxHeight: "100%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
margin: "0 auto",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<MediaAreaForSight
|
||||
onFinishUpload={(mediaId) => {
|
||||
linkPreviewMedia(mediaId);
|
||||
}}
|
||||
onFilesDrop={() => {}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={false}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="w-[25%] mr-10">
|
||||
{type === "article" && activeArticleIndex !== null && sight[language].right[activeArticleIndex] && (
|
||||
<Paper
|
||||
className="flex-1 flex flex-col max-w-[500px]"
|
||||
sx={{
|
||||
borderRadius: "10px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
elevation={2}
|
||||
>
|
||||
<Box
|
||||
className=" overflow-hidden"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
background: "#877361",
|
||||
borderColor: "grey.300",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
<Box
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
width: "550px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{type === "media" && (
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<TextField
|
||||
type="number"
|
||||
label="Размер шрифта превью (px)"
|
||||
size="small"
|
||||
value={sight.preview_font_size ?? ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
if (raw === "") {
|
||||
updateSightInfo({ preview_font_size: undefined });
|
||||
return;
|
||||
}
|
||||
const val = Math.max(
|
||||
1,
|
||||
Math.min(300, Math.round(Number(raw))),
|
||||
);
|
||||
if (Number.isFinite(val)) {
|
||||
updateSightInfo({ preview_font_size: val });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{sight[language].right[activeArticleIndex].media.length >
|
||||
0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
padding: "2px 2px 0px 2px",
|
||||
|
||||
"& img": {
|
||||
borderTopLeftRadius: "10px",
|
||||
borderTopRightRadius: "10px",
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MediaViewer
|
||||
media={
|
||||
sight[language].right[activeArticleIndex].media[0]
|
||||
}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
</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={{
|
||||
p: 1,
|
||||
wordBreak: "break-word",
|
||||
fontSize: "24px",
|
||||
fontWeight: 700,
|
||||
lineHeight: "120%",
|
||||
backdropFilter: "blur(12px)",
|
||||
borderBottom: "1px solid #A89F90",
|
||||
boxShadow:
|
||||
"inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
|
||||
background:
|
||||
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="white">
|
||||
{sight[language].right[activeArticleIndex].heading ||
|
||||
"Выберите статью"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
padding: 1,
|
||||
minHeight: "200px",
|
||||
maxHeight: "300px",
|
||||
overflowY: "auto",
|
||||
"&::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
"&": {
|
||||
scrollbarWidth: "none",
|
||||
},
|
||||
background:
|
||||
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
|
||||
|
||||
flexGrow: 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
|
||||
sx={{
|
||||
p: 2,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: "24px",
|
||||
fontWeight: 700,
|
||||
lineHeight: "120%",
|
||||
flexWrap: "wrap",
|
||||
|
||||
gap: 1,
|
||||
backdropFilter: "blur(12px)",
|
||||
boxShadow:
|
||||
"inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
|
||||
background:
|
||||
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
||||
}}
|
||||
>
|
||||
{sight[language].right.length > 0 &&
|
||||
sight[language].right.map((article, index) => (
|
||||
<button
|
||||
className={`inline-block text-left text-xs text-white ${
|
||||
activeArticleIndex === index ? "underline" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setActiveArticleIndex(index);
|
||||
setType("article");
|
||||
}}
|
||||
>
|
||||
{article.heading}
|
||||
</button>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
slotProps={{ input: { min: 1, max: 300 } }}
|
||||
sx={{ width: "200px" }}
|
||||
/>
|
||||
<Slider
|
||||
value={sight.preview_font_size ?? 40}
|
||||
min={1}
|
||||
max={300}
|
||||
step={1}
|
||||
onChange={(_, newValue) => {
|
||||
if (typeof newValue === "number") {
|
||||
updateSightInfo({ preview_font_size: newValue });
|
||||
}
|
||||
}}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack direction="row" spacing={1} alignItems="flex-start">
|
||||
<TextField
|
||||
label="Полное название (поддерживает перенос ↵)"
|
||||
multiline
|
||||
minRows={1}
|
||||
maxRows={4}
|
||||
size="small"
|
||||
value={sight[language].name}
|
||||
onChange={(e) =>
|
||||
updateSightInfo({ name: e.target.value }, language)
|
||||
}
|
||||
inputRef={shortNameRef}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={insertNewline}
|
||||
title="Вставить перенос строки"
|
||||
sx={{
|
||||
minWidth: 40,
|
||||
height: 40,
|
||||
fontSize: 18,
|
||||
p: 0,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
↵
|
||||
</Button>
|
||||
</Stack>
|
||||
<SightFramePreview
|
||||
sightName={sight[language].name}
|
||||
previewMedia={previewMedia}
|
||||
articles={sight[language].right}
|
||||
onArticleSelect={(idx) => {
|
||||
handleDisplayArticleFromList(idx);
|
||||
}}
|
||||
previewFontSize={sight.preview_font_size}
|
||||
selectedSection={previewSection}
|
||||
onSectionChange={(section) => {
|
||||
setPreviewSection(section);
|
||||
if (section === -1) {
|
||||
setType("media");
|
||||
} else {
|
||||
handleDisplayArticleFromList(section);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -603,7 +567,6 @@ export const CreateRightTab = observer(
|
||||
right: 0,
|
||||
padding: 2,
|
||||
backgroundColor: "background.paper",
|
||||
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
@@ -650,10 +613,18 @@ export const CreateRightTab = observer(
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
try {
|
||||
const idx = activeArticleIndex ?? 0;
|
||||
await deleteRightArticle(currentRightArticle?.id || 0);
|
||||
setIsDeleteModalOpen(false);
|
||||
setActiveArticleIndex(null);
|
||||
setType("media");
|
||||
if (idx > 0) {
|
||||
setActiveArticleIndex(idx - 1);
|
||||
setPreviewSection(idx - 1);
|
||||
setType("article");
|
||||
} else {
|
||||
setActiveArticleIndex(null);
|
||||
setPreviewSection(-1);
|
||||
setType("media");
|
||||
}
|
||||
toast.success("Статья удалена");
|
||||
} catch {
|
||||
toast.error("Не удалось удалить статью");
|
||||
@@ -663,5 +634,5 @@ export const CreateRightTab = observer(
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
|
||||
import { ReactMarkdownComponent } from "../../ReactMarkdown";
|
||||
import { ThreeViewErrorBoundary } from "../../MediaViewer/ThreeViewErrorBoundary";
|
||||
@@ -34,6 +34,8 @@ interface SightFramePreviewProps {
|
||||
articles: Article[];
|
||||
onArticleSelect: (index: number) => void;
|
||||
previewFontSize?: number;
|
||||
selectedSection: number;
|
||||
onSectionChange: (section: number) => void;
|
||||
}
|
||||
|
||||
// Matches SightFrame.jsx renderCurrentMedia — same structure, same CSS classes
|
||||
@@ -153,11 +155,10 @@ export function SightFramePreview({
|
||||
articles,
|
||||
onArticleSelect,
|
||||
previewFontSize,
|
||||
selectedSection,
|
||||
onSectionChange,
|
||||
}: SightFramePreviewProps) {
|
||||
const token = localStorage.getItem("token") ?? "";
|
||||
|
||||
// -1 = intro (section 0 in SightFrame)
|
||||
const [selectedSection, setSelectedSection] = useState<number>(-1);
|
||||
const [threeViewResetKey, setThreeViewResetKey] = useState(0);
|
||||
const threeViewControlRef = useRef<ThreeViewHandle | null>(null);
|
||||
|
||||
@@ -175,6 +176,17 @@ export function SightFramePreview({
|
||||
// Replicates processedSightName from SightFrame.jsx
|
||||
const processedSightName = useMemo(() => {
|
||||
if (!sightName) return sightName;
|
||||
|
||||
// Handle \n line breaks
|
||||
if (sightName.includes("\n")) {
|
||||
return sightName.split("\n").map((line, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <br />}
|
||||
{line}
|
||||
</React.Fragment>
|
||||
));
|
||||
}
|
||||
|
||||
const namePattern =
|
||||
/([А-Яа-яA-Za-z0-9]\. [А-Яа-яA-Za-z0-9]\. [А-Яа-яA-Za-z0-9]+)/g;
|
||||
const parts = sightName.split(namePattern);
|
||||
@@ -199,10 +211,9 @@ export function SightFramePreview({
|
||||
// Replicates titleLineHeight from SightFrame.jsx
|
||||
const titleLineHeight = useMemo(() => {
|
||||
if (!sightName) return "120%";
|
||||
const textLength = sightName.length;
|
||||
const calculatedLineHeight = Math.max(
|
||||
100,
|
||||
Math.min(120, 120 - (textLength / 10) * 1)
|
||||
Math.min(120, 120 - (sightName.length / 10) * 1)
|
||||
);
|
||||
return `${calculatedLineHeight}%`;
|
||||
}, [sightName]);
|
||||
@@ -272,7 +283,7 @@ export function SightFramePreview({
|
||||
<button
|
||||
className="sfp-back-btn"
|
||||
type="button"
|
||||
onClick={() => setSelectedSection(-1)}
|
||||
onClick={() => onSectionChange(-1)}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
@@ -320,7 +331,7 @@ export function SightFramePreview({
|
||||
type="button"
|
||||
className={`sfp-sight-frame-menu-point${selectedSection === index ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
setSelectedSection(index);
|
||||
onSectionChange(index);
|
||||
onArticleSelect(index);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "@widgets";
|
||||
import { Plus, Save, Trash2, Unlink, X } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { MediaViewer } from "../../MediaViewer/index";
|
||||
import {
|
||||
@@ -55,6 +55,27 @@ export const RightWidgetTab = observer(
|
||||
} = editSightStore;
|
||||
|
||||
const [previewMedia, setPreviewMedia] = useState<any | null>(null);
|
||||
const shortNameRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const insertNewline = () => {
|
||||
const input = shortNameRef.current;
|
||||
const currentValue = sight[language].name || "";
|
||||
if (!input) {
|
||||
updateSightInfo(language, { name: currentValue + "\n" });
|
||||
return;
|
||||
}
|
||||
const start = input.selectionStart ?? currentValue.length;
|
||||
const end = input.selectionEnd ?? start;
|
||||
const newValue = currentValue.slice(0, start) + "\n" + currentValue.slice(end);
|
||||
updateSightInfo(language, { name: newValue });
|
||||
requestAnimationFrame(() => {
|
||||
if (shortNameRef.current) {
|
||||
shortNameRef.current.selectionStart = start + 1;
|
||||
shortNameRef.current.selectionEnd = start + 1;
|
||||
shortNameRef.current.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPreviewMedia = async () => {
|
||||
@@ -88,13 +109,23 @@ export const RightWidgetTab = observer(
|
||||
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const [previewSection, setPreviewSection] = useState<number>(-1);
|
||||
const [isSelectMediaModalOpen, setIsSelectMediaModalOpen] = useState(false);
|
||||
const [isDeleteArticleModalOpen, setIsDeleteArticleModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleDeleteArticle = () => {
|
||||
deleteRightArticle(sight[language].right[activeArticleIndex || 0].id);
|
||||
setActiveArticleIndex(null);
|
||||
const idx = activeArticleIndex || 0;
|
||||
deleteRightArticle(sight[language].right[idx].id);
|
||||
if (idx > 0) {
|
||||
setActiveArticleIndex(idx - 1);
|
||||
setPreviewSection(idx - 1);
|
||||
setType("article");
|
||||
} else {
|
||||
setActiveArticleIndex(null);
|
||||
setPreviewSection(-1);
|
||||
setType("media");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectArticle = (index: number) => {
|
||||
@@ -111,6 +142,7 @@ export const RightWidgetTab = observer(
|
||||
if (newIndex > -1) {
|
||||
setActiveArticleIndex(newIndex);
|
||||
setType("article");
|
||||
setPreviewSection(newIndex);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating new article:", error);
|
||||
@@ -178,8 +210,15 @@ export const RightWidgetTab = observer(
|
||||
<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={() => setType("media")}
|
||||
className="w-full bg-green-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300"
|
||||
onClick={() => {
|
||||
setType("media");
|
||||
setPreviewSection(-1);
|
||||
}}
|
||||
className={`w-full p-4 rounded-2xl cursor-pointer text-sm transition-all duration-300 ${
|
||||
type === "media"
|
||||
? "bg-blue-400 text-white"
|
||||
: "bg-gray-200 hover:bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Typography>Предпросмотр медиа</Typography>
|
||||
</Box>
|
||||
@@ -204,14 +243,17 @@ export const RightWidgetTab = observer(
|
||||
<Box
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
className={`w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 ${
|
||||
className={`w-full p-4 rounded-2xl text-sm cursor-pointer transition-all duration-300 ${
|
||||
snapshot.isDragging
|
||||
? "shadow-lg"
|
||||
: ""
|
||||
? "shadow-lg bg-gray-200"
|
||||
: activeArticleIndex === index && type === "article"
|
||||
? "bg-blue-400 text-white"
|
||||
: "bg-gray-200 hover:bg-gray-300"
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleSelectArticle(index);
|
||||
setType("article");
|
||||
setPreviewSection(index);
|
||||
}}
|
||||
>
|
||||
<Box {...provided.dragHandleProps}>
|
||||
@@ -399,38 +441,62 @@ export const RightWidgetTab = observer(
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexShrink: 0, width: "550px", display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
{type === "media" && (
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<TextField
|
||||
type="number"
|
||||
label="Размер шрифта превью (px)"
|
||||
size="small"
|
||||
value={sight.common.preview_font_size ?? ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
if (raw === "") {
|
||||
updateSightInfo(language, { preview_font_size: undefined }, true);
|
||||
return;
|
||||
}
|
||||
const val = Math.max(1, Math.min(300, Math.round(Number(raw))));
|
||||
if (Number.isFinite(val)) {
|
||||
updateSightInfo(language, { preview_font_size: val }, true);
|
||||
}
|
||||
}}
|
||||
slotProps={{ input: { min: 1, max: 300 } }}
|
||||
sx={{ width: "200px" }}
|
||||
/>
|
||||
<Slider
|
||||
value={sight.common.preview_font_size ?? 40}
|
||||
min={1}
|
||||
max={300}
|
||||
step={1}
|
||||
onChange={(_, newValue) => {
|
||||
if (typeof newValue === "number") {
|
||||
updateSightInfo(language, { preview_font_size: newValue }, true);
|
||||
}
|
||||
}}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack direction="row" spacing={1} alignItems="flex-start">
|
||||
<TextField
|
||||
type="number"
|
||||
label="Размер шрифта превью (px)"
|
||||
label="Полное название (поддерживает перенос ↵)"
|
||||
multiline
|
||||
minRows={1}
|
||||
maxRows={4}
|
||||
size="small"
|
||||
value={sight.common.preview_font_size ?? ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
if (raw === "") {
|
||||
updateSightInfo(language, { preview_font_size: undefined }, true);
|
||||
return;
|
||||
}
|
||||
const val = Math.max(1, Math.min(300, Math.round(Number(raw))));
|
||||
if (Number.isFinite(val)) {
|
||||
updateSightInfo(language, { preview_font_size: val }, true);
|
||||
}
|
||||
}}
|
||||
slotProps={{ input: { min: 1, max: 300 } }}
|
||||
sx={{ width: "200px" }}
|
||||
/>
|
||||
<Slider
|
||||
value={sight.common.preview_font_size ?? 40}
|
||||
min={1}
|
||||
max={300}
|
||||
step={1}
|
||||
onChange={(_, newValue) => {
|
||||
if (typeof newValue === "number") {
|
||||
updateSightInfo(language, { preview_font_size: newValue }, true);
|
||||
}
|
||||
}}
|
||||
value={sight[language].name}
|
||||
onChange={(e) => updateSightInfo(language, { name: e.target.value })}
|
||||
inputRef={shortNameRef}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={insertNewline}
|
||||
title="Вставить перенос строки"
|
||||
sx={{ minWidth: 40, height: 40, fontSize: 18, p: 0, flexShrink: 0 }}
|
||||
>
|
||||
↵
|
||||
</Button>
|
||||
</Stack>
|
||||
<SightFramePreview
|
||||
sightName={sight[language].name}
|
||||
@@ -439,8 +505,19 @@ export const RightWidgetTab = observer(
|
||||
onArticleSelect={(idx) => {
|
||||
handleSelectArticle(idx);
|
||||
setType("article");
|
||||
setPreviewSection(idx);
|
||||
}}
|
||||
previewFontSize={sight.common.preview_font_size}
|
||||
selectedSection={previewSection}
|
||||
onSectionChange={(section) => {
|
||||
setPreviewSection(section);
|
||||
if (section === -1) {
|
||||
setType("media");
|
||||
} else {
|
||||
handleSelectArticle(section);
|
||||
setType("article");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -37,7 +37,8 @@ export const TestingModeBanner = observer(() => {
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
ПРОВОДИТСЯ ТЕСТИРОВАНИЕ. ПРОСЬБА НЕ ВЗАИМОДЕЙСТВОВАТЬ С АДМИН-ПАНЕЛЬЮ
|
||||
ПРОВОДИТСЯ ТЕСТИРОВАНИЕ. ПРОСЬБА НЕ ВЗАИМОДЕЙСТВОВАТЬ С ПАНЕЛЬЮ
|
||||
АДМИНИСТРИРОВАНИЯ
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user