Update media select in EditSightPage and CreateSightPage

This commit is contained in:
Илья Куприец 2025-06-03 14:32:42 +03:00
parent f0e03c3a1d
commit 078f051e8a
14 changed files with 1361 additions and 1175 deletions

View File

@ -9,18 +9,26 @@ import { useNavigate } from "react-router-dom";
interface NavigationItemProps {
item: NavigationItem;
open: boolean;
onClick?: () => void;
}
export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
item,
open,
onClick,
}) => {
const Icon = item.icon;
const navigate = useNavigate();
return (
<ListItem
onClick={() => navigate(item.path)}
onClick={() => {
if (onClick) {
onClick();
} else {
navigate(item.path);
}
}}
disablePadding
sx={{ display: "block" }}
>

View File

@ -25,6 +25,7 @@ export const NavigationList = ({ open }: { open: boolean }) => {
key={item.id}
item={item as NavigationItem}
open={open}
onClick={item.onClick ? item.onClick : undefined}
/>
))}
</List>

View File

@ -1,4 +1,4 @@
import { Button, TableBody } from "@mui/material";
import { TableBody } from "@mui/material";
import { TableRow, TableCell } from "@mui/material";
import { Table, TableHead } from "@mui/material";
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
@ -27,15 +27,6 @@ export const MediaListPage = observer(() => {
return (
<>
<div className="flex justify-end p-3 gap-5">
<Button
variant="contained"
color="primary"
onClick={() => navigate("/sight/create")}
>
Создать
</Button>
</div>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>

View File

@ -6,7 +6,6 @@ import {
MonitorSmartphone,
Map,
BookImage,
Newspaper,
} from "lucide-react";
export const DRAWER_WIDTH = 300;
@ -47,12 +46,12 @@ export const NAVIGATION_ITEMS: {
icon: BookImage,
path: "/media",
},
{
id: "articles",
label: "Статьи",
icon: Newspaper,
path: "/articles",
},
// {
// id: "articles",
// label: "Статьи",
// icon: Newspaper,
// path: "/articles",
// },
],
secondary: [
{
@ -61,6 +60,7 @@ export const NAVIGATION_ITEMS: {
icon: Power,
onClick: () => {
authStore.logout();
window.location.href = "/login";
},
},
],

View File

@ -1,13 +1,13 @@
// @shared/stores/createSightStore.ts
import {
Language,
authInstance,
languageInstance,
articlesStore,
languageStore,
mediaStore,
} from "@shared";
import { makeAutoObservable } from "mobx";
import { Language, authInstance, languageInstance, mediaStore } from "@shared";
import { makeAutoObservable, runInAction } from "mobx";
type MediaItem = {
id: string;
filename: string;
media_name?: string;
media_type: number;
};
type SightLanguageInfo = {
name: string;
@ -15,18 +15,13 @@ type SightLanguageInfo = {
left: {
heading: string;
body: string;
media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}[];
media: MediaItem[];
};
right: { id: number; heading: string; body: string; media: [] }[];
right: { id: number; heading: string; body: string; media: MediaItem[] }[];
};
type SightCommonInfo = {
id: number;
// id: number; // ID is 0 until created
city_id: number;
city: string;
latitude: number;
@ -34,48 +29,50 @@ type SightCommonInfo = {
thumbnail: string | null;
watermark_lu: string | null;
watermark_rd: string | null;
left_article: number;
left_article: number; // Can be 0 or a real ID, or placeholder like 10000000
preview_media: string | null;
video_preview: string | null;
};
// SightBaseInfo combines common info with language-specific info
// The 'id' for the sight itself will be assigned upon creation by the backend.
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,
const initialSightState: SightBaseInfo = {
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: [],
},
};
ru: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
en: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
zh: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
};
class CreateSightStore {
sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState)); // Deep copy for reset
uploadMediaOpen = false;
setUploadMediaOpen = (open: boolean) => {
@ -90,313 +87,74 @@ class CreateSightStore {
makeAutoObservable(this);
}
// --- Right Article Management ---
createNewRightArticle = async () => {
const articleId = await languageInstance("ru").post("/article", {
heading: "Введите русский заголовок",
body: "Введите русский текст",
});
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.id}/article`, {
article_id: id,
page_num: this.sight.ru.right.length + 1,
});
this.sight.ru.right.push({
id: id,
heading: "Введите русский заголовок",
body: "Введите русский текст",
media: [],
});
this.sight.en.right.push({
id: id,
heading: "Enter the English heading",
body: "Enter the English text",
media: [],
});
this.sight.zh.right.push({
id: id,
heading: "Введите китайский заголовок",
body: "Введите китайский текст",
media: [],
});
};
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: [],
},
// Create article in DB for all languages
const articleRuData = {
heading: "Новый заголовок (RU)",
body: "Новый текст (RU)",
};
};
const articleEnData = {
heading: "New Heading (EN)",
body: "New Text (EN)",
};
const articleZhData = { heading: "新标题 (ZH)", body: "新文本 (ZH)" };
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}`
try {
const articleRes = await languageInstance("ru").post(
"/article",
articleRuData
);
const { id } = articleRes.data; // New article's ID
this.sight.ru.left.heading = ruArticleData.data.heading;
this.sight.en.left.heading = enArticleData.data.heading;
this.sight.zh.left.heading = zhArticleData.data.heading;
await languageInstance("en").patch(`/article/${id}`, articleEnData);
await languageInstance("zh").patch(`/article/${id}`, articleZhData);
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,
runInAction(() => {
const newArticleEntry = { id, media: [] };
this.sight.ru.right.push({ ...newArticleEntry, ...articleRuData });
this.sight.en.right.push({ ...newArticleEntry, ...articleEnData });
this.sight.zh.right.push({ ...newArticleEntry, ...articleZhData });
});
});
console.log("created");
return id; // Return ID for potential immediate use
} catch (error) {
console.error("Error creating new right article:", error);
throw error;
}
};
linkExistingRightArticle = async (articleId: number) => {
try {
const ruData = await languageInstance("ru").get(`/article/${articleId}`);
const enData = await languageInstance("en").get(`/article/${articleId}`);
const zhData = await languageInstance("zh").get(`/article/${articleId}`);
const mediaRes = await authInstance.get(`/article/${articleId}/media`);
const mediaData: MediaItem[] = mediaRes.data || [];
runInAction(() => {
this.sight.ru.right.push({
id: articleId,
heading: ruData.data.heading,
body: ruData.data.body,
media: mediaData,
});
this.sight.en.right.push({
id: articleId,
heading: enData.data.heading,
body: enData.data.body,
media: mediaData,
});
this.sight.zh.right.push({
id: articleId,
heading: zhData.data.heading,
body: zhData.data.body,
media: mediaData,
});
});
} catch (error) {
console.error("Error linking existing right article:", error);
throw error;
}
};
updateRightArticleInfo = (
@ -405,94 +163,428 @@ class CreateSightStore {
heading: string,
body: string
) => {
this.sight[language].right[index].heading = heading;
this.sight[language].right[index].body = body;
if (this.sight[language].right[index]) {
this.sight[language].right[index].heading = heading;
this.sight[language].right[index].body = body;
}
};
// "Unlink" in create mode means just removing from the list to be created with the sight
unlinkRightAritcle = (articleId: number) => {
// Changed from 'unlinkRightAritcle' spelling
runInAction(() => {
this.sight.ru.right = this.sight.ru.right.filter(
(article) => article.id !== articleId
);
this.sight.en.right = this.sight.en.right.filter(
(article) => article.id !== articleId
);
this.sight.zh.right = this.sight.zh.right.filter(
(article) => article.id !== articleId
);
});
// Note: If this article was created via createNewRightArticle, it still exists in the DB.
// Consider if an orphaned article should be deleted here or managed separately.
// For now, it just removes it from the list associated with *this specific sight creation process*.
};
deleteRightArticle = async (articleId: number) => {
try {
await authInstance.delete(`/article/${articleId}`); // Delete from backend
runInAction(() => {
// Remove from local store for all languages
this.sight.ru.right = this.sight.ru.right.filter(
(article) => article.id !== articleId
);
this.sight.en.right = this.sight.en.right.filter(
(article) => article.id !== articleId
);
this.sight.zh.right = this.sight.zh.right.filter(
(article) => article.id !== articleId
);
});
} catch (error) {
console.error("Error deleting right article:", error);
throw error;
}
};
// --- Right Article Media Management ---
createLinkWithRightArticle = async (media: MediaItem, articleId: number) => {
try {
await authInstance.post(`/article/${articleId}/media`, {
media_id: media.id,
media_order: 1, // Or calculate based on existing media.length + 1
});
runInAction(() => {
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
const article = this.sight[lang].right.find(
(a) => a.id === articleId
);
if (article) {
if (!article.media) article.media = [];
article.media.unshift(media); // Add to the beginning
}
});
});
} catch (error) {
console.error("Error linking media to right article:", error);
throw error;
}
};
deleteRightArticleMedia = async (articleId: number, mediaId: string) => {
try {
await authInstance.delete(`/article/${articleId}/media`, {
data: { media_id: mediaId },
});
runInAction(() => {
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
const article = this.sight[lang].right.find(
(a) => a.id === articleId
);
if (article && article.media) {
article.media = article.media.filter((m) => m.id !== mediaId);
}
});
});
} catch (error) {
console.error("Error deleting media from right article:", error);
throw error;
}
};
// --- Left Article Management (largely unchanged from your provided store) ---
updateLeftInfo = (language: Language, heading: string, body: string) => {
this.sight[language].left.heading = heading;
this.sight[language].left.body = body;
};
unlinkLeftArticle = () => {
/* ... your existing logic ... */
this.sight.left_article = 0;
this.sight.ru.left = { heading: "", body: "", media: [] };
this.sight.en.left = { heading: "", body: "", media: [] };
this.sight.zh.left = { heading: "", body: "", media: [] };
};
updateLeftArticle = async (articleId: number) => {
/* ... your existing logic ... */
this.sight.left_article = articleId;
if (articleId) {
const [ruArticleData, enArticleData, zhArticleData, mediaData] =
await Promise.all([
languageInstance("ru").get(`/article/${articleId}`),
languageInstance("en").get(`/article/${articleId}`),
languageInstance("zh").get(`/article/${articleId}`),
authInstance.get(`/article/${articleId}/media`),
]);
runInAction(() => {
this.sight.ru.left = {
heading: ruArticleData.data.heading,
body: ruArticleData.data.body,
media: mediaData.data || [],
};
this.sight.en.left = {
heading: enArticleData.data.heading,
body: enArticleData.data.body,
media: mediaData.data || [],
};
this.sight.zh.left = {
heading: zhArticleData.data.heading,
body: zhArticleData.data.body,
media: mediaData.data || [],
};
});
} else {
this.unlinkLeftArticle();
}
};
deleteLeftArticle = async (articleId: number) => {
/* ... your existing logic ... */
await authInstance.delete(`/article/${articleId}`);
// articlesStore.getArticles(languageStore.language); // If still needed
this.unlinkLeftArticle();
};
createLeftArticle = async () => {
/* ... your existing logic to create a new left article (placeholder or DB) ... */
const response = await languageInstance("ru").post("/article", {
heading: "Новая левая статья",
body: "Заполните контентом",
});
const newLeftArticleId = response.data.id;
await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
heading: "New Left Article",
body: "Fill with content",
});
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
heading: "新的左侧文章",
body: "填写内容",
});
runInAction(() => {
this.sight.left_article = newLeftArticleId; // Store the actual ID
this.sight.ru.left = {
heading: "Новая левая статья",
body: "Заполните контентом",
media: [],
};
this.sight.en.left = {
heading: "New Left Article",
body: "Fill with content",
media: [],
};
this.sight.zh.left = {
heading: "新的左侧文章",
body: "填写内容",
media: [],
};
});
return newLeftArticleId;
};
// Placeholder for a "new" unsaved left article
setNewLeftArticlePlaceholder = () => {
this.sight.left_article = 10000000; // Special placeholder ID
this.sight.ru.left = {
heading: "Новая левая статья",
body: "Заполните контентом",
media: [],
};
this.sight.en.left = {
heading: "New Left Article",
body: "Fill with content",
media: [],
};
this.sight.zh.left = {
heading: "新的左侧文章",
body: "填写内容",
media: [],
};
};
// --- Sight Preview Media ---
linkPreviewMedia = (mediaId: string) => {
this.sight.preview_media = mediaId;
};
unlinkPreviewMedia = () => {
this.sight.preview_media = null;
};
// --- General Store Methods ---
clearCreateSight = () => {
this.sight = JSON.parse(JSON.stringify(initialSightState)); // Reset to initial
};
updateSightInfo = (
content: Partial<SightLanguageInfo | SightCommonInfo>, // Corrected types
language?: Language
) => {
if (language) {
this.sight[language] = { ...this.sight[language], ...content };
} else {
// Assuming content here is for SightCommonInfo
this.sight = { ...this.sight, ...(content as Partial<SightCommonInfo>) };
}
};
// --- Main Sight Creation Logic ---
createSight = async (primaryLanguage: Language) => {
let finalLeftArticleId = this.sight.left_article;
// 1. Handle Left Article (Create if new, or use existing ID)
if (this.sight.left_article === 10000000) {
// Placeholder for new
const res = await languageInstance("ru").post("/article", {
heading: this.sight.ru.left.heading,
body: this.sight.ru.left.body,
});
finalLeftArticleId = res.data.id;
await languageInstance("en").patch(`/article/${finalLeftArticleId}`, {
heading: this.sight.en.left.heading,
body: this.sight.en.left.body,
});
await languageInstance("zh").patch(`/article/${finalLeftArticleId}`, {
heading: this.sight.zh.left.heading,
body: this.sight.zh.left.body,
});
} else if (
this.sight.left_article !== 0 &&
this.sight.left_article !== null
) {
// Existing, ensure it's up-to-date
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 }
);
}
// else: left_article is 0, so no left article
// 2. Right articles are already created in DB and their IDs are in this.sight[lang].right.
// We just need to update their content if changed before saving the sight.
for (const lang of ["ru", "en", "zh"] as Language[]) {
for (const article of this.sight[lang].right) {
if (article.id == 0 || article.id == null) {
continue;
}
await languageInstance(lang).patch(`/article/${article.id}`, {
heading: article.heading,
body: article.body,
});
// Media for these articles are already linked via createLinkWithRightArticle
}
}
const rightArticleIdsForLink = this.sight[primaryLanguage].right.map(
(a) => a.id
);
// 3. Create Sight object in DB
const sightPayload = {
city_id: this.sight.city_id,
city: this.sight.city,
latitude: this.sight.latitude,
longitude: this.sight.longitude,
name: this.sight[primaryLanguage].name,
address: this.sight[primaryLanguage].address,
thumbnail: this.sight.thumbnail,
watermark_lu: this.sight.watermark_lu,
watermark_rd: this.sight.watermark_rd,
left_article: finalLeftArticleId === 0 ? null : finalLeftArticleId,
preview_media: this.sight.preview_media,
video_preview: this.sight.video_preview,
};
const response = await languageInstance(primaryLanguage).post(
"/sight",
sightPayload
);
const newSightId = response.data.id; // ID of the newly created sight
// 4. Update other languages for the sight
const otherLanguages = (["ru", "en", "zh"] as Language[]).filter(
(l) => l !== primaryLanguage
);
for (const lang of otherLanguages) {
await languageInstance(lang).patch(`/sight/${newSightId}`, {
city_id: this.sight.city_id,
city: this.sight.city,
latitude: this.sight.latitude,
longitude: this.sight.longitude,
name: this.sight[lang].name,
address: this.sight[lang].address,
thumbnail: this.sight.thumbnail,
watermark_lu: this.sight.watermark_lu,
watermark_rd: this.sight.watermark_rd,
left_article: finalLeftArticleId === 0 ? null : finalLeftArticleId,
preview_media: this.sight.preview_media,
video_preview: this.sight.video_preview,
});
}
// 5. Link Right Articles to the new Sight
for (let i = 0; i < rightArticleIdsForLink.length; i++) {
await authInstance.post(`/sight/${newSightId}/article`, {
article_id: rightArticleIdsForLink[i],
page_num: i + 1, // Or other logic for page_num
});
}
console.log("Sight created with ID:", newSightId);
// Optionally: this.clearCreateSight(); // To reset form after successful creation
return newSightId;
};
// --- Media Upload (Generic, used by dialogs) ---
uploadMedia = async (
filename: string,
type: number,
file: File,
media_name?: string
) => {
): Promise<MediaItem> => {
const formData = new FormData();
formData.append("file", file);
formData.append("filename", filename);
if (media_name) {
formData.append("media_name", media_name);
}
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();
runInAction(() => {
this.fileToUpload = null;
this.uploadMediaOpen = false;
});
mediaStore.getMedia(); // Refresh global media list
return {
id: response.data.id,
filename: filename,
media_name: media_name,
media_type: type,
filename: filename, // Or response.data.filename if backend returns it
media_name: media_name, // Or response.data.media_name
media_type: type, // Or response.data.type
};
} catch (error) {
console.log(error);
console.error("Error uploading media:", 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,
});
// For Left Article Media
createLinkWithLeftArticle = async (media: MediaItem) => {
if (!this.sight.left_article || this.sight.left_article === 10000000) {
console.warn(
"Left article not selected or is a placeholder. Cannot link media yet."
);
// If it's a placeholder, we could store the media temporarily and link it after the article is created.
// For simplicity, we'll assume the article must exist.
// A more robust solution might involve creating the article first if it's a placeholder.
return;
}
try {
await authInstance.post(`/article/${this.sight.left_article}/media`, {
media_id: media.id,
media_order: (this.sight.ru.left.media?.length || 0) + 1,
});
runInAction(() => {
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
if (!this.sight[lang].left.media) this.sight[lang].left.media = [];
this.sight[lang].left.media.unshift(media);
});
});
} catch (error) {
console.error("Error linking media to left article:", error);
throw error;
}
};
unlinkRightAritcle = async (id: number) => {
this.sight.ru.right = this.sight.ru.right.filter(
(article) => article.id !== id
);
this.sight.en.right = this.sight.en.right.filter(
(article) => article.id !== id
);
this.sight.zh.right = this.sight.zh.right.filter(
(article) => article.id !== id
);
};
deleteRightArticle = async (id: number) => {
await authInstance.delete(`/article/${id}`);
this.sight.ru.right = this.sight.ru.right.filter(
(article) => article.id !== id
);
this.sight.en.right = this.sight.en.right.filter(
(article) => article.id !== id
);
this.sight.zh.right = this.sight.zh.right.filter(
(article) => article.id !== id
);
deleteLeftArticleMedia = async (mediaId: string) => {
if (!this.sight.left_article || this.sight.left_article === 10000000)
return;
try {
await authInstance.delete(`/article/${this.sight.left_article}/media`, {
data: { media_id: mediaId },
});
runInAction(() => {
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
if (this.sight[lang].left.media) {
this.sight[lang].left.media = this.sight[lang].left.media.filter(
(m) => m.id !== mediaId
);
}
});
});
} catch (error) {
console.error("Error deleting media from left article:", error);
throw error;
}
};
}

View File

@ -88,7 +88,8 @@ class EditSightStore {
const response = await authInstance.get(`/sight/${id}`);
const data = response.data;
if (data.left_article != 0) {
if (data.left_article != 0 && data.left_article != null) {
await this.getLeftArticle(data.left_article);
}
@ -260,7 +261,10 @@ class EditSightStore {
});
this.sight.common.left_article = createdLeftArticleId;
} else if (this.sight.common.left_article != 0) {
} else if (
this.sight.common.left_article != 0 &&
this.sight.common.left_article != null
) {
await languageInstance("ru").patch(
`/article/${this.sight.common.left_article}`,
{
@ -306,6 +310,9 @@ class EditSightStore {
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,

View File

@ -0,0 +1,182 @@
import React, { useRef, useState, DragEvent, useEffect } from "react";
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
import { X, Info } from "lucide-react"; // Assuming lucide-react for icons
import { editSightStore } from "@shared";
interface ImageUploadCardProps {
title: string;
imageKey?: "thumbnail" | "watermark_lu" | "watermark_rd";
imageUrl: string | null | undefined;
onImageClick: () => void;
onDeleteImageClick: () => void;
onSelectFileClick: () => void;
setUploadMediaOpen: (open: boolean) => void;
tooltipText?: string;
}
export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
title,
imageUrl,
onImageClick,
onDeleteImageClick,
onSelectFileClick,
setUploadMediaOpen,
tooltipText,
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragOver, setIsDragOver] = useState(false);
const { setFileToUpload } = editSightStore;
useEffect(() => {
if (isDragOver) {
console.log("isDragOver");
}
}, [isDragOver]);
// --- Click to select file ---
const handleZoneClick = () => {
// Trigger the hidden file input click
fileInputRef.current?.click();
};
const handleFileInputChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files?.[0];
if (file) {
setFileToUpload(file);
setUploadMediaOpen(true);
}
// Reset the input value so selecting the same file again triggers change
event.target.value = "";
};
const token = localStorage.getItem("token");
// --- Drag and Drop Handlers ---
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Crucial to allow a drop
event.stopPropagation();
setIsDragOver(true);
};
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
setIsDragOver(false);
};
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Crucial to allow a drop
event.stopPropagation();
setIsDragOver(false);
const files = event.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
setFileToUpload(file);
setUploadMediaOpen(true);
}
};
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>
{tooltipText && (
<Tooltip title={tooltipText}>
<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: imageUrl ? "pointer" : "default",
}}
onClick={onImageClick}
// Removed onClick on the main Box to avoid conflicts
>
{imageUrl && (
<button
className="absolute top-2 right-2"
onClick={(e) => {
e.stopPropagation();
onDeleteImageClick();
}}
>
<X color="red" />
</button>
)}
{imageUrl ? (
<img
src={`${
import.meta.env.VITE_KRBL_MEDIA
}${imageUrl}/download?token=${token}`}
alt={title}
style={{ maxWidth: "100%", maxHeight: "100%" }}
onClick={onImageClick}
/>
) : (
<div
className={`w-full flex flex-col items-center justify-center gap-3 `}
>
<div
className="flex flex-col p-5 items-center justify-center gap-3"
style={{
border: "2px dashed #ccc",
borderRadius: 1,
cursor: "pointer",
}}
onClick={handleZoneClick} // Click handler for the zone
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<p>Перетащите файл</p>
</div>
<p>или</p>
<Button
variant="contained"
color="primary"
onClick={(e) => {
e.stopPropagation(); // Prevent `handleZoneClick` from firing
onSelectFileClick(); // This button might trigger a different modal
}}
>
Выбрать файл
</Button>
{/* Hidden file input */}
<input
type="file"
ref={fileInputRef}
onChange={handleFileInputChange}
style={{ display: "none" }}
accept="image/*" // Accept only image files
/>
</div>
)}
</Box>
</Paper>
);
};

View File

@ -3,9 +3,6 @@ import {
TextField,
Box,
Autocomplete,
Typography,
Paper,
Tooltip,
MenuItem,
Menu as MuiMenu,
} from "@mui/material";
@ -20,9 +17,9 @@ import {
SightLanguageInfo,
SightCommonInfo,
createSightStore,
UploadMediaDialog,
} from "@shared";
import { LanguageSwitcher } from "@widgets";
import { Info, X } from "lucide-react";
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
@ -33,20 +30,16 @@ 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 [isUploadMediaOpen, setIsUploadMediaOpen] = 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<
@ -109,6 +102,7 @@ export const CreateInformationTab = observer(
});
setActiveMenuType(null);
};
return (
<>
<TabPanel value={value} index={index}>
@ -242,271 +236,77 @@ export const CreateInformationTab = observer(
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
<ImageUploadCard
title="Логотип"
imageKey="thumbnail"
imageUrl={sight.thumbnail}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(sight.thumbnail ?? "");
}}
>
<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
onDeleteImageClick={() => {
handleChange({
thumbnail: null,
});
setActiveMenuType(null);
}}
>
<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
onSelectFileClick={() => {
setActiveMenuType("thumbnail");
setIsAddMediaOpen(true);
}}
>
<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",
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
}}
/>
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>
<ImageUploadCard
title="Водяной знак (л.в)"
imageKey="watermark_lu"
imageUrl={sight.watermark_lu}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(sight.watermark_lu ?? "");
}}
onDeleteImageClick={() => {
handleChange({
watermark_lu: null,
});
setActiveMenuType(null);
}}
onSelectFileClick={() => {
setActiveMenuType("watermark_lu");
setIsAddMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_lu");
}}
/>
<ImageUploadCard
title="Водяной знак (п.в)"
imageKey="watermark_rd"
imageUrl={sight.watermark_rd}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(sight.watermark_rd ?? "");
}}
onDeleteImageClick={() => {
handleChange({
watermark_rd: null,
});
setActiveMenuType(null);
}}
onSelectFileClick={() => {
setActiveMenuType("watermark_rd");
setIsAddMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_rd");
}}
/>
</Box>
</Box>
</Box>
@ -576,6 +376,18 @@ export const CreateInformationTab = observer(
onClose={() => setIsPreviewMediaOpen(false)}
mediaId={mediaId}
/>
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
afterUpload={(media) => {
handleChange({
[activeMenuType ?? "thumbnail"]: media.id,
});
setActiveMenuType(null);
setIsUploadMediaOpen(false);
}}
/>
</>
);
}

View File

@ -32,7 +32,7 @@ export const CreateLeftTab = observer(
deleteLeftArticle,
createLeftArticle,
unlinkLeftArticle,
createLinkWithArticle,
createLinkWithLeftArticle,
} = createSightStore;
const {
deleteMedia,
@ -75,10 +75,10 @@ export const CreateLeftTab = observer(
media_name?: string;
media_type: number;
}) => {
await createLinkWithArticle(media);
await createLinkWithLeftArticle(media);
setIsSelectMediaDialogOpen(false);
},
[createLinkWithArticle]
[createLinkWithLeftArticle]
);
const handleArticleSelect = useCallback(
@ -437,7 +437,7 @@ export const CreateLeftTab = observer(
afterUpload={async (media) => {
setUploadMediaOpen(false);
setFileToUpload(null);
await createLinkWithArticle(media);
await createLinkWithLeftArticle(media);
}}
/>
<SelectArticleModal

View File

@ -13,42 +13,179 @@ import {
languageStore,
SelectArticleModal,
TabPanel,
SelectMediaDialog, // Import
UploadMediaDialog, // Import
} from "@shared";
import {
LanguageSwitcher,
MediaArea, // Import
MediaAreaForSight, // Import
ReactMarkdownComponent,
ReactMarkdownEditor,
} from "@widgets";
import { ImagePlus, Plus } from "lucide-react";
import { ImagePlus, Plus, X } from "lucide-react"; // Import X
import { observer } from "mobx-react-lite";
import { useState } from "react";
import { useState, useEffect } from "react"; // Added useEffect
import { MediaViewer } from "../../MediaViewer/index";
import { toast } from "react-toastify";
type MediaItemShared = {
// Define if not already available from @shared
id: string;
filename: string;
media_name?: string;
media_type: number;
};
// --- 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 {
sight,
createNewRightArticle,
updateRightArticleInfo,
linkPreviewMedia,
unlinkPreviewMedia,
createLinkWithRightArticle,
deleteRightArticleMedia,
setFileToUpload, // From store
setUploadMediaOpen, // From store
uploadMediaOpen, // From store
unlinkRightAritcle, // Corrected spelling
deleteRightArticle,
linkExistingRightArticle,
createSight,
clearCreateSight, // For resetting form
} = createSightStore;
const { language } = languageStore;
const [articleDialogOpen, setArticleDialogOpen] = useState(false);
const [selectArticleDialogOpen, setSelectArticleDialogOpen] =
useState(false);
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
null
);
const [type, setType] = useState<"article" | "media">("media");
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
useState(false);
const [mediaTarget, setMediaTarget] = useState<
"sightPreview" | "rightArticle" | null
>(null);
// Reset activeArticleIndex if language changes and index is out of bounds
useEffect(() => {
if (
activeArticleIndex !== null &&
activeArticleIndex >= sight[language].right.length
) {
setActiveArticleIndex(null);
setType("media"); // Default back to media preview if selected article disappears
}
}, [language, sight[language].right, activeArticleIndex]);
const openMenu = Boolean(anchorEl);
const handleClickMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
const handleCloseMenu = () => {
setAnchorEl(null);
};
const handleSave = () => {
console.log("Saving right widget...");
const handleSave = async () => {
try {
await createSight(language);
toast.success("Достопримечательность успешно создана!");
clearCreateSight(); // Reset form
setActiveArticleIndex(null);
setType("media");
// Potentially navigate away: history.push('/sights-list');
} catch (error) {
console.error("Failed to save sight:", error);
toast.error("Ошибка при создании достопримечательности.");
}
};
const handleSelectArticle = (index: number) => {
setActiveArticleIndex(index);
const handleDisplayArticleFromList = (idx: number) => {
setActiveArticleIndex(idx);
setType("article");
};
const handleCreateNewLocalArticle = async () => {
handleCloseMenu();
try {
const newArticleId = await createNewRightArticle();
// Automatically select the new article if ID is returned
const newIndex = sight[language].right.findIndex(
(a) => a.id === newArticleId
);
if (newIndex > -1) {
setActiveArticleIndex(newIndex);
setType("article");
} else {
// Fallback if findIndex fails (should not happen if store updates correctly)
setActiveArticleIndex(sight[language].right.length - 1);
setType("article");
}
} catch (error) {
toast.error("Не удалось создать новую статью.");
}
};
const handleSelectExistingArticleAndLink = async (
selectedArticleId: number
) => {
try {
await linkExistingRightArticle(selectedArticleId);
setSelectArticleDialogOpen(false); // Close dialog
const newIndex = sight[language].right.findIndex(
(a) => a.id === selectedArticleId
);
if (newIndex > -1) {
setActiveArticleIndex(newIndex);
setType("article");
}
} catch (error) {
toast.error("Не удалось привязать существующую статью.");
}
};
const currentRightArticle =
activeArticleIndex !== null && sight[language].right[activeArticleIndex]
? sight[language].right[activeArticleIndex]
: null;
// Media Handling for Dialogs
const handleOpenUploadMedia = () => {
setUploadMediaOpen(true);
};
const handleOpenSelectMediaDialog = (
target: "sightPreview" | "rightArticle"
) => {
setMediaTarget(target);
setIsSelectMediaDialogOpen(true);
};
const handleMediaSelectedFromDialog = async (media: MediaItemShared) => {
setIsSelectMediaDialogOpen(false);
if (mediaTarget === "sightPreview") {
await linkPreviewMedia(media.id);
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
await createLinkWithRightArticle(media, currentRightArticle.id);
}
setMediaTarget(null);
};
const handleMediaUploaded = async (media: MediaItemShared) => {
// After UploadMediaDialog finishes
setUploadMediaOpen(false);
setFileToUpload(null);
if (mediaTarget === "sightPreview") {
linkPreviewMedia(media.id);
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
await createLinkWithRightArticle(media, currentRightArticle.id);
}
setMediaTarget(null); // Reset target
};
return (
@ -59,7 +196,7 @@ export const CreateRightTab = observer(
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
position: "relative",
@ -68,336 +205,389 @@ export const CreateRightTab = observer(
<BackButton />
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
{/* Left Column: Navigation & Article List */}
<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="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");
// setActiveArticleIndex(null); // Optional: deselect article when switching to general media view
}}
className="w-full bg-green-200 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 hover:bg-gray-300 transition-all duration-300 ${
type === "media"
? "bg-green-300 font-semibold"
: "bg-green-200"
}`}
>
<Typography>Предпросмотр медиа</Typography>
</Box>
{sight[language].right.map((article, index) => (
{sight[language].right.map((article, artIdx) => (
<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);
setType("article");
}}
key={article.id || artIdx} // article.id should be preferred
className={`w-full p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300 ${
type === "article" && activeArticleIndex === artIdx
? "bg-blue-300 font-semibold"
: "bg-gray-200"
}`}
onClick={() => handleDisplayArticleFromList(artIdx)}
>
<Typography>{article.heading}</Typography>
<Typography noWrap title={article.heading}>
{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}
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center hover:bg-blue-600"
onClick={handleClickMenu}
aria-controls={openMenu ? "add-article-menu" : undefined}
aria-haspopup="true"
aria-expanded={openMenu ? "true" : undefined}
>
<Plus size={20} color="white" />
</button>
<Menu
id="basic-menu"
id="add-article-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
"aria-labelledby": "basic-button",
}}
sx={{
mt: 1,
}}
open={openMenu}
onClose={handleCloseMenu}
MenuListProps={{ "aria-labelledby": "basic-button" }}
sx={{ mt: 1 }}
>
<MenuItem
onClick={() => {
createNewRightArticle();
handleClose();
}}
>
<MenuItem onClick={handleCreateNewLocalArticle}>
<Typography>Создать новую</Typography>
</MenuItem>
<MenuItem
onClick={() => {
setArticleDialogOpen(true);
handleClose();
setSelectArticleDialogOpen(true);
handleCloseMenu();
}}
>
<Typography>Выбрать существующую статью</Typography>
</MenuItem>
</Menu>
</Box>
{type === "article" && (
<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 && (
{/* Main content area: Article Editor or Sight Media Preview */}
{type === "article" && currentRightArticle ? (
<Box className="w-[80%] border border-gray-300 rounded-2xl p-3 flex flex-col gap-2 overflow-hidden">
<Box className="flex justify-end gap-2 mb-1 flex-shrink-0">
<Button
variant="outlined"
color="warning"
size="small"
onClick={() => {
if (currentRightArticle) {
unlinkRightAritcle(currentRightArticle.id); // Corrected function name
setActiveArticleIndex(null);
setType("media");
}
}}
>
Открепить
</Button>
<Button
variant="contained"
color="error"
size="small"
sx={{ ml: 1 }}
onClick={() =>
updateSightInfo(
languageStore.language,
{
left: {
heading: data.left.heading,
body: data.left.body,
media: null,
},
},
false
onClick={async () => {
if (
currentRightArticle &&
window.confirm(
`Удалить статью "${currentRightArticle.heading}" окончательно?`
)
) {
try {
await deleteRightArticle(currentRightArticle.id);
setActiveArticleIndex(null);
setType("media");
toast.success("Статья удалена");
} catch {
toast.error("Не удалось удалить статью");
}
}
}}
>
Удалить
</Button>
</Box>
<Box
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
gap: 2,
overflowY: "auto",
pt: 7,
}}
>
<TextField
label="Название информации (правый виджет)"
value={currentRightArticle.heading}
onChange={(e) =>
activeArticleIndex !== null &&
updateRightArticleInfo(
activeArticleIndex,
language,
e.target.value,
currentRightArticle.body
)
}
>
Удалить медиа
</Button>
)}
</Paper> */}
</Box>
</>
variant="outlined"
fullWidth
/>
<Box sx={{ minHeight: 200, flexGrow: 1 }}>
<ReactMarkdownEditor
value={currentRightArticle.body}
onChange={(mdValue) =>
activeArticleIndex !== null &&
updateRightArticleInfo(
activeArticleIndex,
language,
currentRightArticle.heading,
mdValue || ""
)
}
/>
</Box>
<MediaArea
articleId={currentRightArticle.id} // Needs a real ID
mediaIds={currentRightArticle.media || []}
onFilesDrop={(files) => {
if (files.length > 0) {
setFileToUpload(files[0]);
setMediaTarget("rightArticle");
handleOpenUploadMedia();
}
}}
deleteMedia={deleteRightArticleMedia}
setSelectMediaDialogOpen={() =>
handleOpenSelectMediaDialog("rightArticle")
}
/>
</Box>
</Box>
) : type === "media" ? (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 relative flex justify-center items-center">
{type === "media" && (
<Box className="w-[80%] border border-gray-300 rounded-2xl relative">
{sight.preview_media && (
<>
<Box className="absolute top-4 right-4">
<button
className="w-10 h-10 flex items-center justify-center"
onClick={unlinkPreviewMedia}
>
<X size={20} color="red" />
</button>
</Box>
<MediaViewer
media={{
id: sight.preview_media || "",
media_type: 1,
filename: sight.preview_media || "",
}}
/>
</>
)}
{!sight.preview_media && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
)}
</Box>
)}
</Box>
)}
{type === "media" && (
) : (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center">
<MediaViewer
media={{
id: sight.preview_media || "",
media_type: 1,
filename: sight.preview_media || "",
}}
/>
<Typography variant="h6" color="text.secondary">
Выберите статью слева или секцию "Предпросмотр медиа"
</Typography>
</Box>
)}
</Box>
</Box>
{/* Right Column: Live Preview */}
<Box className="w-[25%] mr-10">
{activeArticleIndex !== null && (
{type === "article" && currentRightArticle && (
<Paper
className="flex-1 flex flex-col rounded-2xl"
elevation={2}
sx={{ height: "75vh", overflow: "hidden" }}
>
<Box
className="rounded-2xl overflow-hidden"
sx={{
width: "100%",
height: "75vh",
background: "#877361",
borderColor: "grey.300",
height: "100%",
background: "#877361", // Theme background
display: "flex",
flexDirection: "column",
}}
>
{type === "media" ? (
{currentRightArticle.media &&
currentRightArticle.media.length > 0 ? (
<MediaViewer media={currentRightArticle.media[0]} />
) : (
<Box
sx={{
width: "100%",
height: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Typography color="white">Загрузка...</Typography>
<ImagePlus size={48} color="white" />
</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
sx={{
width: "100%",
minHeight: "70px", // Fixed height for heading container
background: "#877361", // Consistent with theme
display: "flex",
flexShrink: 0,
flex: 1,
alignItems: "center",
borderBottom: "1px solid rgba(255,255,255,0.1)",
px: 2,
py: 1,
}}
>
<Typography
variant="h6"
color="white"
noWrap
title={currentRightArticle.heading}
>
{currentRightArticle.heading || "Заголовок"}
</Typography>
</Box>
<Box
sx={{
px: 2,
py: 1,
flexGrow: 1,
overflowY: "auto",
backgroundColor: "#877361",
color: "white",
"&::-webkit-scrollbar": { width: "8px" },
"&::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(255,255,255,0.3)",
borderRadius: "4px",
},
}}
>
{currentRightArticle.body ? (
<ReactMarkdownComponent
value={currentRightArticle.body}
/>
) : (
<Typography
color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }}
>
Содержимое статьи...
</Typography>
)}
</Box>
</Box>
</Paper>
)}
{/* Optional: Preview for sight.preview_media when type === "media" */}
{type === "media" && sight.preview_media && (
<Paper
className="flex-1 flex flex-col rounded-2xl"
elevation={2}
sx={{ height: "75vh", overflow: "hidden" }}
>
<Box
sx={{
width: "100%",
height: "100%",
background: "#877361",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<MediaViewer
media={{
id: sight.preview_media,
filename: sight.preview_media,
media_type: 1,
}}
/>
</Box>
</Paper>
)}
</Box>
</Box>
{/* Sticky Save Button Footer */}
<Box
sx={{
position: "absolute",
bottom: 0,
left: 0, // ensure it spans from left
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",
borderTop: "1px solid", // Add a subtle top border
borderColor: "divider", // Use theme's divider color
width: "100%",
display: "flex",
justifyContent: "flex-end",
boxShadow: "0 -2px 5px rgba(0,0,0,0.1)", // Optional shadow
}}
>
<Button variant="contained" color="success" onClick={handleSave}>
Сохранить изменения
<Button
variant="contained"
color="success"
onClick={handleSave}
size="large"
>
Сохранить достопримечательность
</Button>
</Box>
</Box>
{/*
{/* Modals */}
<SelectArticleModal
open={openedType === "article"}
onClose={handleCloseSelectModal}
onSelectArticle={handleSelectArticle}
linkedArticleIds={linkedArticleIds}
/> */}
<SelectArticleModal
open={articleDialogOpen}
onClose={() => setArticleDialogOpen(false)}
onSelectArticle={handleSelectArticle}
open={selectArticleDialogOpen}
onClose={() => setSelectArticleDialogOpen(false)}
onSelectArticle={handleSelectExistingArticleAndLink}
// Pass IDs of already linked/added right articles to exclude them from selection
linkedArticleIds={sight[language].right.map((article) => article.id)}
/>
<UploadMediaDialog
open={uploadMediaOpen} // From store
onClose={() => {
setUploadMediaOpen(false);
setFileToUpload(null); // Clear file if dialog is closed without upload
setMediaTarget(null);
}}
afterUpload={handleMediaUploaded} // This will use the mediaTarget
/>
<SelectMediaDialog
open={isSelectMediaDialogOpen}
onClose={() => {
setIsSelectMediaDialogOpen(false);
setMediaTarget(null);
}}
onSelectMedia={handleMediaSelectedFromDialog}
/>
</TabPanel>
);
}

View File

@ -3,9 +3,6 @@ import {
TextField,
Box,
Autocomplete,
Typography,
Paper,
Tooltip,
MenuItem,
Menu as MuiMenu,
} from "@mui/material";
@ -20,9 +17,9 @@ import {
PreviewMediaDialog,
SightLanguageInfo,
SightCommonInfo,
UploadMediaDialog,
} from "@shared";
import { LanguageSwitcher } from "@widgets";
import { Info, ImagePlus } from "lucide-react";
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
@ -34,10 +31,10 @@ import { toast } from "react-toastify";
export const InformationTab = 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 [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
const { language } = languageStore;
const { sight, updateSightInfo, updateSight } = editSightStore;
@ -45,8 +42,6 @@ export const InformationTab = observer(
const [, setCity] = useState<number>(sight.common.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<
@ -76,12 +71,22 @@ export const InformationTab = observer(
handleMenuClose();
};
const handleMediaSelect = () => {
const handleMediaSelect = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
if (!activeMenuType) return;
// Close the dialog
setIsAddMediaOpen(false);
handleChange(
language as Language,
{
[activeMenuType ?? "thumbnail"]: media.id,
},
true
);
setActiveMenuType(null);
setIsUploadMediaOpen(false);
};
const handleChange = (
@ -225,208 +230,87 @@ export const InformationTab = observer(
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
<ImageUploadCard
title="Логотип"
imageKey="thumbnail"
imageUrl={sight.common.thumbnail}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(sight.common.thumbnail ?? "");
}}
>
<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",
backgroundColor: "grey.200",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 1,
mb: 1,
cursor: sight.common.thumbnail ? "pointer" : "default",
"&:hover": {
backgroundColor: sight.common.thumbnail
? "red.300"
: "grey.200",
onDeleteImageClick={() => {
handleChange(
language as Language,
{
thumbnail: null,
},
}}
onClick={() => {
setIsMediaModalOpen(true);
}}
>
{sight.common.thumbnail ? (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
sight.common.thumbnail
}/download?token=${token}`}
alt="Логотип"
style={{ maxWidth: "100%", maxHeight: "100%" }}
onClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(sight.common.thumbnail ?? "");
}}
/>
) : (
<ImagePlus size={24} color="grey" />
)}
</Box>
</Paper>
<Paper
elevation={2}
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
true
);
setActiveMenuType(null);
}}
>
<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",
backgroundColor: "grey.200",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 1,
mb: 1,
cursor: sight.common.watermark_lu
? "pointer"
: "default",
"&:hover": {
backgroundColor: sight.common.watermark_lu
? "grey.300"
: "grey.200",
},
}}
onClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(sight.common.watermark_lu ?? "");
}}
>
{sight.common.watermark_lu ? (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
sight.common.watermark_lu
}/download?token=${token}`}
alt="Знак л.в"
style={{ maxWidth: "100%", maxHeight: "100%" }}
onClick={() => {
setIsMediaModalOpen(true);
setMediaId(sight.common.watermark_lu ?? "");
}}
/>
) : (
<ImagePlus size={24} color="grey" />
)}
</Box>
</Paper>
<Paper
elevation={2}
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
onSelectFileClick={() => {
setActiveMenuType("thumbnail");
setIsAddMediaOpen(true);
}}
>
<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",
backgroundColor: "grey.200",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 1,
mb: 1,
cursor: sight.common.watermark_rd
? "pointer"
: "default",
"&:hover": {
backgroundColor: sight.common.watermark_rd
? "grey.300"
: "grey.200",
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
}}
/>
<ImageUploadCard
title="Водяной знак (л.в)"
imageKey="watermark_lu"
imageUrl={sight.common.watermark_lu}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(sight.common.watermark_lu ?? "");
}}
onDeleteImageClick={() => {
handleChange(
language as Language,
{
watermark_lu: null,
},
}}
onClick={() => {
setIsMediaModalOpen(true);
setMediaId(sight.common.watermark_rd ?? "");
}}
>
{sight.common.watermark_rd ? (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
sight.common.watermark_rd
}/download?token=${token}`}
alt="Знак п.в"
style={{ maxWidth: "100%", maxHeight: "100%" }}
onClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(sight.common.watermark_rd ?? "");
}}
/>
) : (
<ImagePlus size={24} color="grey" />
)}
</Box>
</Paper>
true
);
setActiveMenuType(null);
}}
onSelectFileClick={() => {
setActiveMenuType("watermark_lu");
setIsAddMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_lu");
}}
/>
<ImageUploadCard
title="Водяной знак (п.в)"
imageKey="watermark_rd"
imageUrl={sight.common.watermark_rd}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(sight.common.watermark_rd ?? "");
}}
onDeleteImageClick={() => {
handleChange(
language as Language,
{
watermark_rd: null,
},
true
);
setActiveMenuType(null);
}}
onSelectFileClick={() => {
setActiveMenuType("watermark_rd");
setIsAddMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_rd");
}}
/>
</Box>
</Box>
</Box>
@ -489,6 +373,21 @@ export const InformationTab = observer(
onSelectMedia={handleMediaSelect}
/>
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
afterUpload={(media) => {
handleChange(
language as Language,
{
[activeMenuType ?? "thumbnail"]: media.id,
},
true
);
setActiveMenuType(null);
setIsUploadMediaOpen(false);
}}
/>
<PreviewMediaDialog
open={isPreviewMediaOpen}
onClose={() => setIsPreviewMediaOpen(false)}

View File

@ -9,7 +9,6 @@ import {
} from "@mui/material";
import {
BackButton,
createSightStore,
editSightStore,
languageStore,
SelectArticleModal,
@ -348,6 +347,7 @@ export const RightWidgetTab = observer(
width: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
@ -361,10 +361,11 @@ export const RightWidgetTab = observer(
<Box
sx={{
width: "100%",
minHeight: "70px",
height: "70px",
background: "#877361",
display: "flex",
flexShrink: 0,
alignItems: "center",
borderBottom: "1px solid rgba(255,255,255,0.1)",
px: 2,
@ -380,6 +381,8 @@ export const RightWidgetTab = observer(
sx={{
px: 2,
flexGrow: 1,
flex: 1,
minHeight: "300px",
overflowY: "auto",
backgroundColor: "#877361",
color: "white",

View File

@ -11,3 +11,4 @@ export * from "./MediaViewer";
export * from "./MediaArea";
export * from "./ModelViewer3D";
export * from "./MediaAreaForSight";
export * from "./ImageUploadCard";

View File

@ -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/createmediapage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editmediapage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/medialistpage/index.tsx","./src/pages/previewmediapage/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"}
{"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/createmediapage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editmediapage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/medialistpage/index.tsx","./src/pages/previewmediapage/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/imageuploadcard/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/mediaareaforsight/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"}