Update media select in EditSightPage and CreateSightPage
				
					
				
			This commit is contained in:
		| @@ -9,18 +9,26 @@ import { useNavigate } from "react-router-dom"; | ||||
| interface NavigationItemProps { | ||||
|   item: NavigationItem; | ||||
|   open: boolean; | ||||
|   onClick?: () => void; | ||||
| } | ||||
|  | ||||
| export const NavigationItemComponent: React.FC<NavigationItemProps> = ({ | ||||
|   item, | ||||
|   open, | ||||
|   onClick, | ||||
| }) => { | ||||
|   const Icon = item.icon; | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   return ( | ||||
|     <ListItem | ||||
|       onClick={() => navigate(item.path)} | ||||
|       onClick={() => { | ||||
|         if (onClick) { | ||||
|           onClick(); | ||||
|         } else { | ||||
|           navigate(item.path); | ||||
|         } | ||||
|       }} | ||||
|       disablePadding | ||||
|       sx={{ display: "block" }} | ||||
|     > | ||||
|   | ||||
| @@ -25,6 +25,7 @@ export const NavigationList = ({ open }: { open: boolean }) => { | ||||
|             key={item.id} | ||||
|             item={item as NavigationItem} | ||||
|             open={open} | ||||
|             onClick={item.onClick ? item.onClick : undefined} | ||||
|           /> | ||||
|         ))} | ||||
|       </List> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Button, TableBody } from "@mui/material"; | ||||
| import { TableBody } from "@mui/material"; | ||||
| import { TableRow, TableCell } from "@mui/material"; | ||||
| import { Table, TableHead } from "@mui/material"; | ||||
| import { mediaStore, MEDIA_TYPE_LABELS } from "@shared"; | ||||
| @@ -27,15 +27,6 @@ export const MediaListPage = observer(() => { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="flex justify-end p-3 gap-5"> | ||||
|         <Button | ||||
|           variant="contained" | ||||
|           color="primary" | ||||
|           onClick={() => navigate("/sight/create")} | ||||
|         > | ||||
|           Создать | ||||
|         </Button> | ||||
|       </div> | ||||
|       <Table sx={{ minWidth: 650 }} aria-label="simple table"> | ||||
|         <TableHead> | ||||
|           <TableRow> | ||||
|   | ||||
| @@ -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"; | ||||
|       }, | ||||
|     }, | ||||
|   ], | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| // @shared/stores/createSightStore.ts | ||||
| import { | ||||
|   Language, | ||||
|   authInstance, | ||||
|   languageInstance, | ||||
|   articlesStore, | ||||
|   languageStore, | ||||
|   mediaStore, | ||||
| } from "@shared"; | ||||
| import { makeAutoObservable } from "mobx"; | ||||
| import { Language, authInstance, languageInstance, mediaStore } from "@shared"; | ||||
| import { makeAutoObservable, runInAction } from "mobx"; | ||||
|  | ||||
| type MediaItem = { | ||||
|   id: string; | ||||
|   filename: string; | ||||
|   media_name?: string; | ||||
|   media_type: number; | ||||
| }; | ||||
|  | ||||
| type SightLanguageInfo = { | ||||
|   name: string; | ||||
| @@ -15,18 +15,13 @@ type SightLanguageInfo = { | ||||
|   left: { | ||||
|     heading: string; | ||||
|     body: string; | ||||
|     media: { | ||||
|       id: string; | ||||
|       filename: string; | ||||
|       media_name?: string; | ||||
|       media_type: number; | ||||
|     }[]; | ||||
|     media: MediaItem[]; | ||||
|   }; | ||||
|   right: { id: number; heading: string; body: string; media: [] }[]; | ||||
|   right: { id: number; heading: string; body: string; media: MediaItem[] }[]; | ||||
| }; | ||||
|  | ||||
| type SightCommonInfo = { | ||||
|   id: number; | ||||
|   // id: number; // ID is 0 until created | ||||
|   city_id: number; | ||||
|   city: string; | ||||
|   latitude: number; | ||||
| @@ -34,48 +29,50 @@ type SightCommonInfo = { | ||||
|   thumbnail: string | null; | ||||
|   watermark_lu: string | null; | ||||
|   watermark_rd: string | null; | ||||
|   left_article: number; | ||||
|   left_article: number; // Can be 0 or a real ID, or placeholder like 10000000 | ||||
|   preview_media: string | null; | ||||
|   video_preview: string | null; | ||||
| }; | ||||
|  | ||||
| // SightBaseInfo combines common info with language-specific info | ||||
| // The 'id' for the sight itself will be assigned upon creation by the backend. | ||||
| type SightBaseInfo = SightCommonInfo & { | ||||
|   [key in Language]: SightLanguageInfo; | ||||
| }; | ||||
|  | ||||
| class CreateSightStore { | ||||
|   sight: SightBaseInfo = { | ||||
|     id: 0, | ||||
|     city_id: 0, | ||||
|     city: "", | ||||
|     latitude: 0, | ||||
|     longitude: 0, | ||||
|     thumbnail: null, | ||||
|     watermark_lu: null, | ||||
|     watermark_rd: null, | ||||
|     left_article: 0, | ||||
|     preview_media: null, | ||||
|     video_preview: null, | ||||
| const initialSightState: SightBaseInfo = { | ||||
|   city_id: 0, | ||||
|   city: "", | ||||
|   latitude: 0, | ||||
|   longitude: 0, | ||||
|   thumbnail: null, | ||||
|   watermark_lu: null, | ||||
|   watermark_rd: null, | ||||
|   left_article: 0, | ||||
|   preview_media: null, | ||||
|   video_preview: null, | ||||
|   ru: { | ||||
|     name: "", | ||||
|     address: "", | ||||
|     left: { heading: "", body: "", media: [] }, | ||||
|     right: [], | ||||
|   }, | ||||
|   en: { | ||||
|     name: "", | ||||
|     address: "", | ||||
|     left: { heading: "", body: "", media: [] }, | ||||
|     right: [], | ||||
|   }, | ||||
|   zh: { | ||||
|     name: "", | ||||
|     address: "", | ||||
|     left: { heading: "", body: "", media: [] }, | ||||
|     right: [], | ||||
|   }, | ||||
| }; | ||||
|  | ||||
|     ru: { | ||||
|       name: "", | ||||
|       address: "", | ||||
|       left: { heading: "", body: "", media: [] }, | ||||
|       right: [], | ||||
|     }, | ||||
|     en: { | ||||
|       name: "", | ||||
|       address: "", | ||||
|       left: { heading: "", body: "", media: [] }, | ||||
|       right: [], | ||||
|     }, | ||||
|     zh: { | ||||
|       name: "", | ||||
|       address: "", | ||||
|       left: { heading: "", body: "", media: [] }, | ||||
|       right: [], | ||||
|     }, | ||||
|   }; | ||||
| class CreateSightStore { | ||||
|   sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState)); // Deep copy for reset | ||||
|  | ||||
|   uploadMediaOpen = false; | ||||
|   setUploadMediaOpen = (open: boolean) => { | ||||
| @@ -90,313 +87,74 @@ class CreateSightStore { | ||||
|     makeAutoObservable(this); | ||||
|   } | ||||
|  | ||||
|   // --- Right Article Management --- | ||||
|   createNewRightArticle = async () => { | ||||
|     const articleId = await languageInstance("ru").post("/article", { | ||||
|       heading: "Введите русский заголовок", | ||||
|       body: "Введите русский текст", | ||||
|     }); | ||||
|     const { id } = articleId.data; | ||||
|     await languageInstance("en").patch(`/article/${id}`, { | ||||
|       heading: "Enter the English heading", | ||||
|       body: "Enter the English text", | ||||
|     }); | ||||
|     await languageInstance("zh").patch(`/article/${id}`, { | ||||
|       heading: "Введите китайский заголовок", | ||||
|       body: "Введите китайский текст", | ||||
|     }); | ||||
|     await authInstance.post(`/sight/${this.sight.id}/article`, { | ||||
|       article_id: id, | ||||
|       page_num: this.sight.ru.right.length + 1, | ||||
|     }); | ||||
|  | ||||
|     this.sight.ru.right.push({ | ||||
|       id: id, | ||||
|       heading: "Введите русский заголовок", | ||||
|       body: "Введите русский текст", | ||||
|       media: [], | ||||
|     }); | ||||
|     this.sight.en.right.push({ | ||||
|       id: id, | ||||
|       heading: "Enter the English heading", | ||||
|       body: "Enter the English text", | ||||
|       media: [], | ||||
|     }); | ||||
|     this.sight.zh.right.push({ | ||||
|       id: id, | ||||
|       heading: "Введите китайский заголовок", | ||||
|       body: "Введите китайский текст", | ||||
|       media: [], | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   updateLeftInfo = (language: Language, heading: string, body: string) => { | ||||
|     this.sight[language].left.heading = heading; | ||||
|     this.sight[language].left.body = body; | ||||
|   }; | ||||
|  | ||||
|   clearCreateSight = () => { | ||||
|     this.sight = { | ||||
|       id: 0, | ||||
|       city_id: 0, | ||||
|       city: "", | ||||
|       latitude: 0, | ||||
|       longitude: 0, | ||||
|       thumbnail: null, | ||||
|       watermark_lu: null, | ||||
|       watermark_rd: null, | ||||
|       left_article: 0, | ||||
|       preview_media: null, | ||||
|       video_preview: null, | ||||
|  | ||||
|       ru: { | ||||
|         name: "", | ||||
|         address: "", | ||||
|         left: { heading: "", body: "", media: [] }, | ||||
|         right: [], | ||||
|       }, | ||||
|  | ||||
|       en: { | ||||
|         name: "", | ||||
|         address: "", | ||||
|         left: { heading: "", body: "", media: [] }, | ||||
|         right: [], | ||||
|       }, | ||||
|  | ||||
|       zh: { | ||||
|         name: "", | ||||
|         address: "", | ||||
|         left: { heading: "", body: "", media: [] }, | ||||
|         right: [], | ||||
|       }, | ||||
|     // Create article in DB for all languages | ||||
|     const articleRuData = { | ||||
|       heading: "Новый заголовок (RU)", | ||||
|       body: "Новый текст (RU)", | ||||
|     }; | ||||
|   }; | ||||
|     const articleEnData = { | ||||
|       heading: "New Heading (EN)", | ||||
|       body: "New Text (EN)", | ||||
|     }; | ||||
|     const articleZhData = { heading: "新标题 (ZH)", body: "新文本 (ZH)" }; | ||||
|  | ||||
|   updateSightInfo = ( | ||||
|     content: Partial<SightLanguageInfo | SightCommonInfo>, | ||||
|     language?: Language | ||||
|   ) => { | ||||
|     if (language) { | ||||
|       this.sight[language] = { | ||||
|         ...this.sight[language], | ||||
|         ...content, | ||||
|       }; | ||||
|     } else { | ||||
|       this.sight = { | ||||
|         ...this.sight, | ||||
|         ...content, | ||||
|       }; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   unlinkLeftArticle = () => { | ||||
|     this.sight.left_article = 0; | ||||
|     this.sight.ru.left.heading = ""; | ||||
|     this.sight.en.left.heading = ""; | ||||
|     this.sight.zh.left.heading = ""; | ||||
|     this.sight.ru.left.body = ""; | ||||
|     this.sight.en.left.body = ""; | ||||
|     this.sight.zh.left.body = ""; | ||||
|   }; | ||||
|  | ||||
|   updateLeftArticle = async (articleId: number) => { | ||||
|     this.sight.left_article = articleId; | ||||
|  | ||||
|     if (articleId) { | ||||
|       const ruArticleData = await languageInstance("ru").get( | ||||
|         `/article/${articleId}` | ||||
|       ); | ||||
|       const enArticleData = await languageInstance("en").get( | ||||
|         `/article/${articleId}` | ||||
|       ); | ||||
|       const zhArticleData = await languageInstance("zh").get( | ||||
|         `/article/${articleId}` | ||||
|     try { | ||||
|       const articleRes = await languageInstance("ru").post( | ||||
|         "/article", | ||||
|         articleRuData | ||||
|       ); | ||||
|       const { id } = articleRes.data; // New article's ID | ||||
|  | ||||
|       this.sight.ru.left.heading = ruArticleData.data.heading; | ||||
|       this.sight.en.left.heading = enArticleData.data.heading; | ||||
|       this.sight.zh.left.heading = zhArticleData.data.heading; | ||||
|       await languageInstance("en").patch(`/article/${id}`, articleEnData); | ||||
|       await languageInstance("zh").patch(`/article/${id}`, articleZhData); | ||||
|  | ||||
|       this.sight.ru.left.body = ruArticleData.data.body; | ||||
|       this.sight.en.left.body = enArticleData.data.body; | ||||
|       this.sight.zh.left.body = zhArticleData.data.body; | ||||
|     } else { | ||||
|       this.sight.left_article = 0; | ||||
|       this.sight.ru.left.heading = ""; | ||||
|       this.sight.en.left.heading = ""; | ||||
|       this.sight.zh.left.heading = ""; | ||||
|       this.sight.ru.left.body = ""; | ||||
|       this.sight.en.left.body = ""; | ||||
|       this.sight.zh.left.body = ""; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   deleteLeftArticle = async (articleId: number) => { | ||||
|     await authInstance.delete(`/article/${articleId}`); | ||||
|     articlesStore.getArticles(languageStore.language); | ||||
|     this.sight.left_article = 0; | ||||
|     this.sight.ru.left.heading = ""; | ||||
|     this.sight.en.left.heading = ""; | ||||
|     this.sight.zh.left.heading = ""; | ||||
|     this.sight.ru.left.body = ""; | ||||
|   }; | ||||
|  | ||||
|   createLeftArticle = async () => { | ||||
|     const response = await languageInstance("ru").post("/article", { | ||||
|       heading: "Новая статья", | ||||
|       body: "Заполните статью контентом", | ||||
|     }); | ||||
|  | ||||
|     this.sight.left_article = response.data.id; | ||||
|  | ||||
|     this.sight.ru.left.heading = "Новая статья  "; | ||||
|     this.sight.en.left.heading = ""; | ||||
|     this.sight.zh.left.heading = ""; | ||||
|     this.sight.ru.left.body = "Заполните статью контентом"; | ||||
|     this.sight.en.left.body = ""; | ||||
|     this.sight.zh.left.body = ""; | ||||
|   }; | ||||
|  | ||||
|   createSight = async (language: Language) => { | ||||
|     const rightArticles: number[] = []; | ||||
|  | ||||
|     if (this.sight.left_article !== 0) { | ||||
|       if (this.sight.left_article == 10000000) { | ||||
|         const response = await languageInstance("ru").post("/article", { | ||||
|           heading: this.sight.ru.left.heading, | ||||
|           body: this.sight.ru.left.body, | ||||
|         }); | ||||
|         const { id } = response.data; | ||||
|         await languageInstance("en").patch(`/article/${id}`, { | ||||
|           heading: this.sight.en.left.heading, | ||||
|           body: this.sight.en.left.body, | ||||
|         }); | ||||
|  | ||||
|         await languageInstance("zh").patch(`/article/${id}`, { | ||||
|           heading: this.sight.zh.left.heading, | ||||
|           body: this.sight.zh.left.body, | ||||
|         }); | ||||
|         this.sight.left_article = id; | ||||
|       } else { | ||||
|         await languageInstance("ru").patch( | ||||
|           `/article/${this.sight.left_article}`, | ||||
|           { | ||||
|             heading: this.sight.ru.left.heading, | ||||
|             body: this.sight.ru.left.body, | ||||
|           } | ||||
|         ); | ||||
|  | ||||
|         await languageInstance("en").patch( | ||||
|           `/article/${this.sight.left_article}`, | ||||
|           { | ||||
|             heading: this.sight.en.left.heading, | ||||
|             body: this.sight.en.left.body, | ||||
|           } | ||||
|         ); | ||||
|  | ||||
|         await languageInstance("zh").patch( | ||||
|           `/article/${this.sight.left_article}`, | ||||
|           { | ||||
|             heading: this.sight.zh.left.heading, | ||||
|             body: this.sight.zh.left.body, | ||||
|           } | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     this.sight[language].right.map(async (article, index) => { | ||||
|       try { | ||||
|         const response = await languageInstance(language).post("/article", { | ||||
|           heading: article.heading, | ||||
|           body: article.body, | ||||
|         }); | ||||
|         const { id } = response.data; | ||||
|         const anotherLanguages = ["en", "zh", "ru"].filter( | ||||
|           (lang) => lang !== language | ||||
|         ); | ||||
|         await languageInstance(anotherLanguages[0] as Language).patch( | ||||
|           `/article/${id}`, | ||||
|           { | ||||
|             heading: | ||||
|               this.sight[anotherLanguages[0] as Language].right[index].heading, | ||||
|             body: this.sight[anotherLanguages[0] as Language].right[index].body, | ||||
|           } | ||||
|         ); | ||||
|         await languageInstance(anotherLanguages[1] as Language).patch( | ||||
|           `/article/${id}`, | ||||
|           { | ||||
|             heading: | ||||
|               this.sight[anotherLanguages[1] as Language].right[index].heading, | ||||
|             body: this.sight[anotherLanguages[1] as Language].right[index].body, | ||||
|           } | ||||
|         ); | ||||
|         rightArticles.push(id); | ||||
|       } catch (error) { | ||||
|         console.log(error); | ||||
|       } | ||||
|     }); | ||||
|     const response = await languageInstance(language).post("/sight", { | ||||
|       city_id: this.sight.city_id, | ||||
|       city: this.sight.city, | ||||
|       latitude: this.sight.latitude, | ||||
|       longitude: this.sight.longitude, | ||||
|       name: this.sight[language].name, | ||||
|       address: this.sight[language].address, | ||||
|       thumbnail: this.sight.thumbnail ?? null, | ||||
|       watermark_lu: this.sight.watermark_lu, | ||||
|       watermark_rd: this.sight.watermark_rd, | ||||
|       left_article: this.sight.left_article, | ||||
|       preview_media: this.sight.preview_media, | ||||
|       video_preview: this.sight.video_preview, | ||||
|     }); | ||||
|  | ||||
|     const { id } = response.data; | ||||
|     const anotherLanguages = ["en", "zh", "ru"].filter( | ||||
|       (lang) => lang !== language | ||||
|     ); | ||||
|  | ||||
|     await languageInstance(anotherLanguages[0] as Language).patch( | ||||
|       `/sight/${id}`, | ||||
|       { | ||||
|         city_id: this.sight.city_id, | ||||
|         city: this.sight.city, | ||||
|         latitude: this.sight.latitude, | ||||
|         longitude: this.sight.longitude, | ||||
|         name: this.sight[anotherLanguages[0] as Language as Language].name, | ||||
|         address: | ||||
|           this.sight[anotherLanguages[0] as Language as Language].address, | ||||
|         thumbnail: this.sight.thumbnail ?? null, | ||||
|         watermark_lu: this.sight.watermark_lu, | ||||
|         watermark_rd: this.sight.watermark_rd, | ||||
|         left_article: this.sight.left_article, | ||||
|         preview_media: this.sight.preview_media, | ||||
|         video_preview: this.sight.video_preview, | ||||
|       } | ||||
|     ); | ||||
|     await languageInstance(anotherLanguages[1] as Language).patch( | ||||
|       `/sight/${id}`, | ||||
|       { | ||||
|         city_id: this.sight.city_id, | ||||
|         city: this.sight.city, | ||||
|         latitude: this.sight.latitude, | ||||
|         longitude: this.sight.longitude, | ||||
|         name: this.sight[anotherLanguages[1] as Language].name, | ||||
|         address: this.sight[anotherLanguages[1] as Language].address, | ||||
|         thumbnail: this.sight.thumbnail ?? null, | ||||
|         watermark_lu: this.sight.watermark_lu, | ||||
|         watermark_rd: this.sight.watermark_rd, | ||||
|         left_article: this.sight.left_article, | ||||
|         preview_media: this.sight.preview_media, | ||||
|         video_preview: this.sight.video_preview, | ||||
|       } | ||||
|     ); | ||||
|  | ||||
|     rightArticles.map(async (article, index) => { | ||||
|       await authInstance.post(`/sight/${id}/article`, { | ||||
|         article_id: article, | ||||
|         page_num: index + 1, | ||||
|       runInAction(() => { | ||||
|         const newArticleEntry = { id, media: [] }; | ||||
|         this.sight.ru.right.push({ ...newArticleEntry, ...articleRuData }); | ||||
|         this.sight.en.right.push({ ...newArticleEntry, ...articleEnData }); | ||||
|         this.sight.zh.right.push({ ...newArticleEntry, ...articleZhData }); | ||||
|       }); | ||||
|     }); | ||||
|     console.log("created"); | ||||
|       return id; // Return ID for potential immediate use | ||||
|     } catch (error) { | ||||
|       console.error("Error creating new right article:", error); | ||||
|       throw error; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   linkExistingRightArticle = async (articleId: number) => { | ||||
|     try { | ||||
|       const ruData = await languageInstance("ru").get(`/article/${articleId}`); | ||||
|       const enData = await languageInstance("en").get(`/article/${articleId}`); | ||||
|       const zhData = await languageInstance("zh").get(`/article/${articleId}`); | ||||
|       const mediaRes = await authInstance.get(`/article/${articleId}/media`); | ||||
|       const mediaData: MediaItem[] = mediaRes.data || []; | ||||
|  | ||||
|       runInAction(() => { | ||||
|         this.sight.ru.right.push({ | ||||
|           id: articleId, | ||||
|           heading: ruData.data.heading, | ||||
|           body: ruData.data.body, | ||||
|           media: mediaData, | ||||
|         }); | ||||
|         this.sight.en.right.push({ | ||||
|           id: articleId, | ||||
|           heading: enData.data.heading, | ||||
|           body: enData.data.body, | ||||
|           media: mediaData, | ||||
|         }); | ||||
|         this.sight.zh.right.push({ | ||||
|           id: articleId, | ||||
|           heading: zhData.data.heading, | ||||
|           body: zhData.data.body, | ||||
|           media: mediaData, | ||||
|         }); | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error("Error linking existing right article:", error); | ||||
|       throw error; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   updateRightArticleInfo = ( | ||||
| @@ -405,94 +163,428 @@ class CreateSightStore { | ||||
|     heading: string, | ||||
|     body: string | ||||
|   ) => { | ||||
|     this.sight[language].right[index].heading = heading; | ||||
|     this.sight[language].right[index].body = body; | ||||
|     if (this.sight[language].right[index]) { | ||||
|       this.sight[language].right[index].heading = heading; | ||||
|       this.sight[language].right[index].body = body; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   // "Unlink" in create mode means just removing from the list to be created with the sight | ||||
|   unlinkRightAritcle = (articleId: number) => { | ||||
|     // Changed from 'unlinkRightAritcle' spelling | ||||
|     runInAction(() => { | ||||
|       this.sight.ru.right = this.sight.ru.right.filter( | ||||
|         (article) => article.id !== articleId | ||||
|       ); | ||||
|       this.sight.en.right = this.sight.en.right.filter( | ||||
|         (article) => article.id !== articleId | ||||
|       ); | ||||
|       this.sight.zh.right = this.sight.zh.right.filter( | ||||
|         (article) => article.id !== articleId | ||||
|       ); | ||||
|     }); | ||||
|     // Note: If this article was created via createNewRightArticle, it still exists in the DB. | ||||
|     // Consider if an orphaned article should be deleted here or managed separately. | ||||
|     // For now, it just removes it from the list associated with *this specific sight creation process*. | ||||
|   }; | ||||
|  | ||||
|   deleteRightArticle = async (articleId: number) => { | ||||
|     try { | ||||
|       await authInstance.delete(`/article/${articleId}`); // Delete from backend | ||||
|       runInAction(() => { | ||||
|         // Remove from local store for all languages | ||||
|         this.sight.ru.right = this.sight.ru.right.filter( | ||||
|           (article) => article.id !== articleId | ||||
|         ); | ||||
|         this.sight.en.right = this.sight.en.right.filter( | ||||
|           (article) => article.id !== articleId | ||||
|         ); | ||||
|         this.sight.zh.right = this.sight.zh.right.filter( | ||||
|           (article) => article.id !== articleId | ||||
|         ); | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error("Error deleting right article:", error); | ||||
|       throw error; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   // --- Right Article Media Management --- | ||||
|   createLinkWithRightArticle = async (media: MediaItem, articleId: number) => { | ||||
|     try { | ||||
|       await authInstance.post(`/article/${articleId}/media`, { | ||||
|         media_id: media.id, | ||||
|         media_order: 1, // Or calculate based on existing media.length + 1 | ||||
|       }); | ||||
|       runInAction(() => { | ||||
|         (["ru", "en", "zh"] as Language[]).forEach((lang) => { | ||||
|           const article = this.sight[lang].right.find( | ||||
|             (a) => a.id === articleId | ||||
|           ); | ||||
|           if (article) { | ||||
|             if (!article.media) article.media = []; | ||||
|             article.media.unshift(media); // Add to the beginning | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error("Error linking media to right article:", error); | ||||
|       throw error; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   deleteRightArticleMedia = async (articleId: number, mediaId: string) => { | ||||
|     try { | ||||
|       await authInstance.delete(`/article/${articleId}/media`, { | ||||
|         data: { media_id: mediaId }, | ||||
|       }); | ||||
|       runInAction(() => { | ||||
|         (["ru", "en", "zh"] as Language[]).forEach((lang) => { | ||||
|           const article = this.sight[lang].right.find( | ||||
|             (a) => a.id === articleId | ||||
|           ); | ||||
|           if (article && article.media) { | ||||
|             article.media = article.media.filter((m) => m.id !== mediaId); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error("Error deleting media from right article:", error); | ||||
|       throw error; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   // --- Left Article Management (largely unchanged from your provided store) --- | ||||
|   updateLeftInfo = (language: Language, heading: string, body: string) => { | ||||
|     this.sight[language].left.heading = heading; | ||||
|     this.sight[language].left.body = body; | ||||
|   }; | ||||
|  | ||||
|   unlinkLeftArticle = () => { | ||||
|     /* ... your existing logic ... */ | ||||
|     this.sight.left_article = 0; | ||||
|     this.sight.ru.left = { heading: "", body: "", media: [] }; | ||||
|     this.sight.en.left = { heading: "", body: "", media: [] }; | ||||
|     this.sight.zh.left = { heading: "", body: "", media: [] }; | ||||
|   }; | ||||
|  | ||||
|   updateLeftArticle = async (articleId: number) => { | ||||
|     /* ... your existing logic ... */ | ||||
|     this.sight.left_article = articleId; | ||||
|     if (articleId) { | ||||
|       const [ruArticleData, enArticleData, zhArticleData, mediaData] = | ||||
|         await Promise.all([ | ||||
|           languageInstance("ru").get(`/article/${articleId}`), | ||||
|           languageInstance("en").get(`/article/${articleId}`), | ||||
|           languageInstance("zh").get(`/article/${articleId}`), | ||||
|           authInstance.get(`/article/${articleId}/media`), | ||||
|         ]); | ||||
|       runInAction(() => { | ||||
|         this.sight.ru.left = { | ||||
|           heading: ruArticleData.data.heading, | ||||
|           body: ruArticleData.data.body, | ||||
|           media: mediaData.data || [], | ||||
|         }; | ||||
|         this.sight.en.left = { | ||||
|           heading: enArticleData.data.heading, | ||||
|           body: enArticleData.data.body, | ||||
|           media: mediaData.data || [], | ||||
|         }; | ||||
|         this.sight.zh.left = { | ||||
|           heading: zhArticleData.data.heading, | ||||
|           body: zhArticleData.data.body, | ||||
|           media: mediaData.data || [], | ||||
|         }; | ||||
|       }); | ||||
|     } else { | ||||
|       this.unlinkLeftArticle(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   deleteLeftArticle = async (articleId: number) => { | ||||
|     /* ... your existing logic ... */ | ||||
|     await authInstance.delete(`/article/${articleId}`); | ||||
|     // articlesStore.getArticles(languageStore.language); // If still needed | ||||
|     this.unlinkLeftArticle(); | ||||
|   }; | ||||
|  | ||||
|   createLeftArticle = async () => { | ||||
|     /* ... your existing logic to create a new left article (placeholder or DB) ... */ | ||||
|     const response = await languageInstance("ru").post("/article", { | ||||
|       heading: "Новая левая статья", | ||||
|       body: "Заполните контентом", | ||||
|     }); | ||||
|     const newLeftArticleId = response.data.id; | ||||
|     await languageInstance("en").patch(`/article/${newLeftArticleId}`, { | ||||
|       heading: "New Left Article", | ||||
|       body: "Fill with content", | ||||
|     }); | ||||
|     await languageInstance("zh").patch(`/article/${newLeftArticleId}`, { | ||||
|       heading: "新的左侧文章", | ||||
|       body: "填写内容", | ||||
|     }); | ||||
|  | ||||
|     runInAction(() => { | ||||
|       this.sight.left_article = newLeftArticleId; // Store the actual ID | ||||
|       this.sight.ru.left = { | ||||
|         heading: "Новая левая статья", | ||||
|         body: "Заполните контентом", | ||||
|         media: [], | ||||
|       }; | ||||
|       this.sight.en.left = { | ||||
|         heading: "New Left Article", | ||||
|         body: "Fill with content", | ||||
|         media: [], | ||||
|       }; | ||||
|       this.sight.zh.left = { | ||||
|         heading: "新的左侧文章", | ||||
|         body: "填写内容", | ||||
|         media: [], | ||||
|       }; | ||||
|     }); | ||||
|     return newLeftArticleId; | ||||
|   }; | ||||
|  | ||||
|   // Placeholder for a "new" unsaved left article | ||||
|   setNewLeftArticlePlaceholder = () => { | ||||
|     this.sight.left_article = 10000000; // Special placeholder ID | ||||
|     this.sight.ru.left = { | ||||
|       heading: "Новая левая статья", | ||||
|       body: "Заполните контентом", | ||||
|       media: [], | ||||
|     }; | ||||
|     this.sight.en.left = { | ||||
|       heading: "New Left Article", | ||||
|       body: "Fill with content", | ||||
|       media: [], | ||||
|     }; | ||||
|     this.sight.zh.left = { | ||||
|       heading: "新的左侧文章", | ||||
|       body: "填写内容", | ||||
|       media: [], | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   // --- Sight Preview Media --- | ||||
|   linkPreviewMedia = (mediaId: string) => { | ||||
|     this.sight.preview_media = mediaId; | ||||
|   }; | ||||
|  | ||||
|   unlinkPreviewMedia = () => { | ||||
|     this.sight.preview_media = null; | ||||
|   }; | ||||
|  | ||||
|   // --- General Store Methods --- | ||||
|   clearCreateSight = () => { | ||||
|     this.sight = JSON.parse(JSON.stringify(initialSightState)); // Reset to initial | ||||
|   }; | ||||
|  | ||||
|   updateSightInfo = ( | ||||
|     content: Partial<SightLanguageInfo | SightCommonInfo>, // Corrected types | ||||
|     language?: Language | ||||
|   ) => { | ||||
|     if (language) { | ||||
|       this.sight[language] = { ...this.sight[language], ...content }; | ||||
|     } else { | ||||
|       // Assuming content here is for SightCommonInfo | ||||
|       this.sight = { ...this.sight, ...(content as Partial<SightCommonInfo>) }; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   // --- Main Sight Creation Logic --- | ||||
|   createSight = async (primaryLanguage: Language) => { | ||||
|     let finalLeftArticleId = this.sight.left_article; | ||||
|  | ||||
|     // 1. Handle Left Article (Create if new, or use existing ID) | ||||
|     if (this.sight.left_article === 10000000) { | ||||
|       // Placeholder for new | ||||
|       const res = await languageInstance("ru").post("/article", { | ||||
|         heading: this.sight.ru.left.heading, | ||||
|         body: this.sight.ru.left.body, | ||||
|       }); | ||||
|       finalLeftArticleId = res.data.id; | ||||
|       await languageInstance("en").patch(`/article/${finalLeftArticleId}`, { | ||||
|         heading: this.sight.en.left.heading, | ||||
|         body: this.sight.en.left.body, | ||||
|       }); | ||||
|       await languageInstance("zh").patch(`/article/${finalLeftArticleId}`, { | ||||
|         heading: this.sight.zh.left.heading, | ||||
|         body: this.sight.zh.left.body, | ||||
|       }); | ||||
|     } else if ( | ||||
|       this.sight.left_article !== 0 && | ||||
|       this.sight.left_article !== null | ||||
|     ) { | ||||
|       // Existing, ensure it's up-to-date | ||||
|       await languageInstance("ru").patch( | ||||
|         `/article/${this.sight.left_article}`, | ||||
|         { heading: this.sight.ru.left.heading, body: this.sight.ru.left.body } | ||||
|       ); | ||||
|       await languageInstance("en").patch( | ||||
|         `/article/${this.sight.left_article}`, | ||||
|         { heading: this.sight.en.left.heading, body: this.sight.en.left.body } | ||||
|       ); | ||||
|       await languageInstance("zh").patch( | ||||
|         `/article/${this.sight.left_article}`, | ||||
|         { heading: this.sight.zh.left.heading, body: this.sight.zh.left.body } | ||||
|       ); | ||||
|     } | ||||
|     // else: left_article is 0, so no left article | ||||
|  | ||||
|     // 2. Right articles are already created in DB and their IDs are in this.sight[lang].right. | ||||
|     // We just need to update their content if changed before saving the sight. | ||||
|     for (const lang of ["ru", "en", "zh"] as Language[]) { | ||||
|       for (const article of this.sight[lang].right) { | ||||
|         if (article.id == 0 || article.id == null) { | ||||
|           continue; | ||||
|         } | ||||
|         await languageInstance(lang).patch(`/article/${article.id}`, { | ||||
|           heading: article.heading, | ||||
|           body: article.body, | ||||
|         }); | ||||
|         // Media for these articles are already linked via createLinkWithRightArticle | ||||
|       } | ||||
|     } | ||||
|     const rightArticleIdsForLink = this.sight[primaryLanguage].right.map( | ||||
|       (a) => a.id | ||||
|     ); | ||||
|  | ||||
|     // 3. Create Sight object in DB | ||||
|     const sightPayload = { | ||||
|       city_id: this.sight.city_id, | ||||
|       city: this.sight.city, | ||||
|       latitude: this.sight.latitude, | ||||
|       longitude: this.sight.longitude, | ||||
|       name: this.sight[primaryLanguage].name, | ||||
|       address: this.sight[primaryLanguage].address, | ||||
|       thumbnail: this.sight.thumbnail, | ||||
|       watermark_lu: this.sight.watermark_lu, | ||||
|       watermark_rd: this.sight.watermark_rd, | ||||
|       left_article: finalLeftArticleId === 0 ? null : finalLeftArticleId, | ||||
|       preview_media: this.sight.preview_media, | ||||
|       video_preview: this.sight.video_preview, | ||||
|     }; | ||||
|  | ||||
|     const response = await languageInstance(primaryLanguage).post( | ||||
|       "/sight", | ||||
|       sightPayload | ||||
|     ); | ||||
|     const newSightId = response.data.id; // ID of the newly created sight | ||||
|  | ||||
|     // 4. Update other languages for the sight | ||||
|     const otherLanguages = (["ru", "en", "zh"] as Language[]).filter( | ||||
|       (l) => l !== primaryLanguage | ||||
|     ); | ||||
|     for (const lang of otherLanguages) { | ||||
|       await languageInstance(lang).patch(`/sight/${newSightId}`, { | ||||
|         city_id: this.sight.city_id, | ||||
|         city: this.sight.city, | ||||
|         latitude: this.sight.latitude, | ||||
|         longitude: this.sight.longitude, | ||||
|         name: this.sight[lang].name, | ||||
|         address: this.sight[lang].address, | ||||
|         thumbnail: this.sight.thumbnail, | ||||
|         watermark_lu: this.sight.watermark_lu, | ||||
|         watermark_rd: this.sight.watermark_rd, | ||||
|         left_article: finalLeftArticleId === 0 ? null : finalLeftArticleId, | ||||
|         preview_media: this.sight.preview_media, | ||||
|         video_preview: this.sight.video_preview, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // 5. Link Right Articles to the new Sight | ||||
|     for (let i = 0; i < rightArticleIdsForLink.length; i++) { | ||||
|       await authInstance.post(`/sight/${newSightId}/article`, { | ||||
|         article_id: rightArticleIdsForLink[i], | ||||
|         page_num: i + 1, // Or other logic for page_num | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     console.log("Sight created with ID:", newSightId); | ||||
|     // Optionally: this.clearCreateSight(); // To reset form after successful creation | ||||
|     return newSightId; | ||||
|   }; | ||||
|  | ||||
|   // --- Media Upload (Generic, used by dialogs) --- | ||||
|   uploadMedia = async ( | ||||
|     filename: string, | ||||
|     type: number, | ||||
|     file: File, | ||||
|     media_name?: string | ||||
|   ) => { | ||||
|   ): Promise<MediaItem> => { | ||||
|     const formData = new FormData(); | ||||
|     formData.append("file", file); | ||||
|     formData.append("filename", filename); | ||||
|     if (media_name) { | ||||
|       formData.append("media_name", media_name); | ||||
|     } | ||||
|     if (media_name) formData.append("media_name", media_name); | ||||
|     formData.append("type", type.toString()); | ||||
|  | ||||
|     try { | ||||
|       const response = await authInstance.post(`/media`, formData); | ||||
|       this.fileToUpload = null; | ||||
|       this.uploadMediaOpen = false; | ||||
|       mediaStore.getMedia(); | ||||
|       runInAction(() => { | ||||
|         this.fileToUpload = null; | ||||
|         this.uploadMediaOpen = false; | ||||
|       }); | ||||
|       mediaStore.getMedia(); // Refresh global media list | ||||
|       return { | ||||
|         id: response.data.id, | ||||
|         filename: filename, | ||||
|         media_name: media_name, | ||||
|         media_type: type, | ||||
|         filename: filename, // Or response.data.filename if backend returns it | ||||
|         media_name: media_name, // Or response.data.media_name | ||||
|         media_type: type, // Or response.data.type | ||||
|       }; | ||||
|     } catch (error) { | ||||
|       console.log(error); | ||||
|       console.error("Error uploading media:", error); | ||||
|       throw error; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   createLinkWithArticle = async (media: { | ||||
|     id: string; | ||||
|     filename: string; | ||||
|     media_name?: string; | ||||
|     media_type: number; | ||||
|   }) => { | ||||
|     await authInstance.post(`/article/${this.sight.left_article}/media`, { | ||||
|       media_id: media.id, | ||||
|       media_order: 1, | ||||
|     }); | ||||
|  | ||||
|     this.sight.ru.left.media.unshift({ | ||||
|       id: media.id, | ||||
|       media_type: media.media_type, | ||||
|       filename: media.filename, | ||||
|     }); | ||||
|  | ||||
|     this.sight.en.left.media.unshift({ | ||||
|       id: media.id, | ||||
|       media_type: media.media_type, | ||||
|       filename: media.filename, | ||||
|     }); | ||||
|  | ||||
|     this.sight.zh.left.media.unshift({ | ||||
|       id: media.id, | ||||
|       media_type: media.media_type, | ||||
|       filename: media.filename, | ||||
|     }); | ||||
|   // For Left Article Media | ||||
|   createLinkWithLeftArticle = async (media: MediaItem) => { | ||||
|     if (!this.sight.left_article || this.sight.left_article === 10000000) { | ||||
|       console.warn( | ||||
|         "Left article not selected or is a placeholder. Cannot link media yet." | ||||
|       ); | ||||
|       // If it's a placeholder, we could store the media temporarily and link it after the article is created. | ||||
|       // For simplicity, we'll assume the article must exist. | ||||
|       // A more robust solution might involve creating the article first if it's a placeholder. | ||||
|       return; | ||||
|     } | ||||
|     try { | ||||
|       await authInstance.post(`/article/${this.sight.left_article}/media`, { | ||||
|         media_id: media.id, | ||||
|         media_order: (this.sight.ru.left.media?.length || 0) + 1, | ||||
|       }); | ||||
|       runInAction(() => { | ||||
|         (["ru", "en", "zh"] as Language[]).forEach((lang) => { | ||||
|           if (!this.sight[lang].left.media) this.sight[lang].left.media = []; | ||||
|           this.sight[lang].left.media.unshift(media); | ||||
|         }); | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error("Error linking media to left article:", error); | ||||
|       throw error; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   unlinkRightAritcle = async (id: number) => { | ||||
|     this.sight.ru.right = this.sight.ru.right.filter( | ||||
|       (article) => article.id !== id | ||||
|     ); | ||||
|     this.sight.en.right = this.sight.en.right.filter( | ||||
|       (article) => article.id !== id | ||||
|     ); | ||||
|     this.sight.zh.right = this.sight.zh.right.filter( | ||||
|       (article) => article.id !== id | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   deleteRightArticle = async (id: number) => { | ||||
|     await authInstance.delete(`/article/${id}`); | ||||
|  | ||||
|     this.sight.ru.right = this.sight.ru.right.filter( | ||||
|       (article) => article.id !== id | ||||
|     ); | ||||
|     this.sight.en.right = this.sight.en.right.filter( | ||||
|       (article) => article.id !== id | ||||
|     ); | ||||
|     this.sight.zh.right = this.sight.zh.right.filter( | ||||
|       (article) => article.id !== id | ||||
|     ); | ||||
|   deleteLeftArticleMedia = async (mediaId: string) => { | ||||
|     if (!this.sight.left_article || this.sight.left_article === 10000000) | ||||
|       return; | ||||
|     try { | ||||
|       await authInstance.delete(`/article/${this.sight.left_article}/media`, { | ||||
|         data: { media_id: mediaId }, | ||||
|       }); | ||||
|       runInAction(() => { | ||||
|         (["ru", "en", "zh"] as Language[]).forEach((lang) => { | ||||
|           if (this.sight[lang].left.media) { | ||||
|             this.sight[lang].left.media = this.sight[lang].left.media.filter( | ||||
|               (m) => m.id !== mediaId | ||||
|             ); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error("Error deleting media from left article:", error); | ||||
|       throw error; | ||||
|     } | ||||
|   }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										182
									
								
								src/widgets/ImageUploadCard/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								src/widgets/ImageUploadCard/index.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ); | ||||
| }; | ||||
| @@ -3,9 +3,6 @@ import { | ||||
|   TextField, | ||||
|   Box, | ||||
|   Autocomplete, | ||||
|   Typography, | ||||
|   Paper, | ||||
|   Tooltip, | ||||
|   MenuItem, | ||||
|   Menu as MuiMenu, | ||||
| } from "@mui/material"; | ||||
| @@ -20,9 +17,9 @@ import { | ||||
|   SightLanguageInfo, | ||||
|   SightCommonInfo, | ||||
|   createSightStore, | ||||
|   UploadMediaDialog, | ||||
| } from "@shared"; | ||||
| import { LanguageSwitcher } from "@widgets"; | ||||
| import { Info, X } from "lucide-react"; | ||||
| import { ImageUploadCard, LanguageSwitcher } from "@widgets"; | ||||
|  | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { useEffect, useState } from "react"; | ||||
| @@ -33,20 +30,16 @@ import { toast } from "react-toastify"; | ||||
| export const CreateInformationTab = observer( | ||||
|   ({ value, index }: { value: number; index: number }) => { | ||||
|     const { cities } = cityStore; | ||||
|     const [, setIsMediaModalOpen] = useState(false); | ||||
|     const [mediaId, setMediaId] = useState<string>(""); | ||||
|     const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); | ||||
|  | ||||
|     const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); | ||||
|     const { language } = languageStore; | ||||
|     const { sight, updateSightInfo, createSight } = createSightStore; | ||||
|  | ||||
|     const data = sight[language]; | ||||
|  | ||||
|     const [, setCity] = useState<number>(sight.city_id ?? 0); | ||||
|     const [coordinates, setCoordinates] = useState<string>(`0 0`); | ||||
|  | ||||
|     const token = localStorage.getItem("token"); | ||||
|  | ||||
|     // Menu state for each media button | ||||
|     const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null); | ||||
|     const [activeMenuType, setActiveMenuType] = useState< | ||||
| @@ -109,6 +102,7 @@ export const CreateInformationTab = observer( | ||||
|       }); | ||||
|       setActiveMenuType(null); | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|       <> | ||||
|         <TabPanel value={value} index={index}> | ||||
| @@ -242,271 +236,77 @@ export const CreateInformationTab = observer( | ||||
|                     flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up | ||||
|                   }} | ||||
|                 > | ||||
|                   <Paper | ||||
|                     elevation={2} | ||||
|                     sx={{ | ||||
|                       padding: 2, | ||||
|                       display: "flex", | ||||
|                       flexDirection: "column", | ||||
|                       alignItems: "center", | ||||
|                       gap: 1, | ||||
|                       flex: 1, | ||||
|                       minWidth: 150, // Ensure a minimum width | ||||
|                   <ImageUploadCard | ||||
|                     title="Логотип" | ||||
|                     imageKey="thumbnail" | ||||
|                     imageUrl={sight.thumbnail} | ||||
|                     onImageClick={() => { | ||||
|                       setIsPreviewMediaOpen(true); | ||||
|                       setMediaId(sight.thumbnail ?? ""); | ||||
|                     }} | ||||
|                   > | ||||
|                     <Box sx={{ display: "flex", alignItems: "center" }}> | ||||
|                       <Typography | ||||
|                         variant="subtitle2" | ||||
|                         gutterBottom | ||||
|                         sx={{ mb: 0, mr: 0.5 }} | ||||
|                       > | ||||
|                         Логотип | ||||
|                       </Typography> | ||||
|                     </Box> | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         position: "relative", | ||||
|                         width: "200px", | ||||
|                         height: "200px", | ||||
|  | ||||
|                         display: "flex", | ||||
|                         alignItems: "center", | ||||
|                         justifyContent: "center", | ||||
|                         borderRadius: 1, | ||||
|                         mb: 1, | ||||
|                         cursor: sight.thumbnail ? "pointer" : "default", | ||||
|                       }} | ||||
|                       onClick={() => { | ||||
|                         setIsMediaModalOpen(true); | ||||
|                       }} | ||||
|                     > | ||||
|                       {sight.thumbnail && ( | ||||
|                         <button | ||||
|                           className="absolute top-2 right-2" | ||||
|                           onClick={() => { | ||||
|                             handleChange({ | ||||
|                               thumbnail: null, | ||||
|                             }); | ||||
|                             setActiveMenuType(null); | ||||
|                           }} | ||||
|                         > | ||||
|                           <X color="red" /> | ||||
|                         </button> | ||||
|                       )} | ||||
|                       {sight.thumbnail ? ( | ||||
|                         <img | ||||
|                           src={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||
|                             sight.thumbnail | ||||
|                           }/download?token=${token}`} | ||||
|                           alt="Логотип" | ||||
|                           style={{ maxWidth: "100%", maxHeight: "100%" }} | ||||
|                           onClick={() => { | ||||
|                             setIsPreviewMediaOpen(true); | ||||
|                             setMediaId(sight.thumbnail ?? ""); | ||||
|                           }} | ||||
|                         /> | ||||
|                       ) : ( | ||||
|                         <div className="w-full flex flex-col items-center justify-center gap-3"> | ||||
|                           <div className="w-full h-20 border rounded-md flex flex-col items-center transition-all duration-300 justify-center border-dashed border-gray-300 cursor-pointer hover:bg-gray-100"> | ||||
|                             <p>Перетащите файл</p> | ||||
|                           </div> | ||||
|                           <p>или</p> | ||||
|                           <Button | ||||
|                             variant="contained" | ||||
|                             color="primary" | ||||
|                             onClick={() => { | ||||
|                               setIsAddMediaOpen(true); | ||||
|                               setActiveMenuType("thumbnail"); | ||||
|                             }} | ||||
|                           > | ||||
|                             Выбрать файл | ||||
|                           </Button> | ||||
|                         </div> | ||||
|                       )} | ||||
|                     </Box> | ||||
|                   </Paper> | ||||
|                   <Paper | ||||
|                     elevation={2} | ||||
|                     sx={{ | ||||
|                       padding: 2, | ||||
|                       display: "flex", | ||||
|                       flexDirection: "column", | ||||
|                       alignItems: "center", | ||||
|                       gap: 1, | ||||
|                       flex: 1, | ||||
|                       minWidth: 150, // Ensure a minimum width | ||||
|                     onDeleteImageClick={() => { | ||||
|                       handleChange({ | ||||
|                         thumbnail: null, | ||||
|                       }); | ||||
|                       setActiveMenuType(null); | ||||
|                     }} | ||||
|                   > | ||||
|                     <Box sx={{ display: "flex", alignItems: "center" }}> | ||||
|                       <Typography | ||||
|                         variant="subtitle2" | ||||
|                         gutterBottom | ||||
|                         sx={{ mb: 0, mr: 0.5 }} | ||||
|                       > | ||||
|                         Водяной знак (л.в) | ||||
|                       </Typography> | ||||
|                       <Tooltip title={"asf"}> | ||||
|                         <Info | ||||
|                           size={16} | ||||
|                           color="gray" | ||||
|                           style={{ cursor: "pointer" }} | ||||
|                         /> | ||||
|                       </Tooltip> | ||||
|                     </Box> | ||||
|  | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         position: "relative", | ||||
|                         width: "200px", | ||||
|                         height: "200px", | ||||
|  | ||||
|                         display: "flex", | ||||
|                         alignItems: "center", | ||||
|                         justifyContent: "center", | ||||
|                         borderRadius: 1, | ||||
|                         mb: 1, | ||||
|                         cursor: sight.watermark_lu ? "pointer" : "default", | ||||
|                       }} | ||||
|                       onClick={() => { | ||||
|                         setIsMediaModalOpen(true); | ||||
|                       }} | ||||
|                     > | ||||
|                       {sight.watermark_lu && ( | ||||
|                         <button | ||||
|                           className="absolute top-2 right-2" | ||||
|                           onClick={() => { | ||||
|                             handleChange({ | ||||
|                               watermark_lu: null, | ||||
|                             }); | ||||
|                             setActiveMenuType(null); | ||||
|                           }} | ||||
|                         > | ||||
|                           <X color="red" /> | ||||
|                         </button> | ||||
|                       )} | ||||
|                       {sight.watermark_lu ? ( | ||||
|                         <img | ||||
|                           src={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||
|                             sight.watermark_lu | ||||
|                           }/download?token=${token}`} | ||||
|                           alt="Логотип" | ||||
|                           style={{ maxWidth: "100%", maxHeight: "100%" }} | ||||
|                           onClick={() => { | ||||
|                             setIsPreviewMediaOpen(true); | ||||
|                             setMediaId(sight.watermark_lu ?? ""); | ||||
|                           }} | ||||
|                         /> | ||||
|                       ) : ( | ||||
|                         <div className="w-full flex flex-col items-center justify-center gap-3"> | ||||
|                           <div className="w-full h-20 border rounded-md flex flex-col items-center transition-all duration-300 justify-center border-dashed border-gray-300 cursor-pointer hover:bg-gray-100"> | ||||
|                             <p>Перетащите файл</p> | ||||
|                           </div> | ||||
|                           <p>или</p> | ||||
|                           <Button | ||||
|                             variant="contained" | ||||
|                             color="primary" | ||||
|                             onClick={() => { | ||||
|                               setActiveMenuType("watermark_lu"); | ||||
|                               setIsAddMediaOpen(true); | ||||
|                             }} | ||||
|                           > | ||||
|                             Выбрать файл | ||||
|                           </Button> | ||||
|                         </div> | ||||
|                       )} | ||||
|                     </Box> | ||||
|                   </Paper> | ||||
|  | ||||
|                   <Paper | ||||
|                     elevation={2} | ||||
|                     sx={{ | ||||
|                       padding: 2, | ||||
|                       display: "flex", | ||||
|                       flexDirection: "column", | ||||
|                       alignItems: "center", | ||||
|                       gap: 1, | ||||
|                       flex: 1, | ||||
|                       minWidth: 150, // Ensure a minimum width | ||||
|                     onSelectFileClick={() => { | ||||
|                       setActiveMenuType("thumbnail"); | ||||
|                       setIsAddMediaOpen(true); | ||||
|                     }} | ||||
|                   > | ||||
|                     <Box sx={{ display: "flex", alignItems: "center" }}> | ||||
|                       <Typography | ||||
|                         variant="subtitle2" | ||||
|                         gutterBottom | ||||
|                         sx={{ mb: 0, mr: 0.5 }} | ||||
|                       > | ||||
|                         Водяной знак (п.в) | ||||
|                       </Typography> | ||||
|                       <Tooltip title={"asfaf"}> | ||||
|                         <Info | ||||
|                           size={16} | ||||
|                           color="gray" | ||||
|                           style={{ cursor: "pointer" }} | ||||
|                         /> | ||||
|                       </Tooltip> | ||||
|                     </Box> | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         position: "relative", | ||||
|                         width: "200px", | ||||
|                         height: "200px", | ||||
|                     setUploadMediaOpen={() => { | ||||
|                       setIsUploadMediaOpen(true); | ||||
|                       setActiveMenuType("thumbnail"); | ||||
|                     }} | ||||
|                   /> | ||||
|  | ||||
|                         display: "flex", | ||||
|                         alignItems: "center", | ||||
|                         justifyContent: "center", | ||||
|                         borderRadius: 1, | ||||
|                         mb: 1, | ||||
|                         cursor: sight.watermark_rd ? "pointer" : "default", | ||||
|                       }} | ||||
|                       onClick={() => { | ||||
|                         setIsMediaModalOpen(true); | ||||
|                       }} | ||||
|                     > | ||||
|                       {sight.watermark_rd && ( | ||||
|                         <button | ||||
|                           className="absolute top-2 right-2" | ||||
|                           onClick={() => { | ||||
|                             handleChange({ | ||||
|                               watermark_rd: null, | ||||
|                             }); | ||||
|                             setActiveMenuType(null); | ||||
|                           }} | ||||
|                         > | ||||
|                           <X color="red" /> | ||||
|                         </button> | ||||
|                       )} | ||||
|                       {sight.watermark_rd ? ( | ||||
|                         <img | ||||
|                           src={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||
|                             sight.watermark_rd | ||||
|                           }/download?token=${token}`} | ||||
|                           alt="Логотип" | ||||
|                           style={{ maxWidth: "100%", maxHeight: "100%" }} | ||||
|                           onClick={() => { | ||||
|                             setIsPreviewMediaOpen(true); | ||||
|                             setMediaId(sight.watermark_rd ?? ""); | ||||
|                           }} | ||||
|                         /> | ||||
|                       ) : ( | ||||
|                         <div className="w-full flex flex-col items-center justify-center gap-3"> | ||||
|                           <div className="w-full h-20 border rounded-md flex flex-col items-center transition-all duration-300 justify-center border-dashed border-gray-300 cursor-pointer hover:bg-gray-100"> | ||||
|                             <p>Перетащите файл</p> | ||||
|                           </div> | ||||
|                           <p>или</p> | ||||
|                           <Button | ||||
|                             variant="contained" | ||||
|                             color="primary" | ||||
|                             onClick={() => { | ||||
|                               setActiveMenuType("watermark_rd"); | ||||
|                               setIsAddMediaOpen(true); | ||||
|                             }} | ||||
|                           > | ||||
|                             Выбрать файл | ||||
|                           </Button> | ||||
|                         </div> | ||||
|                       )} | ||||
|                     </Box> | ||||
|                   </Paper> | ||||
|                   <ImageUploadCard | ||||
|                     title="Водяной знак (л.в)" | ||||
|                     imageKey="watermark_lu" | ||||
|                     imageUrl={sight.watermark_lu} | ||||
|                     onImageClick={() => { | ||||
|                       setIsPreviewMediaOpen(true); | ||||
|                       setMediaId(sight.watermark_lu ?? ""); | ||||
|                     }} | ||||
|                     onDeleteImageClick={() => { | ||||
|                       handleChange({ | ||||
|                         watermark_lu: null, | ||||
|                       }); | ||||
|                       setActiveMenuType(null); | ||||
|                     }} | ||||
|                     onSelectFileClick={() => { | ||||
|                       setActiveMenuType("watermark_lu"); | ||||
|                       setIsAddMediaOpen(true); | ||||
|                     }} | ||||
|                     setUploadMediaOpen={() => { | ||||
|                       setIsUploadMediaOpen(true); | ||||
|                       setActiveMenuType("watermark_lu"); | ||||
|                     }} | ||||
|                   /> | ||||
|  | ||||
|                   <ImageUploadCard | ||||
|                     title="Водяной знак (п.в)" | ||||
|                     imageKey="watermark_rd" | ||||
|                     imageUrl={sight.watermark_rd} | ||||
|                     onImageClick={() => { | ||||
|                       setIsPreviewMediaOpen(true); | ||||
|                       setMediaId(sight.watermark_rd ?? ""); | ||||
|                     }} | ||||
|                     onDeleteImageClick={() => { | ||||
|                       handleChange({ | ||||
|                         watermark_rd: null, | ||||
|                       }); | ||||
|                       setActiveMenuType(null); | ||||
|                     }} | ||||
|                     onSelectFileClick={() => { | ||||
|                       setActiveMenuType("watermark_rd"); | ||||
|                       setIsAddMediaOpen(true); | ||||
|                     }} | ||||
|                     setUploadMediaOpen={() => { | ||||
|                       setIsUploadMediaOpen(true); | ||||
|                       setActiveMenuType("watermark_rd"); | ||||
|                     }} | ||||
|                   /> | ||||
|                 </Box> | ||||
|               </Box> | ||||
|             </Box> | ||||
| @@ -576,6 +376,18 @@ export const CreateInformationTab = observer( | ||||
|           onClose={() => setIsPreviewMediaOpen(false)} | ||||
|           mediaId={mediaId} | ||||
|         /> | ||||
|  | ||||
|         <UploadMediaDialog | ||||
|           open={isUploadMediaOpen} | ||||
|           onClose={() => setIsUploadMediaOpen(false)} | ||||
|           afterUpload={(media) => { | ||||
|             handleChange({ | ||||
|               [activeMenuType ?? "thumbnail"]: media.id, | ||||
|             }); | ||||
|             setActiveMenuType(null); | ||||
|             setIsUploadMediaOpen(false); | ||||
|           }} | ||||
|         /> | ||||
|       </> | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -32,7 +32,7 @@ export const CreateLeftTab = observer( | ||||
|       deleteLeftArticle, | ||||
|       createLeftArticle, | ||||
|       unlinkLeftArticle, | ||||
|       createLinkWithArticle, | ||||
|       createLinkWithLeftArticle, | ||||
|     } = createSightStore; | ||||
|     const { | ||||
|       deleteMedia, | ||||
| @@ -75,10 +75,10 @@ export const CreateLeftTab = observer( | ||||
|         media_name?: string; | ||||
|         media_type: number; | ||||
|       }) => { | ||||
|         await createLinkWithArticle(media); | ||||
|         await createLinkWithLeftArticle(media); | ||||
|         setIsSelectMediaDialogOpen(false); | ||||
|       }, | ||||
|       [createLinkWithArticle] | ||||
|       [createLinkWithLeftArticle] | ||||
|     ); | ||||
|  | ||||
|     const handleArticleSelect = useCallback( | ||||
| @@ -437,7 +437,7 @@ export const CreateLeftTab = observer( | ||||
|           afterUpload={async (media) => { | ||||
|             setUploadMediaOpen(false); | ||||
|             setFileToUpload(null); | ||||
|             await createLinkWithArticle(media); | ||||
|             await createLinkWithLeftArticle(media); | ||||
|           }} | ||||
|         /> | ||||
|         <SelectArticleModal | ||||
|   | ||||
| @@ -13,42 +13,179 @@ import { | ||||
|   languageStore, | ||||
|   SelectArticleModal, | ||||
|   TabPanel, | ||||
|   SelectMediaDialog, // Import | ||||
|   UploadMediaDialog, // Import | ||||
| } from "@shared"; | ||||
| import { | ||||
|   LanguageSwitcher, | ||||
|   MediaArea, // Import | ||||
|   MediaAreaForSight, // Import | ||||
|   ReactMarkdownComponent, | ||||
|   ReactMarkdownEditor, | ||||
| } from "@widgets"; | ||||
| import { ImagePlus, Plus } from "lucide-react"; | ||||
| import { ImagePlus, Plus, X } from "lucide-react"; // Import X | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { useState } from "react"; | ||||
| import { useState, useEffect } from "react"; // Added useEffect | ||||
| import { MediaViewer } from "../../MediaViewer/index"; | ||||
| import { toast } from "react-toastify"; | ||||
|  | ||||
| type MediaItemShared = { | ||||
|   // Define if not already available from @shared | ||||
|   id: string; | ||||
|   filename: string; | ||||
|   media_name?: string; | ||||
|   media_type: number; | ||||
| }; | ||||
|  | ||||
| // --- RightWidgetTab (Parent) Component --- | ||||
| export const CreateRightTab = observer( | ||||
|   ({ value, index }: { value: number; index: number }) => { | ||||
|     const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
|     const { sight, createNewRightArticle, updateRightArticleInfo } = | ||||
|       createSightStore; | ||||
|     const { | ||||
|       sight, | ||||
|       createNewRightArticle, | ||||
|       updateRightArticleInfo, | ||||
|       linkPreviewMedia, | ||||
|       unlinkPreviewMedia, | ||||
|       createLinkWithRightArticle, | ||||
|       deleteRightArticleMedia, | ||||
|       setFileToUpload, // From store | ||||
|       setUploadMediaOpen, // From store | ||||
|       uploadMediaOpen, // From store | ||||
|       unlinkRightAritcle, // Corrected spelling | ||||
|       deleteRightArticle, | ||||
|       linkExistingRightArticle, | ||||
|       createSight, | ||||
|       clearCreateSight, // For resetting form | ||||
|     } = createSightStore; | ||||
|     const { language } = languageStore; | ||||
|     const [articleDialogOpen, setArticleDialogOpen] = useState(false); | ||||
|  | ||||
|     const [selectArticleDialogOpen, setSelectArticleDialogOpen] = | ||||
|       useState(false); | ||||
|     const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>( | ||||
|       null | ||||
|     ); | ||||
|     const [type, setType] = useState<"article" | "media">("media"); | ||||
|     const open = Boolean(anchorEl); | ||||
|     const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { | ||||
|  | ||||
|     const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] = | ||||
|       useState(false); | ||||
|     const [mediaTarget, setMediaTarget] = useState< | ||||
|       "sightPreview" | "rightArticle" | null | ||||
|     >(null); | ||||
|  | ||||
|     // Reset activeArticleIndex if language changes and index is out of bounds | ||||
|     useEffect(() => { | ||||
|       if ( | ||||
|         activeArticleIndex !== null && | ||||
|         activeArticleIndex >= sight[language].right.length | ||||
|       ) { | ||||
|         setActiveArticleIndex(null); | ||||
|         setType("media"); // Default back to media preview if selected article disappears | ||||
|       } | ||||
|     }, [language, sight[language].right, activeArticleIndex]); | ||||
|  | ||||
|     const openMenu = Boolean(anchorEl); | ||||
|     const handleClickMenu = (event: React.MouseEvent<HTMLButtonElement>) => { | ||||
|       setAnchorEl(event.currentTarget); | ||||
|     }; | ||||
|     const handleClose = () => { | ||||
|     const handleCloseMenu = () => { | ||||
|       setAnchorEl(null); | ||||
|     }; | ||||
|     const handleSave = () => { | ||||
|       console.log("Saving right widget..."); | ||||
|  | ||||
|     const handleSave = async () => { | ||||
|       try { | ||||
|         await createSight(language); | ||||
|         toast.success("Достопримечательность успешно создана!"); | ||||
|         clearCreateSight(); // Reset form | ||||
|         setActiveArticleIndex(null); | ||||
|         setType("media"); | ||||
|         // Potentially navigate away: history.push('/sights-list'); | ||||
|       } catch (error) { | ||||
|         console.error("Failed to save sight:", error); | ||||
|         toast.error("Ошибка при создании достопримечательности."); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     const handleSelectArticle = (index: number) => { | ||||
|       setActiveArticleIndex(index); | ||||
|     const handleDisplayArticleFromList = (idx: number) => { | ||||
|       setActiveArticleIndex(idx); | ||||
|       setType("article"); | ||||
|     }; | ||||
|  | ||||
|     const handleCreateNewLocalArticle = async () => { | ||||
|       handleCloseMenu(); | ||||
|       try { | ||||
|         const newArticleId = await createNewRightArticle(); | ||||
|         // Automatically select the new article if ID is returned | ||||
|         const newIndex = sight[language].right.findIndex( | ||||
|           (a) => a.id === newArticleId | ||||
|         ); | ||||
|         if (newIndex > -1) { | ||||
|           setActiveArticleIndex(newIndex); | ||||
|           setType("article"); | ||||
|         } else { | ||||
|           // Fallback if findIndex fails (should not happen if store updates correctly) | ||||
|           setActiveArticleIndex(sight[language].right.length - 1); | ||||
|           setType("article"); | ||||
|         } | ||||
|       } catch (error) { | ||||
|         toast.error("Не удалось создать новую статью."); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     const handleSelectExistingArticleAndLink = async ( | ||||
|       selectedArticleId: number | ||||
|     ) => { | ||||
|       try { | ||||
|         await linkExistingRightArticle(selectedArticleId); | ||||
|         setSelectArticleDialogOpen(false); // Close dialog | ||||
|         const newIndex = sight[language].right.findIndex( | ||||
|           (a) => a.id === selectedArticleId | ||||
|         ); | ||||
|         if (newIndex > -1) { | ||||
|           setActiveArticleIndex(newIndex); | ||||
|           setType("article"); | ||||
|         } | ||||
|       } catch (error) { | ||||
|         toast.error("Не удалось привязать существующую статью."); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     const currentRightArticle = | ||||
|       activeArticleIndex !== null && sight[language].right[activeArticleIndex] | ||||
|         ? sight[language].right[activeArticleIndex] | ||||
|         : null; | ||||
|  | ||||
|     // Media Handling for Dialogs | ||||
|     const handleOpenUploadMedia = () => { | ||||
|       setUploadMediaOpen(true); | ||||
|     }; | ||||
|  | ||||
|     const handleOpenSelectMediaDialog = ( | ||||
|       target: "sightPreview" | "rightArticle" | ||||
|     ) => { | ||||
|       setMediaTarget(target); | ||||
|       setIsSelectMediaDialogOpen(true); | ||||
|     }; | ||||
|  | ||||
|     const handleMediaSelectedFromDialog = async (media: MediaItemShared) => { | ||||
|       setIsSelectMediaDialogOpen(false); | ||||
|       if (mediaTarget === "sightPreview") { | ||||
|         await linkPreviewMedia(media.id); | ||||
|       } else if (mediaTarget === "rightArticle" && currentRightArticle) { | ||||
|         await createLinkWithRightArticle(media, currentRightArticle.id); | ||||
|       } | ||||
|       setMediaTarget(null); | ||||
|     }; | ||||
|  | ||||
|     const handleMediaUploaded = async (media: MediaItemShared) => { | ||||
|       // After UploadMediaDialog finishes | ||||
|       setUploadMediaOpen(false); | ||||
|       setFileToUpload(null); | ||||
|       if (mediaTarget === "sightPreview") { | ||||
|         linkPreviewMedia(media.id); | ||||
|       } else if (mediaTarget === "rightArticle" && currentRightArticle) { | ||||
|         await createLinkWithRightArticle(media, currentRightArticle.id); | ||||
|       } | ||||
|       setMediaTarget(null); // Reset target | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
| @@ -59,7 +196,7 @@ export const CreateRightTab = observer( | ||||
|             display: "flex", | ||||
|             flexDirection: "column", | ||||
|             height: "100%", | ||||
|             minHeight: "calc(100vh - 200px)", // Adjust as needed | ||||
|             minHeight: "calc(100vh - 200px)", | ||||
|             gap: 2, | ||||
|             paddingBottom: "70px", // Space for the save button | ||||
|             position: "relative", | ||||
| @@ -68,336 +205,389 @@ export const CreateRightTab = observer( | ||||
|           <BackButton /> | ||||
|  | ||||
|           <Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}> | ||||
|             {/* Left Column: Navigation & Article List */} | ||||
|             <Box className="flex flex-col w-[75%] gap-2"> | ||||
|               <Box className="w-full flex gap-2 "> | ||||
|                 <Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl  overflow-y-auto gap-3 border border-gray-300 p-3"> | ||||
|                 <Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl overflow-y-auto gap-3 border border-gray-300 p-3"> | ||||
|                   <Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto"> | ||||
|                     <Box | ||||
|                       onClick={() => { | ||||
|                         setType("media"); | ||||
|                         // setActiveArticleIndex(null); // Optional: deselect article when switching to general media view | ||||
|                       }} | ||||
|                       className="w-full  bg-green-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300" | ||||
|                       className={`w-full p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300 ${ | ||||
|                         type === "media" | ||||
|                           ? "bg-green-300 font-semibold" | ||||
|                           : "bg-green-200" | ||||
|                       }`} | ||||
|                     > | ||||
|                       <Typography>Предпросмотр медиа</Typography> | ||||
|                     </Box> | ||||
|  | ||||
|                     {sight[language].right.map((article, index) => ( | ||||
|                     {sight[language].right.map((article, artIdx) => ( | ||||
|                       <Box | ||||
|                         key={index} | ||||
|                         className="w-full  bg-gray-200 p-4 rounded-2xl  text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300" | ||||
|                         onClick={() => { | ||||
|                           handleSelectArticle(index); | ||||
|                           setType("article"); | ||||
|                         }} | ||||
|                         key={article.id || artIdx} // article.id should be preferred | ||||
|                         className={`w-full p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300 ${ | ||||
|                           type === "article" && activeArticleIndex === artIdx | ||||
|                             ? "bg-blue-300 font-semibold" | ||||
|                             : "bg-gray-200" | ||||
|                         }`} | ||||
|                         onClick={() => handleDisplayArticleFromList(artIdx)} | ||||
|                       > | ||||
|                         <Typography>{article.heading}</Typography> | ||||
|                         <Typography noWrap title={article.heading}> | ||||
|                           {article.heading || "Без названия"} | ||||
|                         </Typography> | ||||
|                       </Box> | ||||
|                     ))} | ||||
|                   </Box> | ||||
|                   <button | ||||
|                     className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center" | ||||
|                     onClick={handleClick} | ||||
|                     className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center hover:bg-blue-600" | ||||
|                     onClick={handleClickMenu} | ||||
|                     aria-controls={openMenu ? "add-article-menu" : undefined} | ||||
|                     aria-haspopup="true" | ||||
|                     aria-expanded={openMenu ? "true" : undefined} | ||||
|                   > | ||||
|                     <Plus size={20} color="white" /> | ||||
|                   </button> | ||||
|                   <Menu | ||||
|                     id="basic-menu" | ||||
|                     id="add-article-menu" | ||||
|                     anchorEl={anchorEl} | ||||
|                     open={open} | ||||
|                     onClose={handleClose} | ||||
|                     MenuListProps={{ | ||||
|                       "aria-labelledby": "basic-button", | ||||
|                     }} | ||||
|                     sx={{ | ||||
|                       mt: 1, | ||||
|                     }} | ||||
|                     open={openMenu} | ||||
|                     onClose={handleCloseMenu} | ||||
|                     MenuListProps={{ "aria-labelledby": "basic-button" }} | ||||
|                     sx={{ mt: 1 }} | ||||
|                   > | ||||
|                     <MenuItem | ||||
|                       onClick={() => { | ||||
|                         createNewRightArticle(); | ||||
|                         handleClose(); | ||||
|                       }} | ||||
|                     > | ||||
|                     <MenuItem onClick={handleCreateNewLocalArticle}> | ||||
|                       <Typography>Создать новую</Typography> | ||||
|                     </MenuItem> | ||||
|                     <MenuItem | ||||
|                       onClick={() => { | ||||
|                         setArticleDialogOpen(true); | ||||
|                         handleClose(); | ||||
|                         setSelectArticleDialogOpen(true); | ||||
|                         handleCloseMenu(); | ||||
|                       }} | ||||
|                     > | ||||
|                       <Typography>Выбрать существующую статью</Typography> | ||||
|                     </MenuItem> | ||||
|                   </Menu> | ||||
|                 </Box> | ||||
|                 {type === "article" && ( | ||||
|                   <Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3"> | ||||
|                     {activeArticleIndex !== null && ( | ||||
|                       <> | ||||
|                         <Box className="flex justify-end gap-2 mb-3"> | ||||
|                           <Button variant="contained" color="primary"> | ||||
|                             Открепить | ||||
|                           </Button> | ||||
|  | ||||
|                           <Button variant="contained" color="success"> | ||||
|                             Удалить | ||||
|                           </Button> | ||||
|                         </Box> | ||||
|                         <Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}> | ||||
|                           {/* Левая колонка: Редактирование */} | ||||
|  | ||||
|                           <Box | ||||
|                             sx={{ | ||||
|                               flex: 2, | ||||
|                               display: "flex", | ||||
|                               flexDirection: "column", | ||||
|                               gap: 2, | ||||
|                             }} | ||||
|                           > | ||||
|                             <TextField | ||||
|                               label="Название информации" | ||||
|                               value={ | ||||
|                                 sight[language].right[activeArticleIndex] | ||||
|                                   .heading | ||||
|                               } | ||||
|                               onChange={(e) => | ||||
|                                 updateRightArticleInfo( | ||||
|                                   activeArticleIndex, | ||||
|                                   language, | ||||
|                                   e.target.value, | ||||
|                                   sight[language].right[activeArticleIndex].body | ||||
|                                 ) | ||||
|                               } | ||||
|                               variant="outlined" | ||||
|                               fullWidth | ||||
|                             /> | ||||
|  | ||||
|                             <ReactMarkdownEditor | ||||
|                               value={ | ||||
|                                 sight[language].right[activeArticleIndex].body | ||||
|                               } | ||||
|                               onChange={(value) => | ||||
|                                 updateRightArticleInfo( | ||||
|                                   activeArticleIndex, | ||||
|                                   language, | ||||
|                                   sight[language].right[activeArticleIndex] | ||||
|                                     .heading, | ||||
|                                   value | ||||
|                                 ) | ||||
|                               } | ||||
|                             /> | ||||
|                           </Box> | ||||
|                           {/* Блок МЕДИА для статьи */} | ||||
|                           {/* <Paper elevation={1} sx={{ padding: 2, mt: 1 }}> | ||||
|                     <Typography variant="h6" gutterBottom> | ||||
|                       МЕДИА | ||||
|                     </Typography> | ||||
|                     {data.left.media ? ( | ||||
|                       <Box sx={{ mb: 1 }}> | ||||
|                         <img | ||||
|                           src={data.left.media.filename} | ||||
|                           alt="Selected media" | ||||
|                           style={{ | ||||
|                             maxWidth: "100%", | ||||
|                             maxHeight: "150px", | ||||
|                             objectFit: "contain", | ||||
|                           }} | ||||
|                         /> | ||||
|                       </Box> | ||||
|                     ) : ( | ||||
|                       <Box | ||||
|                         sx={{ | ||||
|                           width: "100%", | ||||
|                           height: 100, | ||||
|                           backgroundColor: "grey.100", | ||||
|                           display: "flex", | ||||
|                           alignItems: "center", | ||||
|                           justifyContent: "center", | ||||
|                           borderRadius: 1, | ||||
|                           mb: 1, | ||||
|                           border: "2px dashed", | ||||
|                           borderColor: "grey.300", | ||||
|                         }} | ||||
|                       > | ||||
|                         <Typography color="text.secondary">Нет медиа</Typography> | ||||
|                       </Box> | ||||
|                     )} | ||||
|                     <Button | ||||
|                       variant="contained" | ||||
|                       startIcon={<ImagePlus size={18} />} | ||||
|                       onClick={handleOpenMediaDialog} | ||||
|                     > | ||||
|                       Выбрать/Загрузить медиа | ||||
|                     </Button> | ||||
|                     {data.left.media && ( | ||||
|                 {/* Main content area: Article Editor or Sight Media Preview */} | ||||
|                 {type === "article" && currentRightArticle ? ( | ||||
|                   <Box className="w-[80%] border border-gray-300 rounded-2xl p-3 flex flex-col gap-2 overflow-hidden"> | ||||
|                     <Box className="flex justify-end gap-2 mb-1 flex-shrink-0"> | ||||
|                       <Button | ||||
|                         variant="outlined" | ||||
|                         color="warning" | ||||
|                         size="small" | ||||
|                         onClick={() => { | ||||
|                           if (currentRightArticle) { | ||||
|                             unlinkRightAritcle(currentRightArticle.id); // Corrected function name | ||||
|                             setActiveArticleIndex(null); | ||||
|                             setType("media"); | ||||
|                           } | ||||
|                         }} | ||||
|                       > | ||||
|                         Открепить | ||||
|                       </Button> | ||||
|                       <Button | ||||
|                         variant="contained" | ||||
|                         color="error" | ||||
|                         size="small" | ||||
|                         sx={{ ml: 1 }} | ||||
|                         onClick={() => | ||||
|                           updateSightInfo( | ||||
|                             languageStore.language, | ||||
|                             { | ||||
|                               left: { | ||||
|                                   heading:      data.left.heading, | ||||
|                                 body: data.left.body, | ||||
|                                 media: null, | ||||
|                               }, | ||||
|                             }, | ||||
|                             false | ||||
|                         onClick={async () => { | ||||
|                           if ( | ||||
|                             currentRightArticle && | ||||
|                             window.confirm( | ||||
|                               `Удалить статью "${currentRightArticle.heading}" окончательно?` | ||||
|                             ) | ||||
|                           ) { | ||||
|                             try { | ||||
|                               await deleteRightArticle(currentRightArticle.id); | ||||
|                               setActiveArticleIndex(null); | ||||
|                               setType("media"); | ||||
|                               toast.success("Статья удалена"); | ||||
|                             } catch { | ||||
|                               toast.error("Не удалось удалить статью"); | ||||
|                             } | ||||
|                           } | ||||
|                         }} | ||||
|                       > | ||||
|                         Удалить | ||||
|                       </Button> | ||||
|                     </Box> | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         flexGrow: 1, | ||||
|                         display: "flex", | ||||
|                         flexDirection: "column", | ||||
|                         gap: 2, | ||||
|                         overflowY: "auto", | ||||
|                         pt: 7, | ||||
|                       }} | ||||
|                     > | ||||
|                       <TextField | ||||
|                         label="Название информации (правый виджет)" | ||||
|                         value={currentRightArticle.heading} | ||||
|                         onChange={(e) => | ||||
|                           activeArticleIndex !== null && | ||||
|                           updateRightArticleInfo( | ||||
|                             activeArticleIndex, | ||||
|                             language, | ||||
|                             e.target.value, | ||||
|                             currentRightArticle.body | ||||
|                           ) | ||||
|                         } | ||||
|                       > | ||||
|                         Удалить медиа | ||||
|                       </Button> | ||||
|                     )} | ||||
|                   </Paper> */} | ||||
|                         </Box> | ||||
|                       </> | ||||
|                         variant="outlined" | ||||
|                         fullWidth | ||||
|                       /> | ||||
|                       <Box sx={{ minHeight: 200, flexGrow: 1 }}> | ||||
|                         <ReactMarkdownEditor | ||||
|                           value={currentRightArticle.body} | ||||
|                           onChange={(mdValue) => | ||||
|                             activeArticleIndex !== null && | ||||
|                             updateRightArticleInfo( | ||||
|                               activeArticleIndex, | ||||
|                               language, | ||||
|                               currentRightArticle.heading, | ||||
|                               mdValue || "" | ||||
|                             ) | ||||
|                           } | ||||
|                         /> | ||||
|                       </Box> | ||||
|                       <MediaArea | ||||
|                         articleId={currentRightArticle.id} // Needs a real ID | ||||
|                         mediaIds={currentRightArticle.media || []} | ||||
|                         onFilesDrop={(files) => { | ||||
|                           if (files.length > 0) { | ||||
|                             setFileToUpload(files[0]); | ||||
|                             setMediaTarget("rightArticle"); | ||||
|                             handleOpenUploadMedia(); | ||||
|                           } | ||||
|                         }} | ||||
|                         deleteMedia={deleteRightArticleMedia} | ||||
|                         setSelectMediaDialogOpen={() => | ||||
|                           handleOpenSelectMediaDialog("rightArticle") | ||||
|                         } | ||||
|                       /> | ||||
|                     </Box> | ||||
|                   </Box> | ||||
|                 ) : type === "media" ? ( | ||||
|                   <Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 relative flex justify-center items-center"> | ||||
|                     {type === "media" && ( | ||||
|                       <Box className="w-[80%] border border-gray-300 rounded-2xl relative"> | ||||
|                         {sight.preview_media && ( | ||||
|                           <> | ||||
|                             <Box className="absolute top-4 right-4"> | ||||
|                               <button | ||||
|                                 className="w-10 h-10 flex items-center justify-center" | ||||
|                                 onClick={unlinkPreviewMedia} | ||||
|                               > | ||||
|                                 <X size={20} color="red" /> | ||||
|                               </button> | ||||
|                             </Box> | ||||
|  | ||||
|                             <MediaViewer | ||||
|                               media={{ | ||||
|                                 id: sight.preview_media || "", | ||||
|                                 media_type: 1, | ||||
|                                 filename: sight.preview_media || "", | ||||
|                               }} | ||||
|                             /> | ||||
|                           </> | ||||
|                         )} | ||||
|                         {!sight.preview_media && ( | ||||
|                           <MediaAreaForSight | ||||
|                             onFinishUpload={(mediaId) => { | ||||
|                               linkPreviewMedia(mediaId); | ||||
|                             }} | ||||
|                             onFilesDrop={() => {}} | ||||
|                           /> | ||||
|                         )} | ||||
|                       </Box> | ||||
|                     )} | ||||
|                   </Box> | ||||
|                 )} | ||||
|                 {type === "media" && ( | ||||
|                 ) : ( | ||||
|                   <Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center"> | ||||
|                     <MediaViewer | ||||
|                       media={{ | ||||
|                         id: sight.preview_media || "", | ||||
|                         media_type: 1, | ||||
|                         filename: sight.preview_media || "", | ||||
|                       }} | ||||
|                     /> | ||||
|                     <Typography variant="h6" color="text.secondary"> | ||||
|                       Выберите статью слева или секцию "Предпросмотр медиа" | ||||
|                     </Typography> | ||||
|                   </Box> | ||||
|                 )} | ||||
|               </Box> | ||||
|             </Box> | ||||
|  | ||||
|             {/* Right Column: Live Preview */} | ||||
|             <Box className="w-[25%] mr-10"> | ||||
|               {activeArticleIndex !== null && ( | ||||
|               {type === "article" && currentRightArticle && ( | ||||
|                 <Paper | ||||
|                   className="flex-1 flex flex-col rounded-2xl" | ||||
|                   elevation={2} | ||||
|                   sx={{ height: "75vh", overflow: "hidden" }} | ||||
|                 > | ||||
|                   <Box | ||||
|                     className="rounded-2xl overflow-hidden" | ||||
|                     sx={{ | ||||
|                       width: "100%", | ||||
|                       height: "75vh", | ||||
|                       background: "#877361", | ||||
|                       borderColor: "grey.300", | ||||
|                       height: "100%", | ||||
|                       background: "#877361", // Theme background | ||||
|                       display: "flex", | ||||
|                       flexDirection: "column", | ||||
|                     }} | ||||
|                   > | ||||
|                     {type === "media" ? ( | ||||
|                     {currentRightArticle.media && | ||||
|                     currentRightArticle.media.length > 0 ? ( | ||||
|                       <MediaViewer media={currentRightArticle.media[0]} /> | ||||
|                     ) : ( | ||||
|                       <Box | ||||
|                         sx={{ | ||||
|                           width: "100%", | ||||
|                           height: "100%", | ||||
|                           height: 200, | ||||
|                           flexShrink: 0, | ||||
|                           backgroundColor: "rgba(0,0,0,0.1)", | ||||
|                           display: "flex", | ||||
|                           alignItems: "center", | ||||
|                           justifyContent: "center", | ||||
|                         }} | ||||
|                       > | ||||
|                         <Typography color="white">Загрузка...</Typography> | ||||
|                         <ImagePlus size={48} color="white" /> | ||||
|                       </Box> | ||||
|                     ) : ( | ||||
|                       <> | ||||
|                         <Box | ||||
|                           sx={{ | ||||
|                             width: "100%", | ||||
|                             height: 200, | ||||
|                             flexShrink: 0, | ||||
|                             backgroundColor: "rgba(0,0,0,0.1)", | ||||
|                             display: "flex", | ||||
|                             alignItems: "center", | ||||
|                             justifyContent: "center", | ||||
|                           }} | ||||
|                         > | ||||
|                           <ImagePlus size={48} color="white" /> | ||||
|                         </Box> | ||||
|  | ||||
|                         <Box | ||||
|                           sx={{ | ||||
|                             width: "100%", | ||||
|                             minHeight: "70px", | ||||
|                             background: "#877361", | ||||
|                             display: "flex", | ||||
|                             flexShrink: 0, | ||||
|                             alignItems: "center", | ||||
|                             borderBottom: "1px solid rgba(255,255,255,0.1)", | ||||
|                             px: 2, | ||||
|                           }} | ||||
|                         > | ||||
|                           <Typography variant="h6" color="white"> | ||||
|                             {sight[language].right[activeArticleIndex] | ||||
|                               .heading || "Выберите статью"} | ||||
|                           </Typography> | ||||
|                         </Box> | ||||
|  | ||||
|                         <Box | ||||
|                           sx={{ | ||||
|                             px: 2, | ||||
|                             flexGrow: 1, | ||||
|  | ||||
|                             overflowY: "auto", | ||||
|                             backgroundColor: "#877361", | ||||
|                             color: "white", | ||||
|                             py: 1, | ||||
|                           }} | ||||
|                         > | ||||
|                           {sight[language].right[activeArticleIndex].body ? ( | ||||
|                             <ReactMarkdownComponent | ||||
|                               value={ | ||||
|                                 sight[language].right[activeArticleIndex].body | ||||
|                               } | ||||
|                             /> | ||||
|                           ) : ( | ||||
|                             <Typography | ||||
|                               color="rgba(255,255,255,0.7)" | ||||
|                               sx={{ textAlign: "center", mt: 4 }} | ||||
|                             > | ||||
|                               Предпросмотр статьи появится здесь | ||||
|                             </Typography> | ||||
|                           )} | ||||
|                         </Box> | ||||
|                       </> | ||||
|                     )} | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         width: "100%", | ||||
|                         minHeight: "70px", // Fixed height for heading container | ||||
|                         background: "#877361", // Consistent with theme | ||||
|                         display: "flex", | ||||
|                         flexShrink: 0, | ||||
|                         flex: 1, | ||||
|                         alignItems: "center", | ||||
|                         borderBottom: "1px solid rgba(255,255,255,0.1)", | ||||
|                         px: 2, | ||||
|                         py: 1, | ||||
|                       }} | ||||
|                     > | ||||
|                       <Typography | ||||
|                         variant="h6" | ||||
|                         color="white" | ||||
|                         noWrap | ||||
|                         title={currentRightArticle.heading} | ||||
|                       > | ||||
|                         {currentRightArticle.heading || "Заголовок"} | ||||
|                       </Typography> | ||||
|                     </Box> | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         px: 2, | ||||
|                         py: 1, | ||||
|                         flexGrow: 1, | ||||
|                         overflowY: "auto", | ||||
|                         backgroundColor: "#877361", | ||||
|                         color: "white", | ||||
|                         "&::-webkit-scrollbar": { width: "8px" }, | ||||
|                         "&::-webkit-scrollbar-thumb": { | ||||
|                           backgroundColor: "rgba(255,255,255,0.3)", | ||||
|                           borderRadius: "4px", | ||||
|                         }, | ||||
|                       }} | ||||
|                     > | ||||
|                       {currentRightArticle.body ? ( | ||||
|                         <ReactMarkdownComponent | ||||
|                           value={currentRightArticle.body} | ||||
|                         /> | ||||
|                       ) : ( | ||||
|                         <Typography | ||||
|                           color="rgba(255,255,255,0.7)" | ||||
|                           sx={{ textAlign: "center", mt: 4 }} | ||||
|                         > | ||||
|                           Содержимое статьи... | ||||
|                         </Typography> | ||||
|                       )} | ||||
|                     </Box> | ||||
|                   </Box> | ||||
|                 </Paper> | ||||
|               )} | ||||
|               {/* Optional: Preview for sight.preview_media when type === "media" */} | ||||
|               {type === "media" && sight.preview_media && ( | ||||
|                 <Paper | ||||
|                   className="flex-1 flex flex-col rounded-2xl" | ||||
|                   elevation={2} | ||||
|                   sx={{ height: "75vh", overflow: "hidden" }} | ||||
|                 > | ||||
|                   <Box | ||||
|                     sx={{ | ||||
|                       width: "100%", | ||||
|                       height: "100%", | ||||
|                       background: "#877361", | ||||
|                       display: "flex", | ||||
|                       alignItems: "center", | ||||
|                       justifyContent: "center", | ||||
|                     }} | ||||
|                   > | ||||
|                     <MediaViewer | ||||
|                       media={{ | ||||
|                         id: sight.preview_media, | ||||
|                         filename: sight.preview_media, | ||||
|                         media_type: 1, | ||||
|                       }} | ||||
|                     /> | ||||
|                   </Box> | ||||
|                 </Paper> | ||||
|               )} | ||||
|             </Box> | ||||
|           </Box> | ||||
|  | ||||
|           {/* Sticky Save Button Footer */} | ||||
|           <Box | ||||
|             sx={{ | ||||
|               position: "absolute", | ||||
|               bottom: 0, | ||||
|               left: 0, // ensure it spans from left | ||||
|               right: 0, | ||||
|               padding: 2, | ||||
|               backgroundColor: "background.paper", // Ensure button is visible | ||||
|               width: "100%", // Cover the full width to make it a sticky footer | ||||
|               backgroundColor: "background.paper", | ||||
|               borderTop: "1px solid", // Add a subtle top border | ||||
|               borderColor: "divider", // Use theme's divider color | ||||
|               width: "100%", | ||||
|               display: "flex", | ||||
|               justifyContent: "flex-end", | ||||
|               boxShadow: "0 -2px 5px rgba(0,0,0,0.1)", // Optional shadow | ||||
|             }} | ||||
|           > | ||||
|             <Button variant="contained" color="success" onClick={handleSave}> | ||||
|               Сохранить изменения | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               color="success" | ||||
|               onClick={handleSave} | ||||
|               size="large" | ||||
|             > | ||||
|               Сохранить достопримечательность | ||||
|             </Button> | ||||
|           </Box> | ||||
|         </Box> | ||||
|         {/*  | ||||
|  | ||||
|         {/* Modals */} | ||||
|         <SelectArticleModal | ||||
|             open={openedType === "article"} | ||||
|           onClose={handleCloseSelectModal} | ||||
|           onSelectArticle={handleSelectArticle} | ||||
|           linkedArticleIds={linkedArticleIds} | ||||
|         /> */} | ||||
|         <SelectArticleModal | ||||
|           open={articleDialogOpen} | ||||
|           onClose={() => setArticleDialogOpen(false)} | ||||
|           onSelectArticle={handleSelectArticle} | ||||
|           open={selectArticleDialogOpen} | ||||
|           onClose={() => setSelectArticleDialogOpen(false)} | ||||
|           onSelectArticle={handleSelectExistingArticleAndLink} | ||||
|           // Pass IDs of already linked/added right articles to exclude them from selection | ||||
|           linkedArticleIds={sight[language].right.map((article) => article.id)} | ||||
|         /> | ||||
|         <UploadMediaDialog | ||||
|           open={uploadMediaOpen} // From store | ||||
|           onClose={() => { | ||||
|             setUploadMediaOpen(false); | ||||
|             setFileToUpload(null); // Clear file if dialog is closed without upload | ||||
|             setMediaTarget(null); | ||||
|           }} | ||||
|           afterUpload={handleMediaUploaded} // This will use the mediaTarget | ||||
|         /> | ||||
|         <SelectMediaDialog | ||||
|           open={isSelectMediaDialogOpen} | ||||
|           onClose={() => { | ||||
|             setIsSelectMediaDialogOpen(false); | ||||
|             setMediaTarget(null); | ||||
|           }} | ||||
|           onSelectMedia={handleMediaSelectedFromDialog} | ||||
|         /> | ||||
|       </TabPanel> | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -3,9 +3,6 @@ import { | ||||
|   TextField, | ||||
|   Box, | ||||
|   Autocomplete, | ||||
|   Typography, | ||||
|   Paper, | ||||
|   Tooltip, | ||||
|   MenuItem, | ||||
|   Menu as MuiMenu, | ||||
| } from "@mui/material"; | ||||
| @@ -20,9 +17,9 @@ import { | ||||
|   PreviewMediaDialog, | ||||
|   SightLanguageInfo, | ||||
|   SightCommonInfo, | ||||
|   UploadMediaDialog, | ||||
| } from "@shared"; | ||||
| import { LanguageSwitcher } from "@widgets"; | ||||
| import { Info, ImagePlus } from "lucide-react"; | ||||
| import { ImageUploadCard, LanguageSwitcher } from "@widgets"; | ||||
|  | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { useEffect, useState } from "react"; | ||||
| @@ -34,10 +31,10 @@ import { toast } from "react-toastify"; | ||||
| export const InformationTab = observer( | ||||
|   ({ value, index }: { value: number; index: number }) => { | ||||
|     const { cities } = cityStore; | ||||
|     const [, setIsMediaModalOpen] = useState(false); | ||||
|  | ||||
|     const [mediaId, setMediaId] = useState<string>(""); | ||||
|     const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); | ||||
|  | ||||
|     const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); | ||||
|     const { language } = languageStore; | ||||
|  | ||||
|     const { sight, updateSightInfo, updateSight } = editSightStore; | ||||
| @@ -45,8 +42,6 @@ export const InformationTab = observer( | ||||
|     const [, setCity] = useState<number>(sight.common.city_id ?? 0); | ||||
|     const [coordinates, setCoordinates] = useState<string>(`0 0`); | ||||
|  | ||||
|     const token = localStorage.getItem("token"); | ||||
|  | ||||
|     // Menu state for each media button | ||||
|     const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null); | ||||
|     const [activeMenuType, setActiveMenuType] = useState< | ||||
| @@ -76,12 +71,22 @@ export const InformationTab = observer( | ||||
|       handleMenuClose(); | ||||
|     }; | ||||
|  | ||||
|     const handleMediaSelect = () => { | ||||
|     const handleMediaSelect = (media: { | ||||
|       id: string; | ||||
|       filename: string; | ||||
|       media_name?: string; | ||||
|       media_type: number; | ||||
|     }) => { | ||||
|       if (!activeMenuType) return; | ||||
|  | ||||
|       // Close the dialog | ||||
|       setIsAddMediaOpen(false); | ||||
|       handleChange( | ||||
|         language as Language, | ||||
|         { | ||||
|           [activeMenuType ?? "thumbnail"]: media.id, | ||||
|         }, | ||||
|         true | ||||
|       ); | ||||
|       setActiveMenuType(null); | ||||
|       setIsUploadMediaOpen(false); | ||||
|     }; | ||||
|  | ||||
|     const handleChange = ( | ||||
| @@ -225,208 +230,87 @@ export const InformationTab = observer( | ||||
|                     flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up | ||||
|                   }} | ||||
|                 > | ||||
|                   <Paper | ||||
|                     elevation={2} | ||||
|                     sx={{ | ||||
|                       padding: 2, | ||||
|                       display: "flex", | ||||
|                       flexDirection: "column", | ||||
|                       alignItems: "center", | ||||
|                       gap: 1, | ||||
|                       flex: 1, | ||||
|                       minWidth: 150, // Ensure a minimum width | ||||
|                   <ImageUploadCard | ||||
|                     title="Логотип" | ||||
|                     imageKey="thumbnail" | ||||
|                     imageUrl={sight.common.thumbnail} | ||||
|                     onImageClick={() => { | ||||
|                       setIsPreviewMediaOpen(true); | ||||
|                       setMediaId(sight.common.thumbnail ?? ""); | ||||
|                     }} | ||||
|                   > | ||||
|                     <Box sx={{ display: "flex", alignItems: "center" }}> | ||||
|                       <Typography | ||||
|                         variant="subtitle2" | ||||
|                         gutterBottom | ||||
|                         sx={{ mb: 0, mr: 0.5 }} | ||||
|                       > | ||||
|                         Логотип | ||||
|                       </Typography> | ||||
|                     </Box> | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         position: "relative", | ||||
|                         width: "200px", | ||||
|                         height: "200px", | ||||
|                         backgroundColor: "grey.200", | ||||
|                         display: "flex", | ||||
|                         alignItems: "center", | ||||
|                         justifyContent: "center", | ||||
|                         borderRadius: 1, | ||||
|                         mb: 1, | ||||
|                         cursor: sight.common.thumbnail ? "pointer" : "default", | ||||
|                         "&:hover": { | ||||
|                           backgroundColor: sight.common.thumbnail | ||||
|                             ? "red.300" | ||||
|                             : "grey.200", | ||||
|                     onDeleteImageClick={() => { | ||||
|                       handleChange( | ||||
|                         language as Language, | ||||
|                         { | ||||
|                           thumbnail: null, | ||||
|                         }, | ||||
|                       }} | ||||
|                       onClick={() => { | ||||
|                         setIsMediaModalOpen(true); | ||||
|                       }} | ||||
|                     > | ||||
|                       {sight.common.thumbnail ? ( | ||||
|                         <img | ||||
|                           src={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||
|                             sight.common.thumbnail | ||||
|                           }/download?token=${token}`} | ||||
|                           alt="Логотип" | ||||
|                           style={{ maxWidth: "100%", maxHeight: "100%" }} | ||||
|                           onClick={() => { | ||||
|                             setIsPreviewMediaOpen(true); | ||||
|                             setMediaId(sight.common.thumbnail ?? ""); | ||||
|                           }} | ||||
|                         /> | ||||
|                       ) : ( | ||||
|                         <ImagePlus size={24} color="grey" /> | ||||
|                       )} | ||||
|                     </Box> | ||||
|                   </Paper> | ||||
|                   <Paper | ||||
|                     elevation={2} | ||||
|                     sx={{ | ||||
|                       padding: 2, | ||||
|                       display: "flex", | ||||
|                       flexDirection: "column", | ||||
|                       alignItems: "center", | ||||
|                       gap: 1, | ||||
|                       flex: 1, | ||||
|                       minWidth: 150, // Ensure a minimum width | ||||
|                         true | ||||
|                       ); | ||||
|                       setActiveMenuType(null); | ||||
|                     }} | ||||
|                   > | ||||
|                     <Box sx={{ display: "flex", alignItems: "center" }}> | ||||
|                       <Typography | ||||
|                         variant="subtitle2" | ||||
|                         gutterBottom | ||||
|                         sx={{ mb: 0, mr: 0.5 }} | ||||
|                       > | ||||
|                         Водяной знак (л.в) | ||||
|                       </Typography> | ||||
|                       <Tooltip title={"asf"}> | ||||
|                         <Info | ||||
|                           size={16} | ||||
|                           color="gray" | ||||
|                           style={{ cursor: "pointer" }} | ||||
|                         /> | ||||
|                       </Tooltip> | ||||
|                     </Box> | ||||
|  | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         position: "relative", | ||||
|                         width: "200px", | ||||
|                         height: "200px", | ||||
|                         backgroundColor: "grey.200", | ||||
|                         display: "flex", | ||||
|                         alignItems: "center", | ||||
|                         justifyContent: "center", | ||||
|                         borderRadius: 1, | ||||
|                         mb: 1, | ||||
|                         cursor: sight.common.watermark_lu | ||||
|                           ? "pointer" | ||||
|                           : "default", | ||||
|                         "&:hover": { | ||||
|                           backgroundColor: sight.common.watermark_lu | ||||
|                             ? "grey.300" | ||||
|                             : "grey.200", | ||||
|                         }, | ||||
|                       }} | ||||
|                       onClick={() => { | ||||
|                         setIsPreviewMediaOpen(true); | ||||
|                         setMediaId(sight.common.watermark_lu ?? ""); | ||||
|                       }} | ||||
|                     > | ||||
|                       {sight.common.watermark_lu ? ( | ||||
|                         <img | ||||
|                           src={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||
|                             sight.common.watermark_lu | ||||
|                           }/download?token=${token}`} | ||||
|                           alt="Знак л.в" | ||||
|                           style={{ maxWidth: "100%", maxHeight: "100%" }} | ||||
|                           onClick={() => { | ||||
|                             setIsMediaModalOpen(true); | ||||
|                             setMediaId(sight.common.watermark_lu ?? ""); | ||||
|                           }} | ||||
|                         /> | ||||
|                       ) : ( | ||||
|                         <ImagePlus size={24} color="grey" /> | ||||
|                       )} | ||||
|                     </Box> | ||||
|                   </Paper> | ||||
|  | ||||
|                   <Paper | ||||
|                     elevation={2} | ||||
|                     sx={{ | ||||
|                       padding: 2, | ||||
|                       display: "flex", | ||||
|                       flexDirection: "column", | ||||
|                       alignItems: "center", | ||||
|                       gap: 1, | ||||
|                       flex: 1, | ||||
|                       minWidth: 150, // Ensure a minimum width | ||||
|                     onSelectFileClick={() => { | ||||
|                       setActiveMenuType("thumbnail"); | ||||
|                       setIsAddMediaOpen(true); | ||||
|                     }} | ||||
|                   > | ||||
|                     <Box sx={{ display: "flex", alignItems: "center" }}> | ||||
|                       <Typography | ||||
|                         variant="subtitle2" | ||||
|                         gutterBottom | ||||
|                         sx={{ mb: 0, mr: 0.5 }} | ||||
|                       > | ||||
|                         Водяной знак (п.в) | ||||
|                       </Typography> | ||||
|                       <Tooltip title={"asfaf"}> | ||||
|                         <Info | ||||
|                           size={16} | ||||
|                           color="gray" | ||||
|                           style={{ cursor: "pointer" }} | ||||
|                         /> | ||||
|                       </Tooltip> | ||||
|                     </Box> | ||||
|                     <Box | ||||
|                       sx={{ | ||||
|                         position: "relative", | ||||
|                         width: "200px", | ||||
|                         height: "200px", | ||||
|                         backgroundColor: "grey.200", | ||||
|                         display: "flex", | ||||
|                         alignItems: "center", | ||||
|                         justifyContent: "center", | ||||
|                         borderRadius: 1, | ||||
|                         mb: 1, | ||||
|                         cursor: sight.common.watermark_rd | ||||
|                           ? "pointer" | ||||
|                           : "default", | ||||
|                         "&:hover": { | ||||
|                           backgroundColor: sight.common.watermark_rd | ||||
|                             ? "grey.300" | ||||
|                             : "grey.200", | ||||
|                     setUploadMediaOpen={() => { | ||||
|                       setIsUploadMediaOpen(true); | ||||
|                       setActiveMenuType("thumbnail"); | ||||
|                     }} | ||||
|                   /> | ||||
|                   <ImageUploadCard | ||||
|                     title="Водяной знак (л.в)" | ||||
|                     imageKey="watermark_lu" | ||||
|                     imageUrl={sight.common.watermark_lu} | ||||
|                     onImageClick={() => { | ||||
|                       setIsPreviewMediaOpen(true); | ||||
|                       setMediaId(sight.common.watermark_lu ?? ""); | ||||
|                     }} | ||||
|                     onDeleteImageClick={() => { | ||||
|                       handleChange( | ||||
|                         language as Language, | ||||
|                         { | ||||
|                           watermark_lu: null, | ||||
|                         }, | ||||
|                       }} | ||||
|                       onClick={() => { | ||||
|                         setIsMediaModalOpen(true); | ||||
|                         setMediaId(sight.common.watermark_rd ?? ""); | ||||
|                       }} | ||||
|                     > | ||||
|                       {sight.common.watermark_rd ? ( | ||||
|                         <img | ||||
|                           src={`${import.meta.env.VITE_KRBL_MEDIA}${ | ||||
|                             sight.common.watermark_rd | ||||
|                           }/download?token=${token}`} | ||||
|                           alt="Знак п.в" | ||||
|                           style={{ maxWidth: "100%", maxHeight: "100%" }} | ||||
|                           onClick={() => { | ||||
|                             setIsPreviewMediaOpen(true); | ||||
|                             setMediaId(sight.common.watermark_rd ?? ""); | ||||
|                           }} | ||||
|                         /> | ||||
|                       ) : ( | ||||
|                         <ImagePlus size={24} color="grey" /> | ||||
|                       )} | ||||
|                     </Box> | ||||
|                   </Paper> | ||||
|                         true | ||||
|                       ); | ||||
|                       setActiveMenuType(null); | ||||
|                     }} | ||||
|                     onSelectFileClick={() => { | ||||
|                       setActiveMenuType("watermark_lu"); | ||||
|                       setIsAddMediaOpen(true); | ||||
|                     }} | ||||
|                     setUploadMediaOpen={() => { | ||||
|                       setIsUploadMediaOpen(true); | ||||
|                       setActiveMenuType("watermark_lu"); | ||||
|                     }} | ||||
|                   /> | ||||
|                   <ImageUploadCard | ||||
|                     title="Водяной знак (п.в)" | ||||
|                     imageKey="watermark_rd" | ||||
|                     imageUrl={sight.common.watermark_rd} | ||||
|                     onImageClick={() => { | ||||
|                       setIsPreviewMediaOpen(true); | ||||
|                       setMediaId(sight.common.watermark_rd ?? ""); | ||||
|                     }} | ||||
|                     onDeleteImageClick={() => { | ||||
|                       handleChange( | ||||
|                         language as Language, | ||||
|                         { | ||||
|                           watermark_rd: null, | ||||
|                         }, | ||||
|                         true | ||||
|                       ); | ||||
|                       setActiveMenuType(null); | ||||
|                     }} | ||||
|                     onSelectFileClick={() => { | ||||
|                       setActiveMenuType("watermark_rd"); | ||||
|                       setIsAddMediaOpen(true); | ||||
|                     }} | ||||
|                     setUploadMediaOpen={() => { | ||||
|                       setIsUploadMediaOpen(true); | ||||
|                       setActiveMenuType("watermark_rd"); | ||||
|                     }} | ||||
|                   /> | ||||
|                 </Box> | ||||
|               </Box> | ||||
|             </Box> | ||||
| @@ -489,6 +373,21 @@ export const InformationTab = observer( | ||||
|           onSelectMedia={handleMediaSelect} | ||||
|         /> | ||||
|  | ||||
|         <UploadMediaDialog | ||||
|           open={isUploadMediaOpen} | ||||
|           onClose={() => setIsUploadMediaOpen(false)} | ||||
|           afterUpload={(media) => { | ||||
|             handleChange( | ||||
|               language as Language, | ||||
|               { | ||||
|                 [activeMenuType ?? "thumbnail"]: media.id, | ||||
|               }, | ||||
|               true | ||||
|             ); | ||||
|             setActiveMenuType(null); | ||||
|             setIsUploadMediaOpen(false); | ||||
|           }} | ||||
|         /> | ||||
|         <PreviewMediaDialog | ||||
|           open={isPreviewMediaOpen} | ||||
|           onClose={() => setIsPreviewMediaOpen(false)} | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import { | ||||
| } from "@mui/material"; | ||||
| import { | ||||
|   BackButton, | ||||
|   createSightStore, | ||||
|   editSightStore, | ||||
|   languageStore, | ||||
|   SelectArticleModal, | ||||
| @@ -348,6 +347,7 @@ export const RightWidgetTab = observer( | ||||
|                             width: "100%", | ||||
|                             height: 200, | ||||
|                             flexShrink: 0, | ||||
|  | ||||
|                             backgroundColor: "rgba(0,0,0,0.1)", | ||||
|                             display: "flex", | ||||
|                             alignItems: "center", | ||||
| @@ -361,10 +361,11 @@ export const RightWidgetTab = observer( | ||||
|                       <Box | ||||
|                         sx={{ | ||||
|                           width: "100%", | ||||
|                           minHeight: "70px", | ||||
|                           height: "70px", | ||||
|                           background: "#877361", | ||||
|                           display: "flex", | ||||
|                           flexShrink: 0, | ||||
|  | ||||
|                           alignItems: "center", | ||||
|                           borderBottom: "1px solid rgba(255,255,255,0.1)", | ||||
|                           px: 2, | ||||
| @@ -380,6 +381,8 @@ export const RightWidgetTab = observer( | ||||
|                         sx={{ | ||||
|                           px: 2, | ||||
|                           flexGrow: 1, | ||||
|                           flex: 1, | ||||
|                           minHeight: "300px", | ||||
|                           overflowY: "auto", | ||||
|                           backgroundColor: "#877361", | ||||
|                           color: "white", | ||||
|   | ||||
| @@ -11,3 +11,4 @@ export * from "./MediaViewer"; | ||||
| export * from "./MediaArea"; | ||||
| export * from "./ModelViewer3D"; | ||||
| export * from "./MediaAreaForSight"; | ||||
| export * from "./ImageUploadCard"; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user