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 { interface NavigationItemProps {
item: NavigationItem; item: NavigationItem;
open: boolean; open: boolean;
onClick?: () => void;
} }
export const NavigationItemComponent: React.FC<NavigationItemProps> = ({ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
item, item,
open, open,
onClick,
}) => { }) => {
const Icon = item.icon; const Icon = item.icon;
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<ListItem <ListItem
onClick={() => navigate(item.path)} onClick={() => {
if (onClick) {
onClick();
} else {
navigate(item.path);
}
}}
disablePadding disablePadding
sx={{ display: "block" }} sx={{ display: "block" }}
> >

View File

@ -25,6 +25,7 @@ export const NavigationList = ({ open }: { open: boolean }) => {
key={item.id} key={item.id}
item={item as NavigationItem} item={item as NavigationItem}
open={open} open={open}
onClick={item.onClick ? item.onClick : undefined}
/> />
))} ))}
</List> </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 { TableRow, TableCell } from "@mui/material";
import { Table, TableHead } from "@mui/material"; import { Table, TableHead } from "@mui/material";
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared"; import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
@ -27,15 +27,6 @@ export const MediaListPage = observer(() => {
return ( 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"> <Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead> <TableHead>
<TableRow> <TableRow>

View File

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

View File

@ -1,13 +1,13 @@
// @shared/stores/createSightStore.ts // @shared/stores/createSightStore.ts
import { import { Language, authInstance, languageInstance, mediaStore } from "@shared";
Language, import { makeAutoObservable, runInAction } from "mobx";
authInstance,
languageInstance, type MediaItem = {
articlesStore, id: string;
languageStore, filename: string;
mediaStore, media_name?: string;
} from "@shared"; media_type: number;
import { makeAutoObservable } from "mobx"; };
type SightLanguageInfo = { type SightLanguageInfo = {
name: string; name: string;
@ -15,18 +15,13 @@ type SightLanguageInfo = {
left: { left: {
heading: string; heading: string;
body: string; body: string;
media: { media: MediaItem[];
id: string;
filename: string;
media_name?: string;
media_type: number;
}[];
}; };
right: { id: number; heading: string; body: string; media: [] }[]; right: { id: number; heading: string; body: string; media: MediaItem[] }[];
}; };
type SightCommonInfo = { type SightCommonInfo = {
id: number; // id: number; // ID is 0 until created
city_id: number; city_id: number;
city: string; city: string;
latitude: number; latitude: number;
@ -34,18 +29,18 @@ type SightCommonInfo = {
thumbnail: string | null; thumbnail: string | null;
watermark_lu: string | null; watermark_lu: string | null;
watermark_rd: 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; preview_media: string | null;
video_preview: 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 & { type SightBaseInfo = SightCommonInfo & {
[key in Language]: SightLanguageInfo; [key in Language]: SightLanguageInfo;
}; };
class CreateSightStore { const initialSightState: SightBaseInfo = {
sight: SightBaseInfo = {
id: 0,
city_id: 0, city_id: 0,
city: "", city: "",
latitude: 0, latitude: 0,
@ -56,7 +51,6 @@ class CreateSightStore {
left_article: 0, left_article: 0,
preview_media: null, preview_media: null,
video_preview: null, video_preview: null,
ru: { ru: {
name: "", name: "",
address: "", address: "",
@ -77,6 +71,9 @@ class CreateSightStore {
}, },
}; };
class CreateSightStore {
sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState)); // Deep copy for reset
uploadMediaOpen = false; uploadMediaOpen = false;
setUploadMediaOpen = (open: boolean) => { setUploadMediaOpen = (open: boolean) => {
this.uploadMediaOpen = open; this.uploadMediaOpen = open;
@ -90,313 +87,74 @@ class CreateSightStore {
makeAutoObservable(this); makeAutoObservable(this);
} }
// --- Right Article Management ---
createNewRightArticle = async () => { createNewRightArticle = async () => {
const articleId = await languageInstance("ru").post("/article", { // Create article in DB for all languages
heading: "Введите русский заголовок", const articleRuData = {
body: "Введите русский текст", heading: "Новый заголовок (RU)",
}); body: "Новый текст (RU)",
const { id } = articleId.data; };
await languageInstance("en").patch(`/article/${id}`, { const articleEnData = {
heading: "Enter the English heading", heading: "New Heading (EN)",
body: "Enter the English text", body: "New Text (EN)",
}); };
await languageInstance("zh").patch(`/article/${id}`, { const articleZhData = { heading: "新标题 (ZH)", body: "新文本 (ZH)" };
heading: "Введите китайский заголовок",
body: "Введите китайский текст",
});
await authInstance.post(`/sight/${this.sight.id}/article`, {
article_id: id,
page_num: this.sight.ru.right.length + 1,
});
try {
const articleRes = await languageInstance("ru").post(
"/article",
articleRuData
);
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 });
this.sight.en.right.push({ ...newArticleEntry, ...articleEnData });
this.sight.zh.right.push({ ...newArticleEntry, ...articleZhData });
});
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({ this.sight.ru.right.push({
id: id, id: articleId,
heading: "Введите русский заголовок", heading: ruData.data.heading,
body: "Введите русский текст", body: ruData.data.body,
media: [], media: mediaData,
}); });
this.sight.en.right.push({ this.sight.en.right.push({
id: id, id: articleId,
heading: "Enter the English heading", heading: enData.data.heading,
body: "Enter the English text", body: enData.data.body,
media: [], media: mediaData,
}); });
this.sight.zh.right.push({ this.sight.zh.right.push({
id: id, id: articleId,
heading: "Введите китайский заголовок", heading: zhData.data.heading,
body: "Введите китайский текст", body: zhData.data.body,
media: [], media: mediaData,
}); });
};
updateLeftInfo = (language: Language, heading: string, body: string) => {
this.sight[language].left.heading = heading;
this.sight[language].left.body = body;
};
clearCreateSight = () => {
this.sight = {
id: 0,
city_id: 0,
city: "",
latitude: 0,
longitude: 0,
thumbnail: null,
watermark_lu: null,
watermark_rd: null,
left_article: 0,
preview_media: null,
video_preview: null,
ru: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
en: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
zh: {
name: "",
address: "",
left: { heading: "", body: "", media: [] },
right: [],
},
};
};
updateSightInfo = (
content: Partial<SightLanguageInfo | SightCommonInfo>,
language?: Language
) => {
if (language) {
this.sight[language] = {
...this.sight[language],
...content,
};
} else {
this.sight = {
...this.sight,
...content,
};
}
};
unlinkLeftArticle = () => {
this.sight.left_article = 0;
this.sight.ru.left.heading = "";
this.sight.en.left.heading = "";
this.sight.zh.left.heading = "";
this.sight.ru.left.body = "";
this.sight.en.left.body = "";
this.sight.zh.left.body = "";
};
updateLeftArticle = async (articleId: number) => {
this.sight.left_article = articleId;
if (articleId) {
const ruArticleData = await languageInstance("ru").get(
`/article/${articleId}`
);
const enArticleData = await languageInstance("en").get(
`/article/${articleId}`
);
const zhArticleData = await languageInstance("zh").get(
`/article/${articleId}`
);
this.sight.ru.left.heading = ruArticleData.data.heading;
this.sight.en.left.heading = enArticleData.data.heading;
this.sight.zh.left.heading = zhArticleData.data.heading;
this.sight.ru.left.body = ruArticleData.data.body;
this.sight.en.left.body = enArticleData.data.body;
this.sight.zh.left.body = zhArticleData.data.body;
} else {
this.sight.left_article = 0;
this.sight.ru.left.heading = "";
this.sight.en.left.heading = "";
this.sight.zh.left.heading = "";
this.sight.ru.left.body = "";
this.sight.en.left.body = "";
this.sight.zh.left.body = "";
}
};
deleteLeftArticle = async (articleId: number) => {
await authInstance.delete(`/article/${articleId}`);
articlesStore.getArticles(languageStore.language);
this.sight.left_article = 0;
this.sight.ru.left.heading = "";
this.sight.en.left.heading = "";
this.sight.zh.left.heading = "";
this.sight.ru.left.body = "";
};
createLeftArticle = async () => {
const response = await languageInstance("ru").post("/article", {
heading: "Новая статья",
body: "Заполните статью контентом",
}); });
this.sight.left_article = response.data.id;
this.sight.ru.left.heading = "Новая статья ";
this.sight.en.left.heading = "";
this.sight.zh.left.heading = "";
this.sight.ru.left.body = "Заполните статью контентом";
this.sight.en.left.body = "";
this.sight.zh.left.body = "";
};
createSight = async (language: Language) => {
const rightArticles: number[] = [];
if (this.sight.left_article !== 0) {
if (this.sight.left_article == 10000000) {
const response = await languageInstance("ru").post("/article", {
heading: this.sight.ru.left.heading,
body: this.sight.ru.left.body,
});
const { id } = response.data;
await languageInstance("en").patch(`/article/${id}`, {
heading: this.sight.en.left.heading,
body: this.sight.en.left.body,
});
await languageInstance("zh").patch(`/article/${id}`, {
heading: this.sight.zh.left.heading,
body: this.sight.zh.left.body,
});
this.sight.left_article = id;
} else {
await languageInstance("ru").patch(
`/article/${this.sight.left_article}`,
{
heading: this.sight.ru.left.heading,
body: this.sight.ru.left.body,
}
);
await languageInstance("en").patch(
`/article/${this.sight.left_article}`,
{
heading: this.sight.en.left.heading,
body: this.sight.en.left.body,
}
);
await languageInstance("zh").patch(
`/article/${this.sight.left_article}`,
{
heading: this.sight.zh.left.heading,
body: this.sight.zh.left.body,
}
);
}
}
this.sight[language].right.map(async (article, index) => {
try {
const response = await languageInstance(language).post("/article", {
heading: article.heading,
body: article.body,
});
const { id } = response.data;
const anotherLanguages = ["en", "zh", "ru"].filter(
(lang) => lang !== language
);
await languageInstance(anotherLanguages[0] as Language).patch(
`/article/${id}`,
{
heading:
this.sight[anotherLanguages[0] as Language].right[index].heading,
body: this.sight[anotherLanguages[0] as Language].right[index].body,
}
);
await languageInstance(anotherLanguages[1] as Language).patch(
`/article/${id}`,
{
heading:
this.sight[anotherLanguages[1] as Language].right[index].heading,
body: this.sight[anotherLanguages[1] as Language].right[index].body,
}
);
rightArticles.push(id);
} catch (error) { } catch (error) {
console.log(error); console.error("Error linking existing right article:", error);
throw error;
} }
});
const response = await languageInstance(language).post("/sight", {
city_id: this.sight.city_id,
city: this.sight.city,
latitude: this.sight.latitude,
longitude: this.sight.longitude,
name: this.sight[language].name,
address: this.sight[language].address,
thumbnail: this.sight.thumbnail ?? null,
watermark_lu: this.sight.watermark_lu,
watermark_rd: this.sight.watermark_rd,
left_article: this.sight.left_article,
preview_media: this.sight.preview_media,
video_preview: this.sight.video_preview,
});
const { id } = response.data;
const anotherLanguages = ["en", "zh", "ru"].filter(
(lang) => lang !== language
);
await languageInstance(anotherLanguages[0] as Language).patch(
`/sight/${id}`,
{
city_id: this.sight.city_id,
city: this.sight.city,
latitude: this.sight.latitude,
longitude: this.sight.longitude,
name: this.sight[anotherLanguages[0] as Language as Language].name,
address:
this.sight[anotherLanguages[0] as Language as Language].address,
thumbnail: this.sight.thumbnail ?? null,
watermark_lu: this.sight.watermark_lu,
watermark_rd: this.sight.watermark_rd,
left_article: this.sight.left_article,
preview_media: this.sight.preview_media,
video_preview: this.sight.video_preview,
}
);
await languageInstance(anotherLanguages[1] as Language).patch(
`/sight/${id}`,
{
city_id: this.sight.city_id,
city: this.sight.city,
latitude: this.sight.latitude,
longitude: this.sight.longitude,
name: this.sight[anotherLanguages[1] as Language].name,
address: this.sight[anotherLanguages[1] as Language].address,
thumbnail: this.sight.thumbnail ?? null,
watermark_lu: this.sight.watermark_lu,
watermark_rd: this.sight.watermark_rd,
left_article: this.sight.left_article,
preview_media: this.sight.preview_media,
video_preview: this.sight.video_preview,
}
);
rightArticles.map(async (article, index) => {
await authInstance.post(`/sight/${id}/article`, {
article_id: article,
page_num: index + 1,
});
});
console.log("created");
}; };
updateRightArticleInfo = ( updateRightArticleInfo = (
@ -405,94 +163,428 @@ class CreateSightStore {
heading: string, heading: string,
body: string body: string
) => { ) => {
if (this.sight[language].right[index]) {
this.sight[language].right[index].heading = heading; this.sight[language].right[index].heading = heading;
this.sight[language].right[index].body = body; 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 ( uploadMedia = async (
filename: string, filename: string,
type: number, type: number,
file: File, file: File,
media_name?: string media_name?: string
) => { ): Promise<MediaItem> => {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("filename", filename); formData.append("filename", filename);
if (media_name) { if (media_name) formData.append("media_name", media_name);
formData.append("media_name", media_name);
}
formData.append("type", type.toString()); formData.append("type", type.toString());
try { try {
const response = await authInstance.post(`/media`, formData); const response = await authInstance.post(`/media`, formData);
runInAction(() => {
this.fileToUpload = null; this.fileToUpload = null;
this.uploadMediaOpen = false; this.uploadMediaOpen = false;
mediaStore.getMedia(); });
mediaStore.getMedia(); // Refresh global media list
return { return {
id: response.data.id, id: response.data.id,
filename: filename, filename: filename, // Or response.data.filename if backend returns it
media_name: media_name, media_name: media_name, // Or response.data.media_name
media_type: type, media_type: type, // Or response.data.type
}; };
} catch (error) { } catch (error) {
console.log(error); console.error("Error uploading media:", error);
throw error; throw error;
} }
}; };
createLinkWithArticle = async (media: { // For Left Article Media
id: string; createLinkWithLeftArticle = async (media: MediaItem) => {
filename: string; if (!this.sight.left_article || this.sight.left_article === 10000000) {
media_name?: string; console.warn(
media_type: number; "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`, { await authInstance.post(`/article/${this.sight.left_article}/media`, {
media_id: media.id, media_id: media.id,
media_order: 1, media_order: (this.sight.ru.left.media?.length || 0) + 1,
}); });
runInAction(() => {
this.sight.ru.left.media.unshift({ (["ru", "en", "zh"] as Language[]).forEach((lang) => {
id: media.id, if (!this.sight[lang].left.media) this.sight[lang].left.media = [];
media_type: media.media_type, this.sight[lang].left.media.unshift(media);
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,
}); });
} catch (error) {
console.error("Error linking media to left article:", error);
throw error;
}
}; };
unlinkRightAritcle = async (id: number) => { deleteLeftArticleMedia = async (mediaId: string) => {
this.sight.ru.right = this.sight.ru.right.filter( if (!this.sight.left_article || this.sight.left_article === 10000000)
(article) => article.id !== id return;
); try {
this.sight.en.right = this.sight.en.right.filter( await authInstance.delete(`/article/${this.sight.left_article}/media`, {
(article) => article.id !== id data: { media_id: mediaId },
); });
this.sight.zh.right = this.sight.zh.right.filter( runInAction(() => {
(article) => article.id !== id (["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
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
); );
}
});
});
} 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 response = await authInstance.get(`/sight/${id}`);
const data = response.data; const data = response.data;
if (data.left_article != 0) {
if (data.left_article != 0 && data.left_article != null) {
await this.getLeftArticle(data.left_article); await this.getLeftArticle(data.left_article);
} }
@ -260,7 +261,10 @@ class EditSightStore {
}); });
this.sight.common.left_article = createdLeftArticleId; 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( await languageInstance("ru").patch(
`/article/${this.sight.common.left_article}`, `/article/${this.sight.common.left_article}`,
{ {
@ -306,6 +310,9 @@ class EditSightStore {
for (const language of ["ru", "en", "zh"] as Language[]) { for (const language of ["ru", "en", "zh"] as Language[]) {
for (const article of this.sight[language].right) { for (const article of this.sight[language].right) {
if (article.id == 0 || article.id == null) {
continue;
}
await languageInstance(language).patch(`/article/${article.id}`, { await languageInstance(language).patch(`/article/${article.id}`, {
heading: article.heading, heading: article.heading,
body: article.body, 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, TextField,
Box, Box,
Autocomplete, Autocomplete,
Typography,
Paper,
Tooltip,
MenuItem, MenuItem,
Menu as MuiMenu, Menu as MuiMenu,
} from "@mui/material"; } from "@mui/material";
@ -20,9 +17,9 @@ import {
SightLanguageInfo, SightLanguageInfo,
SightCommonInfo, SightCommonInfo,
createSightStore, createSightStore,
UploadMediaDialog,
} from "@shared"; } from "@shared";
import { LanguageSwitcher } from "@widgets"; import { ImageUploadCard, LanguageSwitcher } from "@widgets";
import { Info, X } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -33,20 +30,16 @@ import { toast } from "react-toastify";
export const CreateInformationTab = observer( export const CreateInformationTab = observer(
({ value, index }: { value: number; index: number }) => { ({ value, index }: { value: number; index: number }) => {
const { cities } = cityStore; const { cities } = cityStore;
const [, setIsMediaModalOpen] = useState(false);
const [mediaId, setMediaId] = useState<string>(""); const [mediaId, setMediaId] = useState<string>("");
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
const { language } = languageStore; const { language } = languageStore;
const { sight, updateSightInfo, createSight } = createSightStore; const { sight, updateSightInfo, createSight } = createSightStore;
const data = sight[language]; const data = sight[language];
const [, setCity] = useState<number>(sight.city_id ?? 0); const [, setCity] = useState<number>(sight.city_id ?? 0);
const [coordinates, setCoordinates] = useState<string>(`0 0`); const [coordinates, setCoordinates] = useState<string>(`0 0`);
const token = localStorage.getItem("token");
// Menu state for each media button // Menu state for each media button
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null); const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
const [activeMenuType, setActiveMenuType] = useState< const [activeMenuType, setActiveMenuType] = useState<
@ -109,6 +102,7 @@ export const CreateInformationTab = observer(
}); });
setActiveMenuType(null); setActiveMenuType(null);
}; };
return ( return (
<> <>
<TabPanel value={value} index={index}> <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 flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
}} }}
> >
<Paper <ImageUploadCard
elevation={2} title="Логотип"
sx={{ imageKey="thumbnail"
padding: 2, imageUrl={sight.thumbnail}
display: "flex", onImageClick={() => {
flexDirection: "column", setIsPreviewMediaOpen(true);
alignItems: "center", setMediaId(sight.thumbnail ?? "");
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
}} }}
> onDeleteImageClick={() => {
<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({ handleChange({
thumbnail: null, thumbnail: null,
}); });
setActiveMenuType(null); setActiveMenuType(null);
}} }}
> onSelectFileClick={() => {
<X color="red" /> setActiveMenuType("thumbnail");
</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); setIsAddMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail"); setActiveMenuType("thumbnail");
}} }}
>
Выбрать файл
</Button>
</div>
)}
</Box>
</Paper>
<Paper
elevation={2}
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="subtitle2"
gutterBottom
sx={{ mb: 0, mr: 0.5 }}
>
Водяной знак (л.в)
</Typography>
<Tooltip title={"asf"}>
<Info
size={16}
color="gray"
style={{ cursor: "pointer" }}
/> />
</Tooltip>
</Box>
<Box <ImageUploadCard
sx={{ title="Водяной знак (л.в)"
position: "relative", imageKey="watermark_lu"
width: "200px", imageUrl={sight.watermark_lu}
height: "200px", onImageClick={() => {
setIsPreviewMediaOpen(true);
display: "flex", setMediaId(sight.watermark_lu ?? "");
alignItems: "center",
justifyContent: "center",
borderRadius: 1,
mb: 1,
cursor: sight.watermark_lu ? "pointer" : "default",
}} }}
onClick={() => { onDeleteImageClick={() => {
setIsMediaModalOpen(true);
}}
>
{sight.watermark_lu && (
<button
className="absolute top-2 right-2"
onClick={() => {
handleChange({ handleChange({
watermark_lu: null, watermark_lu: null,
}); });
setActiveMenuType(null); setActiveMenuType(null);
}} }}
> onSelectFileClick={() => {
<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"); setActiveMenuType("watermark_lu");
setIsAddMediaOpen(true); setIsAddMediaOpen(true);
}} }}
> setUploadMediaOpen={() => {
Выбрать файл setIsUploadMediaOpen(true);
</Button> setActiveMenuType("watermark_lu");
</div>
)}
</Box>
</Paper>
<Paper
elevation={2}
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
}} }}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="subtitle2"
gutterBottom
sx={{ mb: 0, mr: 0.5 }}
>
Водяной знак (п.в)
</Typography>
<Tooltip title={"asfaf"}>
<Info
size={16}
color="gray"
style={{ cursor: "pointer" }}
/> />
</Tooltip>
</Box>
<Box
sx={{
position: "relative",
width: "200px",
height: "200px",
display: "flex", <ImageUploadCard
alignItems: "center", title="Водяной знак (п.в)"
justifyContent: "center", imageKey="watermark_rd"
borderRadius: 1, imageUrl={sight.watermark_rd}
mb: 1, onImageClick={() => {
cursor: sight.watermark_rd ? "pointer" : "default", setIsPreviewMediaOpen(true);
setMediaId(sight.watermark_rd ?? "");
}} }}
onClick={() => { onDeleteImageClick={() => {
setIsMediaModalOpen(true);
}}
>
{sight.watermark_rd && (
<button
className="absolute top-2 right-2"
onClick={() => {
handleChange({ handleChange({
watermark_rd: null, watermark_rd: null,
}); });
setActiveMenuType(null); setActiveMenuType(null);
}} }}
> onSelectFileClick={() => {
<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"); setActiveMenuType("watermark_rd");
setIsAddMediaOpen(true); setIsAddMediaOpen(true);
}} }}
> setUploadMediaOpen={() => {
Выбрать файл setIsUploadMediaOpen(true);
</Button> setActiveMenuType("watermark_rd");
</div> }}
)} />
</Box>
</Paper>
</Box> </Box>
</Box> </Box>
</Box> </Box>
@ -576,6 +376,18 @@ export const CreateInformationTab = observer(
onClose={() => setIsPreviewMediaOpen(false)} onClose={() => setIsPreviewMediaOpen(false)}
mediaId={mediaId} 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, deleteLeftArticle,
createLeftArticle, createLeftArticle,
unlinkLeftArticle, unlinkLeftArticle,
createLinkWithArticle, createLinkWithLeftArticle,
} = createSightStore; } = createSightStore;
const { const {
deleteMedia, deleteMedia,
@ -75,10 +75,10 @@ export const CreateLeftTab = observer(
media_name?: string; media_name?: string;
media_type: number; media_type: number;
}) => { }) => {
await createLinkWithArticle(media); await createLinkWithLeftArticle(media);
setIsSelectMediaDialogOpen(false); setIsSelectMediaDialogOpen(false);
}, },
[createLinkWithArticle] [createLinkWithLeftArticle]
); );
const handleArticleSelect = useCallback( const handleArticleSelect = useCallback(
@ -437,7 +437,7 @@ export const CreateLeftTab = observer(
afterUpload={async (media) => { afterUpload={async (media) => {
setUploadMediaOpen(false); setUploadMediaOpen(false);
setFileToUpload(null); setFileToUpload(null);
await createLinkWithArticle(media); await createLinkWithLeftArticle(media);
}} }}
/> />
<SelectArticleModal <SelectArticleModal

View File

@ -13,42 +13,179 @@ import {
languageStore, languageStore,
SelectArticleModal, SelectArticleModal,
TabPanel, TabPanel,
SelectMediaDialog, // Import
UploadMediaDialog, // Import
} from "@shared"; } from "@shared";
import { import {
LanguageSwitcher, LanguageSwitcher,
MediaArea, // Import
MediaAreaForSight, // Import
ReactMarkdownComponent, ReactMarkdownComponent,
ReactMarkdownEditor, ReactMarkdownEditor,
} from "@widgets"; } from "@widgets";
import { ImagePlus, Plus } from "lucide-react"; import { ImagePlus, Plus, X } from "lucide-react"; // Import X
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useState } from "react"; import { useState, useEffect } from "react"; // Added useEffect
import { MediaViewer } from "../../MediaViewer/index"; 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( export const CreateRightTab = observer(
({ value, index }: { value: number; index: number }) => { ({ value, index }: { value: number; index: number }) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const { sight, createNewRightArticle, updateRightArticleInfo } = const {
createSightStore; 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 { language } = languageStore;
const [articleDialogOpen, setArticleDialogOpen] = useState(false);
const [selectArticleDialogOpen, setSelectArticleDialogOpen] =
useState(false);
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>( const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
null null
); );
const [type, setType] = useState<"article" | "media">("media"); 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); setAnchorEl(event.currentTarget);
}; };
const handleClose = () => { const handleCloseMenu = () => {
setAnchorEl(null); 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) => { const handleDisplayArticleFromList = (idx: number) => {
setActiveArticleIndex(index); 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 ( return (
@ -59,7 +196,7 @@ export const CreateRightTab = observer(
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
height: "100%", height: "100%",
minHeight: "calc(100vh - 200px)", // Adjust as needed minHeight: "calc(100vh - 200px)",
gap: 2, gap: 2,
paddingBottom: "70px", // Space for the save button paddingBottom: "70px", // Space for the save button
position: "relative", position: "relative",
@ -68,6 +205,7 @@ export const CreateRightTab = observer(
<BackButton /> <BackButton />
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}> <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="flex flex-col w-[75%] gap-2">
<Box className="w-full flex 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">
@ -75,191 +213,178 @@ export const CreateRightTab = observer(
<Box <Box
onClick={() => { onClick={() => {
setType("media"); 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> <Typography>Предпросмотр медиа</Typography>
</Box> </Box>
{sight[language].right.map((article, index) => ( {sight[language].right.map((article, artIdx) => (
<Box <Box
key={index} key={article.id || artIdx} // article.id should be preferred
className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300" className={`w-full p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300 ${
onClick={() => { type === "article" && activeArticleIndex === artIdx
handleSelectArticle(index); ? "bg-blue-300 font-semibold"
setType("article"); : "bg-gray-200"
}} }`}
onClick={() => handleDisplayArticleFromList(artIdx)}
> >
<Typography>{article.heading}</Typography> <Typography noWrap title={article.heading}>
{article.heading || "Без названия"}
</Typography>
</Box> </Box>
))} ))}
</Box> </Box>
<button <button
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center" 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={handleClick} onClick={handleClickMenu}
aria-controls={openMenu ? "add-article-menu" : undefined}
aria-haspopup="true"
aria-expanded={openMenu ? "true" : undefined}
> >
<Plus size={20} color="white" /> <Plus size={20} color="white" />
</button> </button>
<Menu <Menu
id="basic-menu" id="add-article-menu"
anchorEl={anchorEl} anchorEl={anchorEl}
open={open} open={openMenu}
onClose={handleClose} onClose={handleCloseMenu}
MenuListProps={{ MenuListProps={{ "aria-labelledby": "basic-button" }}
"aria-labelledby": "basic-button", sx={{ mt: 1 }}
}}
sx={{
mt: 1,
}}
>
<MenuItem
onClick={() => {
createNewRightArticle();
handleClose();
}}
> >
<MenuItem onClick={handleCreateNewLocalArticle}>
<Typography>Создать новую</Typography> <Typography>Создать новую</Typography>
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
setArticleDialogOpen(true); setSelectArticleDialogOpen(true);
handleClose(); handleCloseMenu();
}} }}
> >
<Typography>Выбрать существующую статью</Typography> <Typography>Выбрать существующую статью</Typography>
</MenuItem> </MenuItem>
</Menu> </Menu>
</Box> </Box>
{type === "article" && (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3"> {/* Main content area: Article Editor or Sight Media Preview */}
{activeArticleIndex !== null && ( {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-3"> <Box className="flex justify-end gap-2 mb-1 flex-shrink-0">
<Button variant="contained" color="primary"> <Button
variant="outlined"
color="warning"
size="small"
onClick={() => {
if (currentRightArticle) {
unlinkRightAritcle(currentRightArticle.id); // Corrected function name
setActiveArticleIndex(null);
setType("media");
}
}}
>
Открепить Открепить
</Button> </Button>
<Button
<Button variant="contained" color="success"> variant="contained"
color="error"
size="small"
onClick={async () => {
if (
currentRightArticle &&
window.confirm(
`Удалить статью "${currentRightArticle.heading}" окончательно?`
)
) {
try {
await deleteRightArticle(currentRightArticle.id);
setActiveArticleIndex(null);
setType("media");
toast.success("Статья удалена");
} catch {
toast.error("Не удалось удалить статью");
}
}
}}
>
Удалить Удалить
</Button> </Button>
</Box> </Box>
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
{/* Левая колонка: Редактирование */}
<Box <Box
sx={{ sx={{
flex: 2, flexGrow: 1,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 2, gap: 2,
overflowY: "auto",
pt: 7,
}} }}
> >
<TextField <TextField
label="Название информации" label="Название информации (правый виджет)"
value={ value={currentRightArticle.heading}
sight[language].right[activeArticleIndex]
.heading
}
onChange={(e) => onChange={(e) =>
activeArticleIndex !== null &&
updateRightArticleInfo( updateRightArticleInfo(
activeArticleIndex, activeArticleIndex,
language, language,
e.target.value, e.target.value,
sight[language].right[activeArticleIndex].body currentRightArticle.body
) )
} }
variant="outlined" variant="outlined"
fullWidth fullWidth
/> />
<Box sx={{ minHeight: 200, flexGrow: 1 }}>
<ReactMarkdownEditor <ReactMarkdownEditor
value={ value={currentRightArticle.body}
sight[language].right[activeArticleIndex].body onChange={(mdValue) =>
} activeArticleIndex !== null &&
onChange={(value) =>
updateRightArticleInfo( updateRightArticleInfo(
activeArticleIndex, activeArticleIndex,
language, language,
sight[language].right[activeArticleIndex] currentRightArticle.heading,
.heading, mdValue || ""
value
) )
} }
/> />
</Box> </Box>
{/* Блок МЕДИА для статьи */} <MediaArea
{/* <Paper elevation={1} sx={{ padding: 2, mt: 1 }}> articleId={currentRightArticle.id} // Needs a real ID
<Typography variant="h6" gutterBottom> mediaIds={currentRightArticle.media || []}
МЕДИА onFilesDrop={(files) => {
</Typography> if (files.length > 0) {
{data.left.media ? ( setFileToUpload(files[0]);
<Box sx={{ mb: 1 }}> setMediaTarget("rightArticle");
<img handleOpenUploadMedia();
src={data.left.media.filename} }
alt="Selected media"
style={{
maxWidth: "100%",
maxHeight: "150px",
objectFit: "contain",
}} }}
deleteMedia={deleteRightArticleMedia}
setSelectMediaDialogOpen={() =>
handleOpenSelectMediaDialog("rightArticle")
}
/> />
</Box> </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> </Box>
)} ) : type === "media" ? (
<Button <Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 relative flex justify-center items-center">
variant="contained"
startIcon={<ImagePlus size={18} />}
onClick={handleOpenMediaDialog}
>
Выбрать/Загрузить медиа
</Button>
{data.left.media && (
<Button
variant="outlined"
color="error"
size="small"
sx={{ ml: 1 }}
onClick={() =>
updateSightInfo(
languageStore.language,
{
left: {
heading: data.left.heading,
body: data.left.body,
media: null,
},
},
false
)
}
>
Удалить медиа
</Button>
)}
</Paper> */}
</Box>
</>
)}
</Box>
)}
{type === "media" && ( {type === "media" && (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center"> <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 <MediaViewer
media={{ media={{
id: sight.preview_media || "", id: sight.preview_media || "",
@ -267,41 +392,51 @@ export const CreateRightTab = observer(
filename: sight.preview_media || "", filename: sight.preview_media || "",
}} }}
/> />
</>
)}
{!sight.preview_media && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
)}
</Box>
)}
</Box>
) : (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center">
<Typography variant="h6" color="text.secondary">
Выберите статью слева или секцию "Предпросмотр медиа"
</Typography>
</Box> </Box>
)} )}
</Box> </Box>
</Box> </Box>
{/* Right Column: Live Preview */}
<Box className="w-[25%] mr-10"> <Box className="w-[25%] mr-10">
{activeArticleIndex !== null && ( {type === "article" && currentRightArticle && (
<Paper <Paper
className="flex-1 flex flex-col rounded-2xl" className="flex-1 flex flex-col rounded-2xl"
elevation={2} elevation={2}
sx={{ height: "75vh", overflow: "hidden" }}
> >
<Box <Box
className="rounded-2xl overflow-hidden" className="rounded-2xl overflow-hidden"
sx={{ sx={{
width: "100%", width: "100%",
height: "75vh", height: "100%",
background: "#877361", background: "#877361", // Theme background
borderColor: "grey.300",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}} }}
> >
{type === "media" ? ( {currentRightArticle.media &&
<Box currentRightArticle.media.length > 0 ? (
sx={{ <MediaViewer media={currentRightArticle.media[0]} />
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Typography color="white">Загрузка...</Typography>
</Box>
) : ( ) : (
<>
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
@ -315,89 +450,144 @@ export const CreateRightTab = observer(
> >
<ImagePlus size={48} color="white" /> <ImagePlus size={48} color="white" />
</Box> </Box>
)}
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
minHeight: "70px", minHeight: "70px", // Fixed height for heading container
background: "#877361", background: "#877361", // Consistent with theme
display: "flex", display: "flex",
flexShrink: 0, flexShrink: 0,
flex: 1,
alignItems: "center", alignItems: "center",
borderBottom: "1px solid rgba(255,255,255,0.1)", borderBottom: "1px solid rgba(255,255,255,0.1)",
px: 2, 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, py: 1,
}} }}
> >
{sight[language].right[activeArticleIndex].body ? ( <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 <ReactMarkdownComponent
value={ value={currentRightArticle.body}
sight[language].right[activeArticleIndex].body
}
/> />
) : ( ) : (
<Typography <Typography
color="rgba(255,255,255,0.7)" color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }} sx={{ textAlign: "center", mt: 4 }}
> >
Предпросмотр статьи появится здесь Содержимое статьи...
</Typography> </Typography>
)} )}
</Box> </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> </Box>
</Paper> </Paper>
)} )}
</Box> </Box>
</Box> </Box>
{/* Sticky Save Button Footer */}
<Box <Box
sx={{ sx={{
position: "absolute", position: "absolute",
bottom: 0, bottom: 0,
left: 0, // ensure it spans from left
right: 0, right: 0,
padding: 2, padding: 2,
backgroundColor: "background.paper", // Ensure button is visible backgroundColor: "background.paper",
width: "100%", // Cover the full width to make it a sticky footer borderTop: "1px solid", // Add a subtle top border
borderColor: "divider", // Use theme's divider color
width: "100%",
display: "flex", display: "flex",
justifyContent: "flex-end", 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> </Button>
</Box> </Box>
</Box> </Box>
{/*
{/* Modals */}
<SelectArticleModal <SelectArticleModal
open={openedType === "article"} open={selectArticleDialogOpen}
onClose={handleCloseSelectModal} onClose={() => setSelectArticleDialogOpen(false)}
onSelectArticle={handleSelectArticle} onSelectArticle={handleSelectExistingArticleAndLink}
linkedArticleIds={linkedArticleIds} // Pass IDs of already linked/added right articles to exclude them from selection
/> */}
<SelectArticleModal
open={articleDialogOpen}
onClose={() => setArticleDialogOpen(false)}
onSelectArticle={handleSelectArticle}
linkedArticleIds={sight[language].right.map((article) => article.id)} 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> </TabPanel>
); );
} }

View File

@ -3,9 +3,6 @@ import {
TextField, TextField,
Box, Box,
Autocomplete, Autocomplete,
Typography,
Paper,
Tooltip,
MenuItem, MenuItem,
Menu as MuiMenu, Menu as MuiMenu,
} from "@mui/material"; } from "@mui/material";
@ -20,9 +17,9 @@ import {
PreviewMediaDialog, PreviewMediaDialog,
SightLanguageInfo, SightLanguageInfo,
SightCommonInfo, SightCommonInfo,
UploadMediaDialog,
} from "@shared"; } from "@shared";
import { LanguageSwitcher } from "@widgets"; import { ImageUploadCard, LanguageSwitcher } from "@widgets";
import { Info, ImagePlus } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -34,10 +31,10 @@ import { toast } from "react-toastify";
export const InformationTab = observer( export const InformationTab = observer(
({ value, index }: { value: number; index: number }) => { ({ value, index }: { value: number; index: number }) => {
const { cities } = cityStore; const { cities } = cityStore;
const [, setIsMediaModalOpen] = useState(false);
const [mediaId, setMediaId] = useState<string>(""); const [mediaId, setMediaId] = useState<string>("");
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
const { language } = languageStore; const { language } = languageStore;
const { sight, updateSightInfo, updateSight } = editSightStore; const { sight, updateSightInfo, updateSight } = editSightStore;
@ -45,8 +42,6 @@ export const InformationTab = observer(
const [, setCity] = useState<number>(sight.common.city_id ?? 0); const [, setCity] = useState<number>(sight.common.city_id ?? 0);
const [coordinates, setCoordinates] = useState<string>(`0 0`); const [coordinates, setCoordinates] = useState<string>(`0 0`);
const token = localStorage.getItem("token");
// Menu state for each media button // Menu state for each media button
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null); const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
const [activeMenuType, setActiveMenuType] = useState< const [activeMenuType, setActiveMenuType] = useState<
@ -76,12 +71,22 @@ export const InformationTab = observer(
handleMenuClose(); handleMenuClose();
}; };
const handleMediaSelect = () => { const handleMediaSelect = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
if (!activeMenuType) return; if (!activeMenuType) return;
handleChange(
// Close the dialog language as Language,
setIsAddMediaOpen(false); {
[activeMenuType ?? "thumbnail"]: media.id,
},
true
);
setActiveMenuType(null); setActiveMenuType(null);
setIsUploadMediaOpen(false);
}; };
const handleChange = ( 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 flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
}} }}
> >
<Paper <ImageUploadCard
elevation={2} title="Логотип"
sx={{ imageKey="thumbnail"
padding: 2, imageUrl={sight.common.thumbnail}
display: "flex", onImageClick={() => {
flexDirection: "column",
alignItems: "center",
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="subtitle2"
gutterBottom
sx={{ mb: 0, mr: 0.5 }}
>
Логотип
</Typography>
</Box>
<Box
sx={{
position: "relative",
width: "200px",
height: "200px",
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",
},
}}
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); setIsPreviewMediaOpen(true);
setMediaId(sight.common.thumbnail ?? ""); setMediaId(sight.common.thumbnail ?? "");
}} }}
/> onDeleteImageClick={() => {
) : ( handleChange(
<ImagePlus size={24} color="grey" /> language as Language,
)} {
</Box> thumbnail: null,
</Paper>
<Paper
elevation={2}
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="subtitle2"
gutterBottom
sx={{ mb: 0, mr: 0.5 }}
>
Водяной знак (л.в)
</Typography>
<Tooltip title={"asf"}>
<Info
size={16}
color="gray"
style={{ cursor: "pointer" }}
/>
</Tooltip>
</Box>
<Box
sx={{
position: "relative",
width: "200px",
height: "200px",
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",
}, },
true
);
setActiveMenuType(null);
}} }}
onClick={() => { onSelectFileClick={() => {
setActiveMenuType("thumbnail");
setIsAddMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
}}
/>
<ImageUploadCard
title="Водяной знак (л.в)"
imageKey="watermark_lu"
imageUrl={sight.common.watermark_lu}
onImageClick={() => {
setIsPreviewMediaOpen(true); setIsPreviewMediaOpen(true);
setMediaId(sight.common.watermark_lu ?? ""); setMediaId(sight.common.watermark_lu ?? "");
}} }}
> onDeleteImageClick={() => {
{sight.common.watermark_lu ? ( handleChange(
<img language as Language,
src={`${import.meta.env.VITE_KRBL_MEDIA}${ {
sight.common.watermark_lu watermark_lu: null,
}/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
}}
>
<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",
}, },
true
);
setActiveMenuType(null);
}} }}
onClick={() => { onSelectFileClick={() => {
setIsMediaModalOpen(true); setActiveMenuType("watermark_lu");
setMediaId(sight.common.watermark_rd ?? ""); setIsAddMediaOpen(true);
}} }}
> setUploadMediaOpen={() => {
{sight.common.watermark_rd ? ( setIsUploadMediaOpen(true);
<img setActiveMenuType("watermark_lu");
src={`${import.meta.env.VITE_KRBL_MEDIA}${ }}
sight.common.watermark_rd />
}/download?token=${token}`} <ImageUploadCard
alt="Знак п.в" title="Водяной знак (п.в)"
style={{ maxWidth: "100%", maxHeight: "100%" }} imageKey="watermark_rd"
onClick={() => { imageUrl={sight.common.watermark_rd}
onImageClick={() => {
setIsPreviewMediaOpen(true); setIsPreviewMediaOpen(true);
setMediaId(sight.common.watermark_rd ?? ""); 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");
}}
/> />
) : (
<ImagePlus size={24} color="grey" />
)}
</Box>
</Paper>
</Box> </Box>
</Box> </Box>
</Box> </Box>
@ -489,6 +373,21 @@ export const InformationTab = observer(
onSelectMedia={handleMediaSelect} onSelectMedia={handleMediaSelect}
/> />
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
afterUpload={(media) => {
handleChange(
language as Language,
{
[activeMenuType ?? "thumbnail"]: media.id,
},
true
);
setActiveMenuType(null);
setIsUploadMediaOpen(false);
}}
/>
<PreviewMediaDialog <PreviewMediaDialog
open={isPreviewMediaOpen} open={isPreviewMediaOpen}
onClose={() => setIsPreviewMediaOpen(false)} onClose={() => setIsPreviewMediaOpen(false)}

View File

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

View File

@ -11,3 +11,4 @@ export * from "./MediaViewer";
export * from "./MediaArea"; export * from "./MediaArea";
export * from "./ModelViewer3D"; export * from "./ModelViewer3D";
export * from "./MediaAreaForSight"; 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"}