From 078f051e8a52f19042290e7bae796136585cc166 Mon Sep 17 00:00:00 2001 From: itoshi Date: Tue, 3 Jun 2025 14:32:42 +0300 Subject: [PATCH] Update `media` select in `EditSightPage` and `CreateSightPage` --- src/entities/navigation/ui/index.tsx | 10 +- src/features/navigation/ui/index.tsx | 1 + src/pages/MediaListPage/index.tsx | 11 +- src/shared/config/constants.tsx | 14 +- src/shared/store/CreateSightStore/index.tsx | 922 ++++++++++-------- src/shared/store/EditSightStore/index.tsx | 11 +- src/widgets/ImageUploadCard/index.tsx | 182 ++++ .../SightTabs/CreateInformationTab/index.tsx | 354 ++----- src/widgets/SightTabs/CreateLeftTab/index.tsx | 8 +- .../SightTabs/CreateRightTab/index.tsx | 694 ++++++++----- .../SightTabs/InformationTab/index.tsx | 319 +++--- .../SightTabs/RightWidgetTab/index.tsx | 7 +- src/widgets/index.ts | 1 + tsconfig.tsbuildinfo | 2 +- 14 files changed, 1361 insertions(+), 1175 deletions(-) create mode 100644 src/widgets/ImageUploadCard/index.tsx diff --git a/src/entities/navigation/ui/index.tsx b/src/entities/navigation/ui/index.tsx index f1ef318..061caa3 100644 --- a/src/entities/navigation/ui/index.tsx +++ b/src/entities/navigation/ui/index.tsx @@ -9,18 +9,26 @@ import { useNavigate } from "react-router-dom"; interface NavigationItemProps { item: NavigationItem; open: boolean; + onClick?: () => void; } export const NavigationItemComponent: React.FC = ({ item, open, + onClick, }) => { const Icon = item.icon; const navigate = useNavigate(); return ( navigate(item.path)} + onClick={() => { + if (onClick) { + onClick(); + } else { + navigate(item.path); + } + }} disablePadding sx={{ display: "block" }} > diff --git a/src/features/navigation/ui/index.tsx b/src/features/navigation/ui/index.tsx index 28c6bc3..6f15184 100644 --- a/src/features/navigation/ui/index.tsx +++ b/src/features/navigation/ui/index.tsx @@ -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} /> ))} diff --git a/src/pages/MediaListPage/index.tsx b/src/pages/MediaListPage/index.tsx index 5497d0b..6072c30 100644 --- a/src/pages/MediaListPage/index.tsx +++ b/src/pages/MediaListPage/index.tsx @@ -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 ( <> -
- -
diff --git a/src/shared/config/constants.tsx b/src/shared/config/constants.tsx index a4d8194..d08aafb 100644 --- a/src/shared/config/constants.tsx +++ b/src/shared/config/constants.tsx @@ -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"; }, }, ], diff --git a/src/shared/store/CreateSightStore/index.tsx b/src/shared/store/CreateSightStore/index.tsx index d431a1a..a2f9f95 100644 --- a/src/shared/store/CreateSightStore/index.tsx +++ b/src/shared/store/CreateSightStore/index.tsx @@ -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, - 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, // 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) }; + } + }; + + // --- 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 => { 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; + } }; } diff --git a/src/shared/store/EditSightStore/index.tsx b/src/shared/store/EditSightStore/index.tsx index f4436e7..5f31932 100644 --- a/src/shared/store/EditSightStore/index.tsx +++ b/src/shared/store/EditSightStore/index.tsx @@ -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, diff --git a/src/widgets/ImageUploadCard/index.tsx b/src/widgets/ImageUploadCard/index.tsx new file mode 100644 index 0000000..1d0d543 --- /dev/null +++ b/src/widgets/ImageUploadCard/index.tsx @@ -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 = ({ + title, + + imageUrl, + onImageClick, + onDeleteImageClick, + onSelectFileClick, + setUploadMediaOpen, + tooltipText, +}) => { + const fileInputRef = useRef(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 + ) => { + 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) => { + event.preventDefault(); // Crucial to allow a drop + event.stopPropagation(); + setIsDragOver(true); + }; + + const handleDragLeave = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragOver(false); + }; + + const handleDrop = async (event: DragEvent) => { + 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 ( + + + + {title} + + {tooltipText && ( + + + + )} + + + {imageUrl && ( + + )} + {imageUrl ? ( + {title} + ) : ( +
+
+

Перетащите файл

+
+ +

или

+ + {/* Hidden file input */} + +
+ )} +
+
+ ); +}; diff --git a/src/widgets/SightTabs/CreateInformationTab/index.tsx b/src/widgets/SightTabs/CreateInformationTab/index.tsx index 3288ef4..dd2d1d5 100644 --- a/src/widgets/SightTabs/CreateInformationTab/index.tsx +++ b/src/widgets/SightTabs/CreateInformationTab/index.tsx @@ -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(""); 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(sight.city_id ?? 0); const [coordinates, setCoordinates] = useState(`0 0`); - const token = localStorage.getItem("token"); - // Menu state for each media button const [menuAnchorEl, setMenuAnchorEl] = useState(null); const [activeMenuType, setActiveMenuType] = useState< @@ -109,6 +102,7 @@ export const CreateInformationTab = observer( }); setActiveMenuType(null); }; + return ( <> @@ -242,271 +236,77 @@ export const CreateInformationTab = observer( flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up }} > - { + setIsPreviewMediaOpen(true); + setMediaId(sight.thumbnail ?? ""); }} - > - - - Логотип - - - { - setIsMediaModalOpen(true); - }} - > - {sight.thumbnail && ( - - )} - {sight.thumbnail ? ( - Логотип { - setIsPreviewMediaOpen(true); - setMediaId(sight.thumbnail ?? ""); - }} - /> - ) : ( -
-
-

Перетащите файл

-
-

или

- -
- )} -
-
- { + handleChange({ + thumbnail: null, + }); + setActiveMenuType(null); }} - > - - - Водяной знак (л.в) - - - - - - - { - setIsMediaModalOpen(true); - }} - > - {sight.watermark_lu && ( - - )} - {sight.watermark_lu ? ( - Логотип { - setIsPreviewMediaOpen(true); - setMediaId(sight.watermark_lu ?? ""); - }} - /> - ) : ( -
-
-

Перетащите файл

-
-

или

- -
- )} -
-
- - { + setActiveMenuType("thumbnail"); + setIsAddMediaOpen(true); }} - > - - - Водяной знак (п.в) - - - - - - { + 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 && ( - - )} - {sight.watermark_rd ? ( - Логотип { - setIsPreviewMediaOpen(true); - setMediaId(sight.watermark_rd ?? ""); - }} - /> - ) : ( -
-
-

Перетащите файл

-
-

или

- -
- )} -
-
+ { + 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"); + }} + /> + + { + 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"); + }} + /> @@ -576,6 +376,18 @@ export const CreateInformationTab = observer( onClose={() => setIsPreviewMediaOpen(false)} mediaId={mediaId} /> + + setIsUploadMediaOpen(false)} + afterUpload={(media) => { + handleChange({ + [activeMenuType ?? "thumbnail"]: media.id, + }); + setActiveMenuType(null); + setIsUploadMediaOpen(false); + }} + /> ); } diff --git a/src/widgets/SightTabs/CreateLeftTab/index.tsx b/src/widgets/SightTabs/CreateLeftTab/index.tsx index c180068..7da7c17 100644 --- a/src/widgets/SightTabs/CreateLeftTab/index.tsx +++ b/src/widgets/SightTabs/CreateLeftTab/index.tsx @@ -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); }} /> { const [anchorEl, setAnchorEl] = useState(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( null ); const [type, setType] = useState<"article" | "media">("media"); - const open = Boolean(anchorEl); - const handleClick = (event: React.MouseEvent) => { + + 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) => { 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( + {/* Left Column: Navigation & Article List */} - + { 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" + }`} > Предпросмотр медиа - {sight[language].right.map((article, index) => ( + {sight[language].right.map((article, artIdx) => ( { - 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)} > - {article.heading} + + {article.heading || "Без названия"} + ))} - { - createNewRightArticle(); - handleClose(); - }} - > + Создать новую { - setArticleDialogOpen(true); - handleClose(); + setSelectArticleDialogOpen(true); + handleCloseMenu(); }} > Выбрать существующую статью - {type === "article" && ( - - {activeArticleIndex !== null && ( - <> - - - - - - {/* Левая колонка: Редактирование */} - - - - updateRightArticleInfo( - activeArticleIndex, - language, - e.target.value, - sight[language].right[activeArticleIndex].body - ) - } - variant="outlined" - fullWidth - /> - - - updateRightArticleInfo( - activeArticleIndex, - language, - sight[language].right[activeArticleIndex] - .heading, - value - ) - } - /> - - {/* Блок МЕДИА для статьи */} - {/* - - МЕДИА - - {data.left.media ? ( - - Selected media - - ) : ( - - Нет медиа - - )} - - {data.left.media && ( + {/* Main content area: Article Editor or Sight Media Preview */} + {type === "article" && currentRightArticle ? ( + + + + + + + activeArticleIndex !== null && + updateRightArticleInfo( + activeArticleIndex, + language, + e.target.value, + currentRightArticle.body ) } - > - Удалить медиа - - )} - */} - - + variant="outlined" + fullWidth + /> + + + activeArticleIndex !== null && + updateRightArticleInfo( + activeArticleIndex, + language, + currentRightArticle.heading, + mdValue || "" + ) + } + /> + + { + if (files.length > 0) { + setFileToUpload(files[0]); + setMediaTarget("rightArticle"); + handleOpenUploadMedia(); + } + }} + deleteMedia={deleteRightArticleMedia} + setSelectMediaDialogOpen={() => + handleOpenSelectMediaDialog("rightArticle") + } + /> + + + ) : type === "media" ? ( + + {type === "media" && ( + + {sight.preview_media && ( + <> + + + + + + + )} + {!sight.preview_media && ( + { + linkPreviewMedia(mediaId); + }} + onFilesDrop={() => {}} + /> + )} + )} - )} - {type === "media" && ( + ) : ( - + + Выберите статью слева или секцию "Предпросмотр медиа" + )} + + {/* Right Column: Live Preview */} - {activeArticleIndex !== null && ( + {type === "article" && currentRightArticle && ( - {type === "media" ? ( + {currentRightArticle.media && + currentRightArticle.media.length > 0 ? ( + + ) : ( - Загрузка... + - ) : ( - <> - - - - - - - {sight[language].right[activeArticleIndex] - .heading || "Выберите статью"} - - - - - {sight[language].right[activeArticleIndex].body ? ( - - ) : ( - - Предпросмотр статьи появится здесь - - )} - - )} + + + {currentRightArticle.heading || "Заголовок"} + + + + {currentRightArticle.body ? ( + + ) : ( + + Содержимое статьи... + + )} + + + + )} + {/* Optional: Preview for sight.preview_media when type === "media" */} + {type === "media" && sight.preview_media && ( + + + )} + {/* Sticky Save Button Footer */} - - {/* + + {/* Modals */} */} - 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)} /> + { + setUploadMediaOpen(false); + setFileToUpload(null); // Clear file if dialog is closed without upload + setMediaTarget(null); + }} + afterUpload={handleMediaUploaded} // This will use the mediaTarget + /> + { + setIsSelectMediaDialogOpen(false); + setMediaTarget(null); + }} + onSelectMedia={handleMediaSelectedFromDialog} + />
); } diff --git a/src/widgets/SightTabs/InformationTab/index.tsx b/src/widgets/SightTabs/InformationTab/index.tsx index d2cdacb..4d8d7b1 100644 --- a/src/widgets/SightTabs/InformationTab/index.tsx +++ b/src/widgets/SightTabs/InformationTab/index.tsx @@ -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(""); 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(sight.common.city_id ?? 0); const [coordinates, setCoordinates] = useState(`0 0`); - const token = localStorage.getItem("token"); - // Menu state for each media button const [menuAnchorEl, setMenuAnchorEl] = useState(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 }} > - { + setIsPreviewMediaOpen(true); + setMediaId(sight.common.thumbnail ?? ""); }} - > - - - Логотип - - - { + handleChange( + language as Language, + { + thumbnail: null, }, - }} - onClick={() => { - setIsMediaModalOpen(true); - }} - > - {sight.common.thumbnail ? ( - Логотип { - setIsPreviewMediaOpen(true); - setMediaId(sight.common.thumbnail ?? ""); - }} - /> - ) : ( - - )} - - - - - - Водяной знак (л.в) - - - - - - - { - setIsPreviewMediaOpen(true); - setMediaId(sight.common.watermark_lu ?? ""); - }} - > - {sight.common.watermark_lu ? ( - Знак л.в { - setIsMediaModalOpen(true); - setMediaId(sight.common.watermark_lu ?? ""); - }} - /> - ) : ( - - )} - - - - { + setActiveMenuType("thumbnail"); + setIsAddMediaOpen(true); }} - > - - - Водяной знак (п.в) - - - - - - { + setIsUploadMediaOpen(true); + setActiveMenuType("thumbnail"); + }} + /> + { + 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 ? ( - Знак п.в { - setIsPreviewMediaOpen(true); - setMediaId(sight.common.watermark_rd ?? ""); - }} - /> - ) : ( - - )} - - + true + ); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("watermark_lu"); + setIsAddMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("watermark_lu"); + }} + /> + { + 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"); + }} + /> @@ -489,6 +373,21 @@ export const InformationTab = observer( onSelectMedia={handleMediaSelect} /> + setIsUploadMediaOpen(false)} + afterUpload={(media) => { + handleChange( + language as Language, + { + [activeMenuType ?? "thumbnail"]: media.id, + }, + true + ); + setActiveMenuType(null); + setIsUploadMediaOpen(false); + }} + /> setIsPreviewMediaOpen(false)} diff --git a/src/widgets/SightTabs/RightWidgetTab/index.tsx b/src/widgets/SightTabs/RightWidgetTab/index.tsx index 2cdb9b1..750e4e5 100644 --- a/src/widgets/SightTabs/RightWidgetTab/index.tsx +++ b/src/widgets/SightTabs/RightWidgetTab/index.tsx @@ -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(