feat: fix sight edit

This commit is contained in:
2025-06-07 15:16:29 +03:00
parent 1104e94ba8
commit 0fe4683683
17 changed files with 438 additions and 275 deletions
src
pages
CreateSightPage
EditSightPage
Sight
SightListPage
shared
modals
SelectArticleDialog
UploadMediaDialog
store
CreateSightStore
EditSightStore
widgets
ImageUploadCard
MediaArea
MediaViewer
ReactMarkdownEditor
SightTabs
CreateInformationTab
CreateLeftTab
CreateRightTab
InformationTab
LeftWidgetTab
RightWidgetTab

@ -1,5 +1,10 @@
import { Box, Tab, Tabs } from "@mui/material";
import { articlesStore, cityStore, languageStore } from "@shared";
import {
articlesStore,
cityStore,
createSightStore,
languageStore,
} from "@shared";
import {
CreateInformationTab,
CreateLeftTab,
@ -22,13 +27,15 @@ export const CreateSightPage = observer(() => {
const [value, setValue] = useState(0);
const { getCities } = cityStore;
const { getArticles } = articlesStore;
const { needLeaveAgree } = createSightStore;
const handleChange = (_: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
let blocker = useBlocker(
({ currentLocation, nextLocation }) =>
true && currentLocation.pathname !== nextLocation.pathname
needLeaveAgree && currentLocation.pathname !== nextLocation.pathname
);
useEffect(() => {

@ -20,7 +20,7 @@ function a11yProps(index: number) {
export const EditSightPage = observer(() => {
const [value, setValue] = useState(0);
const { sight, getSightInfo } = editSightStore;
const { sight, getSightInfo, needLeaveAgree } = editSightStore;
const { getArticles } = articlesStore;
const { language } = languageStore;
const { id } = useParams();
@ -28,7 +28,7 @@ export const EditSightPage = observer(() => {
let blocker = useBlocker(
({ currentLocation, nextLocation }) =>
true && currentLocation.pathname !== nextLocation.pathname
needLeaveAgree && currentLocation.pathname !== nextLocation.pathname
);
const handleChange = (_: React.SyntheticEvent, newValue: number) => {

@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { DeleteModal, LanguageSwitcher } from "@widgets";
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
export const SightListPage = observer(() => {
const { sights, getSights, deleteListSight } = sightsStore;
@ -69,6 +69,13 @@ export const SightListPage = observer(() => {
<LanguageSwitcher />
<div className="w-full">
<div className="flex justify-between items-center mb-10">
<h1 className="text-2xl">Достопримечательности</h1>
<CreateButton
label="Создать достопримечательность"
path="/sight/create"
/>
</div>
<DataGrid
rows={rows}
columns={columns}

@ -202,87 +202,100 @@ export const SelectArticleModal = observer(
)}
</List>
</Paper>
<Paper
elevation={3}
sx={{
width: "100%",
minWidth: 320,
maxWidth: 310,
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
padding: 0,
margin: "0px auto",
display: "flex",
flexDirection: "column",
}}
>
<Box
<Box className="max-h-[80%] overflow-y-scroll mx-auto rounded-[10px]">
<Paper
elevation={3}
sx={{
width: "100%",
height: 175,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "3px",
}}
>
{articlesStore.articleMedia ? (
<MediaViewer
media={{
id: articlesStore.articleMedia.id,
media_type: articlesStore.articleMedia.media_type,
filename: articlesStore.articleMedia.filename,
}}
/>
) : (
<ImagePlus size={48} color="white" />
)}
</Box>
minWidth: 320,
maxWidth: 310,
<Box
sx={{
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
color: "white",
margin: "5px 0px 5px 0px",
padding: 0,
margin: "0px auto",
display: "flex",
flexDirection: "column",
gap: 1,
padding: 1,
}}
>
<Typography
variant="h5"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
}}
>
{articlesStore.articleData?.heading || "Название cтатьи"}
</Typography>
</Box>
{articlesStore.articleData?.body && (
<Box
sx={{
padding: 1,
maxHeight: "200px",
overflowY: "scroll",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
overflow: "hidden",
width: "100%",
minHeight: 100,
padding: "3px",
display: "flex",
alignItems: "center",
justifyContent: "center",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
<ReactMarkdownComponent
value={articlesStore.articleData?.body || "Описание"}
/>
{articlesStore.articleMedia ? (
<MediaViewer
media={{
id: articlesStore.articleMedia.id,
media_type: articlesStore.articleMedia.media_type,
filename: articlesStore.articleMedia.filename,
}}
fullWidth={true}
/>
) : (
<ImagePlus size={48} color="white" />
)}
</Box>
)}
</Paper>
<Box
sx={{
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
color: "white",
margin: "5px 0px 5px 0px",
display: "flex",
flexDirection: "column",
gap: 1,
padding: 1,
}}
>
<Typography
variant="h5"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
}}
>
{articlesStore.articleData?.heading || "Название cтатьи"}
</Typography>
</Box>
{articlesStore.articleData?.body && (
<Box
sx={{
padding: 1,
maxHeight: "200px",
overflowY: "scroll",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
}}
>
<ReactMarkdownComponent
value={articlesStore.articleData?.body || "Описание"}
/>
</Box>
)}
</Paper>
</Box>
</DialogContent>
<DialogActions sx={{ p: 2 }}>
<Button onClick={onClose}>Отмена</Button>

@ -1,4 +1,4 @@
import { MEDIA_TYPE_LABELS, editSightStore } from "@shared";
import { MEDIA_TYPE_LABELS, MEDIA_TYPE_VALUES, editSightStore } from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import {
@ -31,6 +31,7 @@ interface UploadMediaDialogProps {
media_type: number;
}) => void;
afterUploadSight?: (id: string) => void;
hardcodeType?: "thumbnail" | "watermark_lu" | "watermark_rd" | null;
}
export const UploadMediaDialog = observer(
@ -39,6 +40,7 @@ export const UploadMediaDialog = observer(
onClose,
afterUpload,
afterUploadSight,
hardcodeType,
}: UploadMediaDialogProps) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -165,10 +167,12 @@ export const UploadMediaDialog = observer(
<FormControl fullWidth sx={{ width: "50%" }}>
<InputLabel>Тип медиа</InputLabel>
<Select
value={mediaType}
value={
hardcodeType ? MEDIA_TYPE_VALUES[hardcodeType] : mediaType
}
disabled={!!hardcodeType}
label="Тип медиа"
onChange={(e) => setMediaType(Number(e.target.value))}
disabled={isLoading}
>
{availableMediaTypes.map((type) => (
<MenuItem key={type} value={type}>

@ -98,18 +98,22 @@ class CreateSightStore {
heading: "New Heading (EN)",
body: "New Text (EN)",
};
const articleZhData = { heading: "新标题 (ZH)", body: "新文本 (ZH)" };
const articleZhData = {
heading: "Новый заголовок (ZH)",
body: "Новый текст (ZH)",
};
try {
const articleRes = await languageInstance("ru").post(
"/article",
articleRuData
);
this.needLeaveAgree = true;
const articleRes = await authInstance.post("/article", {
translation: {
ru: articleRuData,
en: articleEnData,
zh: articleZhData,
},
});
const { id } = articleRes.data; // New article's ID
await languageInstance("en").patch(`/article/${id}`, articleEnData);
await languageInstance("zh").patch(`/article/${id}`, articleZhData);
runInAction(() => {
const newArticleEntry = { id, media: [] };
this.sight.ru.right.push({ ...newArticleEntry, ...articleRuData });
@ -376,6 +380,7 @@ class CreateSightStore {
// --- General Store Methods ---
clearCreateSight = () => {
this.needLeaveAgree = false;
this.sight = JSON.parse(JSON.stringify(initialSightState)); // Reset to initial
};
@ -383,6 +388,7 @@ class CreateSightStore {
content: Partial<SightLanguageInfo | SightCommonInfo>, // Corrected types
language?: Language
) => {
this.needLeaveAgree = true;
if (language) {
this.sight[language] = { ...this.sight[language], ...content };
} else {
@ -587,10 +593,31 @@ class CreateSightStore {
}
};
updateRightArticles = async (articles: any[], language: Language) => {
runInAction(() => {
this.sight[language].right = articles;
});
updateRightArticles = async (articles: any[]) => {
const articlesIds = articles.map((article) => article.id);
const sortArticles = (existing: any[]) => {
const articleMap = new Map(
existing.map((article) => [article.id, article])
);
return articlesIds
.map((id) => articleMap.get(id))
.filter(
(article): article is (typeof existing)[number] =>
article !== undefined
);
};
this.sight.ru.right = sortArticles(this.sight.ru.right);
this.sight.en.right = sortArticles(this.sight.en.right);
this.sight.zh.right = sortArticles(this.sight.zh.right); // теперь zh тоже сортируется одинаково
this.needLeaveAgree = true;
};
needLeaveAgree = false;
setNeedLeaveAgree = (need: boolean) => {
this.needLeaveAgree = need;
};
}

@ -170,6 +170,7 @@ class EditSightStore {
};
clearSightInfo = () => {
this.needLeaveAgree = false;
this.sight = {
common: {
id: 0,
@ -215,6 +216,7 @@ class EditSightStore {
content: Partial<SightLanguageInfo | SightCommonInfo>,
common: boolean = false
) => {
this.needLeaveAgree = true;
if (common) {
this.sight.common = {
...this.sight.common,
@ -262,61 +264,72 @@ class EditSightStore {
this.sight.common.left_article != 0 &&
this.sight.common.left_article != null
) {
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 authInstance.patch(`/article/${this.sight.common.left_article}`, {
translation: {
ru: {
heading: this.sight.ru.left.heading,
body: this.sight.ru.left.body,
},
en: {
heading: this.sight.en.left.heading,
body: this.sight.en.left.body,
},
zh: {
heading: this.sight.zh.left.heading,
body: this.sight.zh.left.body,
},
},
});
}
await languageInstance("ru").patch(`/sight/${this.sight.common.id}`, {
await authInstance.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,
translation: {
ru: {
name: this.sight.ru.name,
address: this.sight.ru.address,
},
en: {
name: this.sight.en.name,
address: this.sight.en.address,
},
zh: {
name: this.sight.zh.name,
address: this.sight.zh.address,
},
},
});
for (const language of ["ru", "en", "zh"] as Language[]) {
for (const article of this.sight[language].right) {
if (article.id == 0 || article.id == null) {
continue;
}
await languageInstance(language).patch(`/article/${article.id}`, {
heading: article.heading,
body: article.body,
});
for (const article of this.sight.ru.right) {
if (article.id == 0 || article.id == null) {
continue;
}
await authInstance.patch(`/article/${article.id}`, {
translation: {
ru: {
heading: article.heading,
body: article.body,
},
en: {
heading: article.heading,
body: article.body,
},
zh: {
heading: article.heading,
body: article.body,
},
},
});
}
const articleIdsInObject = this.sight.ru.right.map((article) => ({
id: article.id,
}));
await authInstance.post(`/sight/${this.sight.common.id}/article/order`, {
articles: articleIdsInObject,
});
// await languageInstance("ru").patch(
// `/sight/${this.sight.common.left_article}/article`,
// {
@ -578,19 +591,27 @@ class EditSightStore {
};
createNewRightArticle = async () => {
const articleId = await languageInstance("ru").post("/article", {
heading: "Введите русский заголовок",
body: "Введите русский текст",
const articleRuData = {
heading: "Новый заголовок (RU)",
body: "Новый текст (RU)",
};
const articleEnData = {
heading: "New Heading (EN)",
body: "New Text (EN)",
};
const articleZhData = {
heading: "Новый заголовок (ZH)",
body: "Новый текст (ZH)",
};
const articleId = await authInstance.post("/article", {
translation: {
ru: articleRuData,
en: articleEnData,
zh: articleZhData,
},
});
const { id } = articleId.data;
await languageInstance("en").patch(`/article/${id}`, {
heading: "Enter the English heading",
body: "Enter the English text",
});
await languageInstance("zh").patch(`/article/${id}`, {
heading: "Введите китайский заголовок",
body: "Введите китайский текст",
});
await authInstance.post(`/sight/${this.sight.common.id}/article`, {
article_id: id,
page_num: this.sight.ru.right.length + 1,
@ -598,20 +619,20 @@ class EditSightStore {
this.sight.ru.right.push({
id: id,
heading: "Введите русский заголовок",
body: "Введите русский текст",
heading: articleRuData.heading,
body: articleRuData.body,
media: [],
});
this.sight.en.right.push({
id: id,
heading: "Enter the English heading",
body: "Enter the English text",
heading: articleEnData.heading,
body: articleEnData.body,
media: [],
});
this.sight.zh.right.push({
id: id,
heading: "Введите китайский заголовок",
body: "Введите китайский текст",
heading: articleZhData.heading,
body: articleZhData.body,
media: [],
});
};
@ -671,14 +692,31 @@ class EditSightStore {
this.sight[language].right[index].body = body;
};
updateRightArticles = async (articles: any[], language: Language) => {
this.sight[language].right = articles;
const articleIdsInObject = articles.map((article) => ({
id: article.id,
}));
await authInstance.post(`/sight/${this.sight.common.id}/article/order`, {
articles: articleIdsInObject,
});
updateRightArticles = async (articles: any[]) => {
const articlesIds = articles.map((article) => article.id);
const sortArticles = (existing: any[]) => {
const articleMap = new Map(
existing.map((article) => [article.id, article])
);
return articlesIds
.map((id) => articleMap.get(id))
.filter(
(article): article is (typeof existing)[number] =>
article !== undefined
);
};
this.sight.ru.right = sortArticles(this.sight.ru.right);
this.sight.en.right = sortArticles(this.sight.en.right);
this.sight.zh.right = sortArticles(this.sight.zh.right); // теперь zh тоже сортируется одинаково
this.needLeaveAgree = true;
};
needLeaveAgree = false;
setNeedLeaveAgree = (need: boolean) => {
this.needLeaveAgree = need;
};
}

@ -1,8 +1,8 @@
import React, { useRef, useState, DragEvent, useEffect } from "react";
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
import { X, Info, MousePointer } from "lucide-react"; // Assuming lucide-react for icons
import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons
import { editSightStore } from "@shared";
import { toast } from "react-toastify";
interface ImageUploadCardProps {
title: string;
imageKey?: "thumbnail" | "watermark_lu" | "watermark_rd";
@ -12,16 +12,18 @@ interface ImageUploadCardProps {
onSelectFileClick: () => void;
setUploadMediaOpen: (open: boolean) => void;
tooltipText?: string;
setHardcodeType?: (type: string) => void;
}
export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
title,
imageKey,
imageUrl,
onImageClick,
onDeleteImageClick,
onSelectFileClick,
setUploadMediaOpen,
setHardcodeType,
tooltipText,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
@ -46,6 +48,9 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
if (file) {
setFileToUpload(file);
setUploadMediaOpen(true);
if (imageKey && setHardcodeType) {
setHardcodeType(imageKey);
}
}
// Reset the input value so selecting the same file again triggers change
event.target.value = "";
@ -73,8 +78,12 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
const files = event.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
setFileToUpload(file);
setUploadMediaOpen(true);
if (file.type.startsWith("image/")) {
setFileToUpload(file);
setUploadMediaOpen(true);
} else {
toast.error("Пожалуйста, выберите изображение");
}
}
};
@ -159,7 +168,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
<Button
variant="contained"
color="primary"
startIcon={<MousePointer color="white" size={18} />}
startIcon={<Plus color="white" size={18} />}
onClick={(e) => {
e.stopPropagation(); // Prevent `handleZoneClick` from firing
onSelectFileClick(); // This button might trigger a different modal

@ -96,10 +96,10 @@ export const MediaArea = observer(
</Button>
</div>
<div className="w-full flex flex-start flex-wrap gap-2 mt-4">
<div className="w-full flex flex-start flex-wrap gap-2 mt-4 py-10">
{mediaIds.map((m) => (
<button
className="relative w-40 h-40"
className="relative w-20 h-20"
key={m.id}
onClick={() => handleMediaModal(m.id)}
>

@ -12,29 +12,17 @@ export interface MediaData {
export function MediaViewer({
media,
className,
}: Readonly<{ media?: MediaData; className?: string }>) {
fullWidth,
}: Readonly<{ media?: MediaData; className?: string; fullWidth?: boolean }>) {
const token = localStorage.getItem("token");
return (
<Box
sx={{
width: "100%",
height: "100%",
display: "flex",
flexGrow: 1,
}}
className={className}
>
<Box className={className} width={fullWidth ? "100%" : "auto"}>
{media?.media_type === 1 && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
media?.id
}/download?token=${token}`}
alt={media?.filename}
style={{
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
)}
@ -46,7 +34,7 @@ export function MediaViewer({
style={{
width: "100%",
height: "100%",
objectFit: "contain",
objectFit: "cover",
borderRadius: 8,
}}
controls
@ -60,12 +48,6 @@ export function MediaViewer({
media?.id
}/download?token=${token}`}
alt={media?.filename}
style={{
width: "100%",
height: "100%",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{media?.media_type === 4 && (
@ -75,10 +57,7 @@ export function MediaViewer({
}/download?token=${token}`}
alt={media?.filename}
style={{
width: "100%",
height: "100%",
objectFit: "contain",
borderRadius: 8,
objectFit: "cover",
}}
/>
)}

@ -25,13 +25,20 @@ const StyledMarkdownEditor = styled("div")(({ theme }) => ({
color: theme.palette.text.primary,
borderColor: theme.palette.divider,
height: "auto",
minHeight: "200px",
minHeight: "400px",
maxHeight: "500px",
overflow: "auto",
overflowY: "auto",
overflowX: "hidden",
wordWrap: "break-word", // ✅ добавлено
whiteSpace: "pre-wrap", // ✅ добавлено
},
"& .CodeMirror-scroll": {
minHeight: "200px",
minHeight: "400px",
maxHeight: "500px",
overflowY: "auto",
overflowX: "hidden",
wordBreak: "break-word", // ✅ добавлено
},
"& .CodeMirror-selected": {
backgroundColor: `${theme.palette.action.selected} !important`,
@ -76,6 +83,9 @@ export const ReactMarkdownEditor = ({
const mergedOptions = useMemo(() => {
return {
...incomingOptions,
codeMirrorOptions: {
lineWrapping: true,
},
forceSync: true,
spellChecker: false,
toolbar: [

@ -48,6 +48,9 @@ export const CreateInformationTab = observer(
"thumbnail" | "watermark_lu" | "watermark_rd" | null
>(null);
const [isAddMediaOpen, setIsAddMediaOpen] = useState(false);
const [hardcodeType, setHardcodeType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | null
>(null);
// const handleMenuOpen = (
// event: React.MouseEvent<HTMLElement>,
@ -260,10 +263,15 @@ export const CreateInformationTab = observer(
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
}}
/>
<ImageUploadCard
title="Водяной знак (л)"
title="Водяной знак (леввый верхний)"
imageKey="watermark_lu"
imageUrl={sight.watermark_lu}
onImageClick={() => {
@ -284,10 +292,15 @@ export const CreateInformationTab = observer(
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_lu");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
}}
/>
<ImageUploadCard
title="Водяной знак (п)"
title="Водяной знак (правый нижний)"
imageKey="watermark_rd"
imageUrl={sight.watermark_rd}
onImageClick={() => {
@ -308,6 +321,11 @@ export const CreateInformationTab = observer(
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_rd");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
}}
/>
</Box>
</Box>
@ -397,6 +415,7 @@ export const CreateInformationTab = observer(
setActiveMenuType(null);
setIsUploadMediaOpen(false);
}}
hardcodeType={hardcodeType}
/>
</>
);

@ -9,6 +9,7 @@ import {
createSightStore,
SelectArticleModal,
UploadMediaDialog,
Language,
} from "@shared";
import {
LanguageSwitcher,
@ -308,8 +309,8 @@ export const CreateLeftTab = observer(
flex: 1,
display: "flex",
flexDirection: "column",
gap: 1.5,
maxWidth: "320px",
gap: 0.5,
}}
>
<Paper
@ -317,49 +318,32 @@ export const CreateLeftTab = observer(
sx={{
width: "100%",
minWidth: 320,
maxWidth: 400,
height: "auto",
minHeight: 500,
backgroundColor: "#877361",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
overflowY: "auto",
padding: 0,
display: "flex",
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>
) : (
)} */}
flexDirection: "column",
borderRadius: "10px",
}}
>
<Box
sx={{
overflow: "hidden",
width: "100%",
height: 200,
backgroundColor: "grey.300",
minHeight: 100,
padding: "3px",
display: "flex",
alignItems: "center",
justifyContent: "center",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
{sight[language].left.media.length > 0 ? (
@ -370,40 +354,67 @@ export const CreateLeftTab = observer(
sight[language].left.media[0].media_type,
filename: sight[language].left.media[0].filename,
}}
fullWidth
/>
) : (
<ImagePlus size={48} color="grey" />
<ImagePlus size={48} color="white" />
)}
</Box>
{/* Заголовок в превью */}
<Box
sx={{
backgroundColor: "#877361",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
color: "white",
padding: 1.5,
margin: "5px 0px 5px 0px",
display: "flex",
flexDirection: "column",
gap: 1,
padding: 1,
}}
>
<Typography
variant="h5"
component="h2"
sx={{ wordBreak: "break-word" }}
sx={{
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
}}
>
{sight[language].left.heading || "Название информации"}
</Typography>
<Typography
variant="h6"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "18px",
lineHeight: "120%",
}}
>
{sight[language as Language].address}
</Typography>
</Box>
{/* Текст статьи в превью */}
<Box
sx={{
padding: 2,
flexGrow: 1,
}}
>
<ReactMarkdownComponent
value={sight[language].left.body}
/>
</Box>
{sight[language].left.body && (
<Box
sx={{
padding: 1,
maxHeight: "300px",
overflowY: "scroll",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
}}
>
<ReactMarkdownComponent
value={sight[language].left.body}
/>
</Box>
)}
</Paper>
</Box>
</Box>

@ -239,7 +239,7 @@ export const CreateRightTab = observer(
// 4. Update the store with the new order:
// This will typically trigger a re-render of the component with the updated list.
updateRightArticles(newRightArticles, language);
updateRightArticles(newRightArticles);
};
return (
@ -511,7 +511,7 @@ export const CreateRightTab = observer(
<Paper
className="flex-1 flex flex-col max-w-[500px]"
sx={{
borderRadius: "16px",
borderRadius: "10px",
overflow: "hidden",
}}
elevation={2}
@ -531,13 +531,17 @@ export const CreateRightTab = observer(
0 ? (
<Box
sx={{
overflow: "hidden",
width: "100%",
maxHeight: "290px",
flexShrink: 0,
padding: "2px 2px 0px 2px",
display: "flex",
alignItems: "center",
justifyContent: "center",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
<MediaViewer
@ -571,6 +575,7 @@ export const CreateRightTab = observer(
fontWeight: 700,
lineHeight: "120%",
backdropFilter: "blur(12px)",
borderBottom: "1px solid #A89F90",
boxShadow:
"inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
background:

@ -50,7 +50,9 @@ export const InformationTab = observer(
"thumbnail" | "watermark_lu" | "watermark_rd" | null
>(null);
const [isAddMediaOpen, setIsAddMediaOpen] = useState(false);
const [hardcodeType, setHardcodeType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | null
>(null);
useEffect(() => {
// Показывать только при инициализации (не менять при ошибках пользователя)
if (sight.common.latitude !== 0 || sight.common.longitude !== 0) {
@ -260,9 +262,14 @@ export const InformationTab = observer(
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
}}
/>
<ImageUploadCard
title="Водяной знак (л)"
title="Водяной знак (левый верхний)"
imageKey="watermark_lu"
imageUrl={sight.common.watermark_lu}
onImageClick={() => {
@ -287,9 +294,14 @@ export const InformationTab = observer(
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_lu");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
}}
/>
<ImageUploadCard
title="Водяной знак (п)"
title="Водяной знак (правый нижний)"
imageKey="watermark_rd"
imageUrl={sight.common.watermark_rd}
onImageClick={() => {
@ -314,6 +326,11 @@ export const InformationTab = observer(
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_rd");
}}
setHardcodeType={(type) => {
setHardcodeType(
type as "thumbnail" | "watermark_lu" | "watermark_rd"
);
}}
/>
</Box>
</Box>
@ -399,6 +416,7 @@ export const InformationTab = observer(
setActiveMenuType(null);
setIsUploadMediaOpen(false);
}}
hardcodeType={hardcodeType}
/>
<PreviewMediaDialog
open={isPreviewMediaOpen}

@ -260,22 +260,32 @@ export const LeftWidgetTab = observer(
sx={{
width: "100%",
minWidth: 320,
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
overflowY: "auto",
padding: 0,
display: "flex",
flexDirection: "column",
borderRadius: "10px",
}}
>
<Box
sx={{
overflow: "hidden",
width: "100%",
height: 175,
minHeight: 100,
padding: "3px",
display: "flex",
alignItems: "center",
padding: "3px",
justifyContent: "center",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
{data.left.media.length > 0 ? (
@ -285,9 +295,10 @@ export const LeftWidgetTab = observer(
media_type: data.left.media[0].media_type,
filename: data.left.media[0].filename,
}}
fullWidth
/>
) : (
<ImagePlus size={48} color="grey" />
<ImagePlus size={48} color="white" />
)}
</Box>

@ -182,7 +182,7 @@ export const RightWidgetTab = observer(
// 4. Update the store with the new order:
// This will typically trigger a re-render of the component with the updated list.
updateRightArticles(newRightArticles, language);
updateRightArticles(newRightArticles);
};
return (
@ -347,7 +347,7 @@ export const RightWidgetTab = observer(
value={
sight[language].right[activeArticleIndex].body
}
onChange={(value) =>
onChange={(value: any) =>
updateRightArticleInfo(
activeArticleIndex,
language,
@ -381,11 +381,11 @@ export const RightWidgetTab = observer(
</Box>
)}
{type === "media" && (
<Box className="w-[80%] border border-gray-300 rounded-2xl relative">
<Box className="w-[80%] border border-gray-300 rounded-2xl relative flex justify-center items-center">
{sight.common.preview_media && (
<>
{type === "media" && (
<Box className="w-[80%] h-full rounded-2xl relative flex items-center justify-center">
<Box className="w-full h-full rounded-2xl relative flex items-center justify-center">
{previewMedia && (
<>
<Box className="absolute top-4 right-4 z-10">
@ -397,7 +397,7 @@ export const RightWidgetTab = observer(
</button>
</Box>
<Box className="w-1/2 h-1/2">
<Box className="w-1/2 h-1/2 flex justify-center items-center">
<MediaViewer
media={{
id: previewMedia.id || "",
@ -453,7 +453,7 @@ export const RightWidgetTab = observer(
<Paper
className="flex-1 flex flex-col max-w-[500px]"
sx={{
borderRadius: "16px",
borderRadius: "10px",
overflow: "hidden",
}}
elevation={2}
@ -473,13 +473,17 @@ export const RightWidgetTab = observer(
0 ? (
<Box
sx={{
overflow: "hidden",
width: "100%",
maxHeight: "290px",
flexShrink: 0,
padding: "2px 2px 0px 2px",
display: "flex",
alignItems: "center",
justifyContent: "center",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
<MediaViewer
@ -513,6 +517,7 @@ export const RightWidgetTab = observer(
fontWeight: 700,
lineHeight: "120%",
backdropFilter: "blur(12px)",
borderBottom: "1px solid #A89F90",
boxShadow:
"inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
background: