From a8777a974ad39019066463fd056b929c0e9685f6 Mon Sep 17 00:00:00 2001 From: itoshi Date: Sun, 1 Jun 2025 23:18:21 +0300 Subject: [PATCH] feat: Sight Page update --- package.json | 1 + src/app/router/index.tsx | 10 +- src/pages/CreateSightPage/index.tsx | 16 +- src/pages/EditSightPage/index.tsx | 14 +- .../modals/SelectArticleDialog/index.tsx | 76 ++- src/shared/modals/SelectMediaDialog/index.tsx | 47 +- src/shared/modals/UploadMediaDialog/index.tsx | 259 ++++++++ src/shared/modals/index.ts | 1 + src/shared/store/CityStore/index.tsx | 2 + src/shared/store/CreateSightStore/index.tsx | 449 ++++++++++++++ src/shared/store/EditSightStore/index.tsx | 330 +++++++++- src/shared/store/index.ts | 1 + src/widgets/MediaArea/index.tsx | 135 ++++ src/widgets/ModelViewer3D/index.tsx | 24 + src/widgets/ReactMarkdownEditor/index.tsx | 8 + .../CreateInformationTab/MediaUploadBox.tsx | 158 +++++ .../SightTabs/CreateInformationTab/index.tsx | 582 ++++++++++++++++++ src/widgets/SightTabs/CreateLeftTab/index.tsx | 451 ++++++++++++++ .../SightTabs/CreateRightTab/index.tsx | 374 +++++++++++ .../SightTabs/InformationTab/index.tsx | 142 ++--- src/widgets/SightTabs/LeftWidgetTab/index.tsx | 549 +++++++++-------- .../SightTabs/RightWidgetTab/index.tsx | 528 +++++++--------- src/widgets/SightTabs/index.ts | 3 + src/widgets/index.ts | 2 + tsconfig.tsbuildinfo | 2 +- yarn.lock | 23 +- 26 files changed, 3460 insertions(+), 727 deletions(-) create mode 100644 src/shared/modals/UploadMediaDialog/index.tsx create mode 100644 src/shared/store/CreateSightStore/index.tsx create mode 100644 src/widgets/MediaArea/index.tsx create mode 100644 src/widgets/ModelViewer3D/index.tsx create mode 100644 src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx create mode 100644 src/widgets/SightTabs/CreateInformationTab/index.tsx create mode 100644 src/widgets/SightTabs/CreateLeftTab/index.tsx create mode 100644 src/widgets/SightTabs/CreateRightTab/index.tsx diff --git a/package.json b/package.json index 54bdc7f..ae27d52 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "path": "^0.12.7", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-dropzone": "^14.3.8", "react-markdown": "^10.1.0", "react-photo-sphere-viewer": "^6.2.3", "react-router-dom": "^7.6.1", diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index 8eabdb9..8afce23 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -6,8 +6,9 @@ import { MainPage, SightPage, } from "@pages"; -import { authStore, editSightStore, sightsStore } from "@shared"; +import { authStore, createSightStore, editSightStore } from "@shared"; import { Layout } from "@widgets"; +import { runInAction } from "mobx"; import { useEffect } from "react"; import { Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom"; @@ -34,10 +35,15 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { export const Router = () => { const pathname = useLocation(); + useEffect(() => { editSightStore.clearSightInfo(); - sightsStore.clearCreateSight(); + createSightStore.clearCreateSight(); + runInAction(() => { + editSightStore.hasLoadedCommon = false; + }); }, [pathname]); + return ( { }; useEffect(() => { - getCities(); - getArticles(languageStore.language); + const fetchData = async () => { + await getCities(); + await getArticles(languageStore.language); + }; + fetchData(); }, []); return ( @@ -60,9 +62,9 @@ export const CreateSightPage = observer(() => {
- - - + + +
); diff --git a/src/pages/EditSightPage/index.tsx b/src/pages/EditSightPage/index.tsx index 73c104c..cd1f932 100644 --- a/src/pages/EditSightPage/index.tsx +++ b/src/pages/EditSightPage/index.tsx @@ -20,7 +20,7 @@ function a11yProps(index: number) { export const EditSightPage = observer(() => { const [value, setValue] = useState(0); - const { getSightInfo } = editSightStore; + const { sight, getSightInfo } = editSightStore; const { getArticles } = articlesStore; const { language } = languageStore; const { id } = useParams(); @@ -75,11 +75,13 @@ export const EditSightPage = observer(() => { -
- - - -
+ {sight.common.id !== 0 && ( +
+ + + +
+ )} ); }); diff --git a/src/shared/modals/SelectArticleDialog/index.tsx b/src/shared/modals/SelectArticleDialog/index.tsx index 8c0e831..0c709d6 100644 --- a/src/shared/modals/SelectArticleDialog/index.tsx +++ b/src/shared/modals/SelectArticleDialog/index.tsx @@ -1,4 +1,4 @@ -import { articlesStore } from "@shared"; +import { articlesStore, authInstance, languageStore } from "@shared"; import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; import { @@ -22,8 +22,13 @@ import { ReactMarkdownComponent } from "@widgets"; interface SelectArticleModalProps { open: boolean; onClose: () => void; - onSelectArticle: (articleId: string) => void; - linkedArticleIds?: string[]; // Add optional prop for linked articles + onSelectArticle: ( + articleId: number, + heading: string, + body: string, + media: { id: string; media_type: number; filename: string }[] + ) => void; + linkedArticleIds?: number[]; // Add optional prop for linked articles } export const SelectArticleModal = observer( @@ -35,7 +40,7 @@ export const SelectArticleModal = observer( }: SelectArticleModalProps) => { const { articles, getArticle, getArticleMedia } = articlesStore; const [searchQuery, setSearchQuery] = useState(""); - const [selectedArticleId, setSelectedArticleId] = useState( + const [selectedArticleId, setSelectedArticleId] = useState( null ); const [isLoading, setIsLoading] = useState(false); @@ -50,12 +55,21 @@ export const SelectArticleModal = observer( }, [open]); useEffect(() => { - const handleKeyPress = (event: KeyboardEvent) => { + const handleKeyPress = async (event: KeyboardEvent) => { if (event.key.toLowerCase() === "enter") { event.preventDefault(); if (selectedArticleId) { - onSelectArticle(selectedArticleId); + const media = await authInstance.get( + `/article/${selectedArticleId}/media` + ); + onSelectArticle( + selectedArticleId, + articlesStore.articleData?.heading || "", + articlesStore.articleData?.body || "", + media.data || [] + ); onClose(); + setSelectedArticleId(null); } } }; @@ -66,9 +80,7 @@ export const SelectArticleModal = observer( }; }, [selectedArticleId, onSelectArticle, onClose]); - const handleArticleClick = async (articleId: string) => { - if (selectedArticleId === articleId) return; - + const handleArticleClick = async (articleId: number) => { setSelectedArticleId(articleId); setIsLoading(true); @@ -86,14 +98,13 @@ export const SelectArticleModal = observer( setIsLoading(false); } }; - // @ts-ignore - const filteredArticles = articles - // @ts-ignore - .filter((article) => !linkedArticleIds.includes(article.id)) - // @ts-ignore - .filter((article) => - article.service_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + + const filteredArticles = articles[languageStore.language].filter( + (article) => !linkedArticleIds.includes(article.id) + ); + // .filter((article) => + // article.service_name.toLowerCase().includes(searchQuery.toLowerCase()) + // ); const token = localStorage.getItem("token"); return ( @@ -150,7 +161,17 @@ export const SelectArticleModal = observer( handleArticleClick(article.id)} - onDoubleClick={() => onSelectArticle(article.id)} + onDoubleClick={async () => { + const media = await authInstance.get( + `/article/${article.id}/media` + ); + onSelectArticle( + article.id, + article.heading, + article.body, + media.data + ); + }} selected={selectedArticleId === article.id} disabled={isLoading} sx={{ @@ -288,9 +309,22 @@ export const SelectArticleModal = observer( + + + + + + + + + + setError(null)} + > + setError(null)}> + {error} + + + + setSuccess(false)} + > + setSuccess(false)}> + Медиа успешно сохранено + + + + ); + } +); diff --git a/src/shared/modals/index.ts b/src/shared/modals/index.ts index 22df340..378cd4f 100644 --- a/src/shared/modals/index.ts +++ b/src/shared/modals/index.ts @@ -1,3 +1,4 @@ export * from "./SelectArticleDialog"; export * from "./SelectMediaDialog"; export * from "./PreviewMediaDialog"; +export * from "./UploadMediaDialog"; diff --git a/src/shared/store/CityStore/index.tsx b/src/shared/store/CityStore/index.tsx index 154d96d..4f87be0 100644 --- a/src/shared/store/CityStore/index.tsx +++ b/src/shared/store/CityStore/index.tsx @@ -17,6 +17,8 @@ class CityStore { } getCities = async () => { + if (this.cities.length !== 0) return; + const response = await authInstance.get("/city"); runInAction(() => { this.cities = response.data; diff --git a/src/shared/store/CreateSightStore/index.tsx b/src/shared/store/CreateSightStore/index.tsx new file mode 100644 index 0000000..227fcb4 --- /dev/null +++ b/src/shared/store/CreateSightStore/index.tsx @@ -0,0 +1,449 @@ +// @shared/stores/createSightStore.ts +import { + Language, + authInstance, + languageInstance, + articlesStore, + languageStore, + mediaStore, +} from "@shared"; +import { makeAutoObservable } from "mobx"; + +type SightLanguageInfo = { + name: string; + address: string; + left: { + heading: string; + body: string; + media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }[]; + }; + right: { heading: string; body: string }[]; +}; + +type SightCommonInfo = { + id: number; + city_id: number; + city: string; + latitude: number; + longitude: number; + thumbnail: string | null; + watermark_lu: string | null; + watermark_rd: string | null; + left_article: number; + preview_media: string | null; + video_preview: string | null; +}; + +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, + + ru: { + name: "", + address: "", + left: { heading: "", body: "", media: [] }, + right: [], + }, + en: { + name: "", + address: "", + left: { heading: "", body: "", media: [] }, + right: [], + }, + zh: { + name: "", + address: "", + left: { heading: "", body: "", media: [] }, + right: [], + }, + }; + + uploadMediaOpen = false; + setUploadMediaOpen = (open: boolean) => { + this.uploadMediaOpen = open; + }; + fileToUpload: File | null = null; + setFileToUpload = (file: File | null) => { + this.fileToUpload = file; + }; + + constructor() { + makeAutoObservable(this); + } + + createNewRightArticle = () => { + this.sight.ru.right.push({ + heading: "Введите русский заголовок", + body: "Введите русский текст", + }); + this.sight.en.right.push({ + heading: "Enter the English heading", + body: "Enter the English text", + }); + this.sight.zh.right.push({ + heading: "Введите китайский заголовок", + body: "Введите китайский текст", + }); + }; + + updateLeftInfo = (language: Language, heading: string, body: string) => { + this.sight[language].left.heading = heading; + this.sight[language].left.body = body; + }; + + clearCreateSight = () => { + this.sight = { + id: 0, + city_id: 0, + city: "", + latitude: 0, + longitude: 0, + thumbnail: null, + watermark_lu: null, + watermark_rd: null, + left_article: 0, + preview_media: null, + video_preview: null, + + ru: { + name: "", + address: "", + left: { heading: "", body: "", media: [] }, + right: [], + }, + + en: { + name: "", + address: "", + left: { heading: "", body: "", media: [] }, + right: [], + }, + + zh: { + name: "", + address: "", + left: { heading: "", body: "", media: [] }, + right: [], + }, + }; + }; + + updateSightInfo = ( + content: Partial, + language?: Language + ) => { + if (language) { + this.sight[language] = { + ...this.sight[language], + ...content, + }; + } else { + this.sight = { + ...this.sight, + ...content, + }; + } + }; + + unlinkLeftArticle = () => { + this.sight.left_article = 0; + this.sight.ru.left.heading = ""; + this.sight.en.left.heading = ""; + this.sight.zh.left.heading = ""; + this.sight.ru.left.body = ""; + this.sight.en.left.body = ""; + this.sight.zh.left.body = ""; + }; + + updateLeftArticle = async (articleId: number) => { + this.sight.left_article = articleId; + + if (articleId) { + const ruArticleData = await languageInstance("ru").get( + `/article/${articleId}` + ); + const enArticleData = await languageInstance("en").get( + `/article/${articleId}` + ); + const zhArticleData = await languageInstance("zh").get( + `/article/${articleId}` + ); + + this.sight.ru.left.heading = ruArticleData.data.heading; + this.sight.en.left.heading = enArticleData.data.heading; + this.sight.zh.left.heading = zhArticleData.data.heading; + + this.sight.ru.left.body = ruArticleData.data.body; + this.sight.en.left.body = enArticleData.data.body; + this.sight.zh.left.body = zhArticleData.data.body; + } else { + this.sight.left_article = 0; + this.sight.ru.left.heading = ""; + this.sight.en.left.heading = ""; + this.sight.zh.left.heading = ""; + this.sight.ru.left.body = ""; + this.sight.en.left.body = ""; + this.sight.zh.left.body = ""; + } + }; + + deleteLeftArticle = async (articleId: number) => { + await authInstance.delete(`/article/${articleId}`); + articlesStore.getArticles(languageStore.language); + this.sight.left_article = 0; + this.sight.ru.left.heading = ""; + this.sight.en.left.heading = ""; + this.sight.zh.left.heading = ""; + this.sight.ru.left.body = ""; + }; + + createLeftArticle = async () => { + const response = await languageInstance("ru").post("/article", { + heading: "Новая статья", + body: "Заполните статью контентом", + }); + + this.sight.left_article = response.data.id; + + this.sight.ru.left.heading = "Новая статья "; + this.sight.en.left.heading = ""; + this.sight.zh.left.heading = ""; + this.sight.ru.left.body = "Заполните статью контентом"; + this.sight.en.left.body = ""; + this.sight.zh.left.body = ""; + }; + + createSight = async (language: Language) => { + const rightArticles: number[] = []; + + if (this.sight.left_article !== 0) { + if (this.sight.left_article == 10000000) { + const response = await languageInstance("ru").post("/article", { + heading: this.sight.ru.left.heading, + body: this.sight.ru.left.body, + }); + const { id } = response.data; + await languageInstance("en").patch(`/article/${id}`, { + heading: this.sight.en.left.heading, + body: this.sight.en.left.body, + }); + + await languageInstance("zh").patch(`/article/${id}`, { + heading: this.sight.zh.left.heading, + body: this.sight.zh.left.body, + }); + this.sight.left_article = id; + } else { + await languageInstance("ru").patch( + `/article/${this.sight.left_article}`, + { + heading: this.sight.ru.left.heading, + body: this.sight.ru.left.body, + } + ); + + await languageInstance("en").patch( + `/article/${this.sight.left_article}`, + { + heading: this.sight.en.left.heading, + body: this.sight.en.left.body, + } + ); + + await languageInstance("zh").patch( + `/article/${this.sight.left_article}`, + { + heading: this.sight.zh.left.heading, + body: this.sight.zh.left.body, + } + ); + } + } + + this.sight[language].right.map(async (article, index) => { + try { + const response = await languageInstance(language).post("/article", { + heading: article.heading, + body: article.body, + }); + const { id } = response.data; + const anotherLanguages = ["en", "zh", "ru"].filter( + (lang) => lang !== language + ); + await languageInstance(anotherLanguages[0] as Language).patch( + `/article/${id}`, + { + heading: + this.sight[anotherLanguages[0] as Language].right[index].heading, + body: this.sight[anotherLanguages[0] as Language].right[index].body, + } + ); + await languageInstance(anotherLanguages[1] as Language).patch( + `/article/${id}`, + { + heading: + this.sight[anotherLanguages[1] as Language].right[index].heading, + body: this.sight[anotherLanguages[1] as Language].right[index].body, + } + ); + rightArticles.push(id); + } catch (error) { + 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, + }); + }); + console.log("created"); + }; + + updateRightArticleInfo = ( + index: number, + language: Language, + heading: string, + body: string + ) => { + this.sight[language].right[index].heading = heading; + this.sight[language].right[index].body = body; + }; + + uploadMedia = async ( + filename: string, + type: number, + file: File, + media_name?: string + ) => { + const formData = new FormData(); + formData.append("file", file); + formData.append("filename", filename); + 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(); + return { + id: response.data.id, + filename: filename, + media_name: media_name, + media_type: type, + }; + } catch (error) { + console.log(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, + }); + }; +} + +export const createSightStore = new CreateSightStore(); diff --git a/src/shared/store/EditSightStore/index.tsx b/src/shared/store/EditSightStore/index.tsx index 19b8c8b..37af665 100644 --- a/src/shared/store/EditSightStore/index.tsx +++ b/src/shared/store/EditSightStore/index.tsx @@ -1,26 +1,31 @@ // @shared/stores/editSightStore.ts -import { authInstance, Language } from "@shared"; +import { authInstance, Language, languageInstance, mediaStore } from "@shared"; import { makeAutoObservable, runInAction } from "mobx"; export type SightLanguageInfo = { id: number; name: string; address: string; - left: { heading: string; body: string }; + left: { + heading: string; + body: string; + media: { id: string; media_type: number; filename: string }[]; + }; right: { heading: string; body: string }[]; }; export type SightCommonInfo = { + id: number; city_id: number; city: string; latitude: number; longitude: number; - thumbnail: string; - watermark_lu: string; - watermark_rd: string; + thumbnail: string | null; + watermark_lu: string | null; + watermark_rd: string | null; left_article: number; - preview_media: string; - video_preview: string; + preview_media: string | null; + video_preview: string | null; }; export type SightBaseInfo = { @@ -31,36 +36,37 @@ export type SightBaseInfo = { class EditSightStore { sight: SightBaseInfo = { common: { + id: 0, city_id: 0, city: "", latitude: 0, longitude: 0, - thumbnail: "", - watermark_lu: "", - watermark_rd: "", + thumbnail: null, + watermark_lu: null, + watermark_rd: null, left_article: 0, - preview_media: "", - video_preview: "", + preview_media: null, + video_preview: null, }, ru: { id: 0, name: "", address: "", - left: { heading: "", body: "" }, + left: { heading: "", body: "", media: [] }, right: [], }, en: { id: 0, name: "", address: "", - left: { heading: "", body: "" }, + left: { heading: "", body: "", media: [] }, right: [], }, zh: { id: 0, name: "", address: "", - left: { heading: "", body: "" }, + left: { heading: "", body: "", media: [] }, right: [], }, }; @@ -77,6 +83,9 @@ class EditSightStore { const response = await authInstance.get(`/sight/${id}`); const data = response.data; + if (data.left_article != 0) { + await this.getLeftArticle(data.left_article); + } runInAction(() => { // Обновляем языковую часть @@ -101,25 +110,62 @@ class EditSightStore { this.sight[language].left.body = body; }; + getRightArticles = async (id: number) => { + const responseRu = await languageInstance("ru").get(`/sight/${id}/article`); + const responseEn = await languageInstance("en").get(`/sight/${id}/article`); + const responseZh = await languageInstance("zh").get(`/sight/${id}/article`); + + const data = { + ru: { + right: responseRu.data, + }, + en: { + right: responseEn.data, + }, + zh: { + right: responseZh.data, + }, + }; + runInAction(() => { + this.sight = { + ...this.sight, + ru: { + ...this.sight.ru, + right: data.ru.right, + }, + en: { + ...this.sight.en, + right: data.en.right, + }, + + zh: { + ...this.sight.zh, + right: data.zh.right, + }, + }; + }); + }; + clearSightInfo = () => { this.sight = { common: { + id: 0, city_id: 0, city: "", latitude: 0, longitude: 0, - thumbnail: "", - watermark_lu: "", - watermark_rd: "", + thumbnail: null, + watermark_lu: null, + watermark_rd: null, left_article: 0, - preview_media: "", - video_preview: "", + preview_media: null, + video_preview: null, }, ru: { id: 0, name: "", address: "", - left: { heading: "", body: "" }, + left: { heading: "", body: "", media: [] }, right: [], }, @@ -127,7 +173,7 @@ class EditSightStore { id: 0, name: "", address: "", - left: { heading: "", body: "" }, + left: { heading: "", body: "", media: [] }, right: [], }, @@ -135,7 +181,7 @@ class EditSightStore { id: 0, name: "", address: "", - left: { heading: "", body: "" }, + left: { heading: "", body: "", media: [] }, right: [], }, }; @@ -158,6 +204,244 @@ class EditSightStore { }; } }; + + unlinkLeftArticle = async () => { + this.sight.common.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 = ""; + }; + + updateSight = async () => { + let createdLeftArticleId = this.sight.common.left_article; + + if (this.sight.common.left_article == 10000000) { + const response = await languageInstance("ru").post(`/article`, { + heading: this.sight.ru.left.heading, + body: this.sight.ru.left.body, + }); + createdLeftArticleId = response.data.id; + await languageInstance("en").patch(`/article/${createdLeftArticleId}`, { + heading: this.sight.en.left.heading, + body: this.sight.en.left.body, + }); + + await languageInstance("zh").patch(`/article/${createdLeftArticleId}`, { + heading: this.sight.zh.left.heading, + body: this.sight.zh.left.body, + }); + + this.sight.common.left_article = createdLeftArticleId; + } else if (this.sight.common.left_article != 0) { + await languageInstance("ru").patch( + `/article/${this.sight.common.left_article}`, + { + heading: this.sight.ru.left.heading, + body: this.sight.ru.left.body, + } + ); + await languageInstance("en").patch( + `/article/${this.sight.common.left_article}`, + { + heading: this.sight.en.left.heading, + body: this.sight.en.left.body, + } + ); + + await languageInstance("zh").patch( + `/article/${this.sight.common.left_article}`, + { + heading: this.sight.zh.left.heading, + body: this.sight.zh.left.body, + } + ); + } + + await languageInstance("ru").patch(`/sight/${this.sight.common.id}`, { + ...this.sight.common, + name: this.sight.ru.name, + address: this.sight.ru.address, + left_article: createdLeftArticleId, + }); + await languageInstance("en").patch(`/sight/${this.sight.common.id}`, { + ...this.sight.common, + name: this.sight.en.name, + address: this.sight.en.address, + left_article: createdLeftArticleId, + }); + await languageInstance("zh").patch(`/sight/${this.sight.common.id}`, { + ...this.sight.common, + name: this.sight.zh.name, + address: this.sight.zh.address, + left_article: createdLeftArticleId, + }); + + if (this.sight.common.left_article == 0) { + return; + } + + // await languageInstance("ru").patch( + // `/sight/${this.sight.common.left_article}/article`, + // { + // heading: this.sight.ru.left.heading, + // body: this.sight.ru.left.body, + // } + // ); + // await languageInstance("en").patch( + // `/sight/${this.sight.common.left_article}/article`, + // { + // heading: this.sight.en.left.heading, + // body: this.sight.en.left.body, + // } + // ); + // await languageInstance("zh").patch( + // `/sight/${this.sight.common.left_article}/article`, + // { + // heading: this.sight.zh.left.heading, + // body: this.sight.zh.left.body, + // } + // ); + }; + + getLeftArticle = async (id: number) => { + const response = await languageInstance("ru").get(`/article/${id}`); + const responseEn = await languageInstance("en").get(`/article/${id}`); + const responseZh = await languageInstance("zh").get(`/article/${id}`); + const mediaIds = await authInstance.get(`/article/${id}/media`); + runInAction(() => { + this.sight.ru.left = { + heading: response.data.heading, + body: response.data.body, + media: mediaIds.data, + }; + this.sight.en.left = { + heading: responseEn.data.heading, + body: responseEn.data.body, + media: mediaIds.data, + }; + this.sight.zh.left = { + heading: responseZh.data.heading, + body: responseZh.data.body, + media: mediaIds.data, + }; + }); + }; + + deleteLeftArticle = async (id: number) => { + await authInstance.delete(`/article/${id}`); + this.sight.common.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 = ""; + }; + + createLeftArticle = async () => { + const response = await languageInstance("ru").post(`/article`, { + heading: "", + body: "", + }); + + this.sight.common.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 = ""; + }; + + deleteMedia = async (article_id: number, media_id: string) => { + await authInstance.delete(`/article/${article_id}/media`, { + data: { + media_id: media_id, + }, + }); + + this.sight.ru.left.media = this.sight.ru.left.media.filter( + (media) => media.id !== media_id + ); + this.sight.en.left.media = this.sight.en.left.media.filter( + (media) => media.id !== media_id + ); + this.sight.zh.left.media = this.sight.zh.left.media.filter( + (media) => media.id !== media_id + ); + }; + + uploadMediaOpen = false; + setUploadMediaOpen = (open: boolean) => { + this.uploadMediaOpen = open; + }; + fileToUpload: File | null = null; + setFileToUpload = (file: File | null) => { + this.fileToUpload = file; + }; + uploadMedia = async ( + filename: string, + type: number, + file: File, + media_name?: string + ) => { + const formData = new FormData(); + formData.append("file", file); + formData.append("filename", filename); + 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(); + return { + id: response.data.id, + filename: filename, + media_name: media_name, + media_type: type, + }; + } catch (error) { + console.log(error); + } + }; + + createLinkWithArticle = async (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + await authInstance.post( + `/article/${this.sight.common.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, + }); + }; } export const editSightStore = new EditSightStore(); diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index cb82e5c..efa6fe6 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -8,3 +8,4 @@ export * from "./CityStore"; export * from "./ArticlesStore"; export * from "./EditSightStore"; export * from "./MediaStore"; +export * from "./CreateSightStore"; diff --git a/src/widgets/MediaArea/index.tsx b/src/widgets/MediaArea/index.tsx new file mode 100644 index 0000000..4b66d6d --- /dev/null +++ b/src/widgets/MediaArea/index.tsx @@ -0,0 +1,135 @@ +import { Box, Button } from "@mui/material"; +import { MediaViewer } from "@widgets"; +import { PreviewMediaDialog } from "@shared"; +import { X, Upload } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useState, DragEvent, useRef } from "react"; + +export const MediaArea = observer( + ({ + articleId, + mediaIds, + deleteMedia, + onFilesDrop, // 👈 Проп для обработки загруженных файлов + setSelectMediaDialogOpen, + }: { + articleId: number; + mediaIds: { id: string; media_type: number; filename: string }[]; + deleteMedia: (id: number, media_id: string) => void; + onFilesDrop?: (files: File[]) => void; + setSelectMediaDialogOpen: (open: boolean) => void; + }) => { + const [mediaModal, setMediaModal] = useState(false); + const [mediaId, setMediaId] = useState(""); + const [isDragging, setIsDragging] = useState(false); + const fileInputRef = useRef(null); + + const handleMediaModal = (mediaId: string) => { + setMediaModal(true); + setMediaId(mediaId); + }; + + const handleDrop = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const files = Array.from(e.dataTransfer.files); + if (files.length && onFilesDrop) { + onFilesDrop(files); + } + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = () => { + setIsDragging(false); + }; + + const handleClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileSelect = (event: React.ChangeEvent) => { + const files = Array.from(event.target.files || []); + if (files.length && onFilesDrop) { + onFilesDrop(files); + } + // Сбрасываем значение input, чтобы можно было выбрать тот же файл снова + event.target.value = ""; + }; + + return ( + <> + + +
+
+ + Перетащите медиа файлы сюда или нажмите для выбора +
+
или
+ +
+ +
+ {mediaIds.map((m) => ( + + + ))} +
+
+ + setMediaModal(false)} + mediaId={mediaId} + /> + + ); + } +); diff --git a/src/widgets/ModelViewer3D/index.tsx b/src/widgets/ModelViewer3D/index.tsx new file mode 100644 index 0000000..5648efd --- /dev/null +++ b/src/widgets/ModelViewer3D/index.tsx @@ -0,0 +1,24 @@ +import { Stage, useGLTF } from "@react-three/drei"; +import { Canvas } from "@react-three/fiber"; +import { OrbitControls } from "@react-three/drei"; + +export const ModelViewer3D = ({ + fileUrl, + height = "100%", +}: { + fileUrl: string; + height: string; +}) => { + const { scene } = useGLTF(fileUrl); + + return ( + + + + + + + + + ); +}; diff --git a/src/widgets/ReactMarkdownEditor/index.tsx b/src/widgets/ReactMarkdownEditor/index.tsx index e4c498e..b695afa 100644 --- a/src/widgets/ReactMarkdownEditor/index.tsx +++ b/src/widgets/ReactMarkdownEditor/index.tsx @@ -24,6 +24,14 @@ const StyledMarkdownEditor = styled("div")(({ theme }) => ({ backgroundColor: theme.palette.background.paper, color: theme.palette.text.primary, borderColor: theme.palette.divider, + height: "auto", + minHeight: "200px", + maxHeight: "500px", + overflow: "auto", + }, + "& .CodeMirror-scroll": { + minHeight: "200px", + maxHeight: "500px", }, // Стили для текста в редакторе "& .CodeMirror-selected": { diff --git a/src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx b/src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx new file mode 100644 index 0000000..a148144 --- /dev/null +++ b/src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx @@ -0,0 +1,158 @@ +// import { Box, Button, Paper, Typography } from "@mui/material"; +// import { X, Upload } from "lucide-react"; +// import { useCallback, useState } from "react"; +// import { useDropzone } from "react-dropzone"; +// import { UploadMediaDialog } from "@shared"; +// import { createSightStore } from "@shared"; + +// interface MediaUploadBoxProps { +// title: string; +// tooltip?: string; +// mediaId: string | null; +// onMediaSelect: (mediaId: string) => void; +// onMediaRemove: () => void; +// onPreviewClick: (mediaId: string) => void; +// token: string; +// type: "thumbnail" | "watermark_lu" | "watermark_rd"; +// } + +// export const MediaUploadBox = ({ +// title, +// tooltip, +// mediaId, +// onMediaSelect, +// onMediaRemove, +// onPreviewClick, +// token, +// type, +// }: MediaUploadBoxProps) => { +// const [uploadMediaOpen, setUploadMediaOpen] = useState(false); +// const [fileToUpload, setFileToUpload] = useState(null); + +// const onDrop = useCallback((acceptedFiles: File[]) => { +// if (acceptedFiles.length > 0) { +// setFileToUpload(acceptedFiles[0]); +// setUploadMediaOpen(true); +// } +// }, []); + +// const { getRootProps, getInputProps, isDragActive } = useDropzone({ +// onDrop, +// accept: { +// "image/*": [".png", ".jpg", ".jpeg", ".gif"], +// }, +// multiple: false, +// }); + +// const handleUploadComplete = async (media: { +// id: string; +// filename: string; +// media_name?: string; +// media_type: number; +// }) => { +// onMediaSelect(media.id); +// }; + +// return ( +// <> +// +// +// +// {title} +// +// +// +// +// {mediaId && ( +// +// )} +// {mediaId ? ( +// {title} { +// e.stopPropagation(); +// onPreviewClick(mediaId); +// }} +// /> +// ) : ( +//
+//
+// +//

+// {isDragActive ? "Отпустите файл здесь" : "Перетащите файл"} +//

+//
+//

или

+// +//
+// )} +//
+//
+ +// { +// setUploadMediaOpen(false); +// setFileToUpload(null); +// }} +// afterUpload={handleUploadComplete} +// /> +// +// ); +// }; diff --git a/src/widgets/SightTabs/CreateInformationTab/index.tsx b/src/widgets/SightTabs/CreateInformationTab/index.tsx new file mode 100644 index 0000000..3288ef4 --- /dev/null +++ b/src/widgets/SightTabs/CreateInformationTab/index.tsx @@ -0,0 +1,582 @@ +import { + Button, + TextField, + Box, + Autocomplete, + Typography, + Paper, + Tooltip, + MenuItem, + Menu as MuiMenu, +} from "@mui/material"; +import { + BackButton, + TabPanel, + languageStore, + Language, + cityStore, + SelectMediaDialog, + PreviewMediaDialog, + SightLanguageInfo, + SightCommonInfo, + createSightStore, +} from "@shared"; +import { LanguageSwitcher } from "@widgets"; +import { Info, X } from "lucide-react"; + +import { observer } from "mobx-react-lite"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; + +// Мокап для всплывающей подсказки + +export const CreateInformationTab = observer( + ({ value, index }: { value: number; index: number }) => { + const { cities } = cityStore; + const [, setIsMediaModalOpen] = useState(false); + const [mediaId, setMediaId] = useState(""); + const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); + + const { language } = languageStore; + const { sight, updateSightInfo, createSight } = createSightStore; + + const data = sight[language]; + + const [, setCity] = useState(sight.city_id ?? 0); + const [coordinates, setCoordinates] = useState(`0 0`); + + const token = localStorage.getItem("token"); + + // Menu state for each media button + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const [activeMenuType, setActiveMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | null + >(null); + const [isAddMediaOpen, setIsAddMediaOpen] = useState(false); + + // const handleMenuOpen = ( + // event: React.MouseEvent, + // type: "thumbnail" | "watermark_lu" | "watermark_rd" + // ) => { + // setMenuAnchorEl(event.currentTarget); + // setActiveMenuType(type); + // }; + + useEffect(() => { + // Показывать только при инициализации (не менять при ошибках пользователя) + if (sight.latitude !== 0 || sight.longitude !== 0) { + setCoordinates(`${sight.latitude} ${sight.longitude}`); + } + // если координаты обнулились — оставить поле как есть + }, [sight.latitude, sight.longitude]); + + const handleMenuClose = () => { + setMenuAnchorEl(null); + setActiveMenuType(null); + }; + + const handleCreateNew = () => { + handleMenuClose(); + }; + + const handleAddMedia = () => { + setIsAddMediaOpen(true); + handleMenuClose(); + }; + + const handleChange = ( + content: Partial, + language?: Language + ) => { + if (language) { + updateSightInfo(content, language); + } else { + updateSightInfo(content); + } + }; + + const handleMediaSelect = ( + media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }, + type: "thumbnail" | "watermark_lu" | "watermark_rd" + ) => { + handleChange({ + [type]: media.id, + }); + setActiveMenuType(null); + }; + return ( + <> + + + + + + {/* Left column with main fields */} + + { + handleChange( + { + name: e.target.value, + }, + language + ); + }} + fullWidth + variant="outlined" + /> + + { + handleChange( + { + address: e.target.value, + }, + language + ); + }} + fullWidth + variant="outlined" + /> + + city.id === sight.city_id) ?? null + } + getOptionLabel={(option) => option.name} + onChange={(_, value) => { + setCity(value?.id ?? 0); + handleChange({ + city_id: value?.id ?? 0, + }); + }} + renderInput={(params) => ( + + )} + /> + + { + const input = e.target.value; + setCoordinates(input); // показываем как есть + + const [latStr, lonStr] = input.split(/\s+/); // учитываем любые пробелы + + const lat = parseFloat(latStr); + const lon = parseFloat(lonStr); + + // Проверка, что обе координаты валидные числа + const isValidLat = !isNaN(lat); + const isValidLon = !isNaN(lon); + + if (isValidLat && isValidLon) { + handleChange({ + latitude: lat, + longitude: lon, + }); + } else { + handleChange( + { + latitude: 0, + longitude: 0, + }, + language + ); + } + }} + fullWidth + variant="outlined" + placeholder="Введите координаты в формате: широта долгота" + /> + + + + + + + + Логотип + + + { + setIsMediaModalOpen(true); + }} + > + {sight.thumbnail && ( + + )} + {sight.thumbnail ? ( + Логотип { + setIsPreviewMediaOpen(true); + setMediaId(sight.thumbnail ?? ""); + }} + /> + ) : ( +
+
+

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

+
+

или

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

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

+
+

или

+ +
+ )} +
+
+ + + + + Водяной знак (п.в) + + + + + + { + setIsMediaModalOpen(true); + }} + > + {sight.watermark_rd && ( + + )} + {sight.watermark_rd ? ( + Логотип { + setIsPreviewMediaOpen(true); + setMediaId(sight.watermark_rd ?? ""); + }} + /> + ) : ( +
+
+

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

+
+

или

+ +
+ )} +
+
+
+
+
+ + {/* LanguageSwitcher positioned at the top right */} + + + + {/* Save Button fixed at the bottom right */} + + + +
+
+ + {/* Media Menu */} + + Создать новую + Выбрать существующую + + + { + setIsAddMediaOpen(false); + setActiveMenuType(null); + }} + onSelectMedia={(media) => { + handleMediaSelect(media, activeMenuType ?? "thumbnail"); + }} + /> + + setIsPreviewMediaOpen(false)} + mediaId={mediaId} + /> + + ); + } +); diff --git a/src/widgets/SightTabs/CreateLeftTab/index.tsx b/src/widgets/SightTabs/CreateLeftTab/index.tsx new file mode 100644 index 0000000..c180068 --- /dev/null +++ b/src/widgets/SightTabs/CreateLeftTab/index.tsx @@ -0,0 +1,451 @@ +// @widgets/LeftWidgetTab.tsx +import { Box, Button, TextField, Paper, Typography } from "@mui/material"; +import { + BackButton, + TabPanel, + languageStore, + SelectMediaDialog, + editSightStore, + createSightStore, + SelectArticleModal, + UploadMediaDialog, +} from "@shared"; +import { + LanguageSwitcher, + MediaArea, + ReactMarkdownComponent, + ReactMarkdownEditor, + MediaViewer, +} from "@widgets"; +import { Trash2, ImagePlus } from "lucide-react"; +import { useState, useCallback } from "react"; +import { observer } from "mobx-react-lite"; +import { toast } from "react-toastify"; + +export const CreateLeftTab = observer( + ({ value, index }: { value: number; index: number }) => { + const { + sight, + updateSightInfo, + updateLeftArticle, + createSight, + deleteLeftArticle, + createLeftArticle, + unlinkLeftArticle, + createLinkWithArticle, + } = createSightStore; + const { + deleteMedia, + setFileToUpload, + uploadMediaOpen, + setUploadMediaOpen, + } = editSightStore; + + const { language } = languageStore; + + const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] = + useState(false); + const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] = + useState(false); + + // const handleMediaSelected = useCallback(() => { + // // При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА + // // сохраняя текущие heading и body. + // updateSightInfo(language, { + // left: { + // heading: data.left.heading, + // body: data.left.body, + // }, + // }); + // setIsSelectMediaDialogOpen(false); + // }, [language, data.left.heading, data.left.body]); + + const handleCloseArticleDialog = useCallback(() => { + setIsSelectArticleDialogOpen(false); + }, []); + + const handleCloseMediaDialog = useCallback(() => { + setIsSelectMediaDialogOpen(false); + }, []); + + const handleMediaSelected = useCallback( + async (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + await createLinkWithArticle(media); + setIsSelectMediaDialogOpen(false); + }, + [createLinkWithArticle] + ); + + const handleArticleSelect = useCallback( + (articleId: number) => { + updateLeftArticle(articleId); + }, + [updateLeftArticle] + ); + + return ( + + + + + + Левая статья + + {sight.left_article ? ( + <> + + + + ) : ( + <> + + + + )} + + + {sight.left_article > 0 && ( + <> + + {/* Левая колонка: Редактирование */} + + + + updateSightInfo( + { + left: { + heading: e.target.value, + body: sight[language].left.body, + media: sight[language].left.media, + }, + }, + language + ) + } + variant="outlined" + fullWidth + /> + + + updateSightInfo( + { + left: { + heading: sight[language].left.heading, + body: value, + media: sight[language].left.media, + }, + }, + language + ) + } + /> + + { + setFileToUpload(files[0]); + setUploadMediaOpen(true); + }} + /> + + {/* Блок МЕДИА для статьи */} + {/* + + МЕДИА + + {data.left.media ? ( + + Selected media + + ) : ( + + Нет медиа + + )} + + {data.left.media && ( + + )} + */} + + + {/* Правая колонка: Предпросмотр */} + + + {/* {data.left.media?.filename ? ( + + Превью медиа + + ) : ( + + )} */} + + + {sight[language].left.media.length > 0 ? ( + + ) : ( + + )} + + + {/* Заголовок в превью */} + + + {sight[language].left.heading || "Название информации"} + + + + {/* Текст статьи в превью */} + + + + + + + + + + + + )} + + + {/* */} + + setUploadMediaOpen(false)} + afterUpload={async (media) => { + setUploadMediaOpen(false); + setFileToUpload(null); + await createLinkWithArticle(media); + }} + /> + + + ); + } +); diff --git a/src/widgets/SightTabs/CreateRightTab/index.tsx b/src/widgets/SightTabs/CreateRightTab/index.tsx new file mode 100644 index 0000000..97b4723 --- /dev/null +++ b/src/widgets/SightTabs/CreateRightTab/index.tsx @@ -0,0 +1,374 @@ +import { + Box, + Button, + Paper, + Typography, + Menu, + MenuItem, + TextField, +} from "@mui/material"; +import { BackButton, createSightStore, languageStore, TabPanel } from "@shared"; +import { + LanguageSwitcher, + ReactMarkdownComponent, + ReactMarkdownEditor, +} from "@widgets"; +import { ImagePlus, Plus } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useState } from "react"; + +// --- RightWidgetTab (Parent) Component --- +export const CreateRightTab = observer( + ({ value, index }: { value: number; index: number }) => { + const [anchorEl, setAnchorEl] = useState(null); + const { sight, createNewRightArticle, updateRightArticleInfo } = + createSightStore; + const { language } = languageStore; + + const [activeArticleIndex, setActiveArticleIndex] = useState( + null + ); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const handleSave = () => { + console.log("Saving right widget..."); + }; + + const handleSelectArticle = (index: number) => { + setActiveArticleIndex(index); + }; + + return ( + + + + + + + + + + + { + // setMediaType("preview"); + }} + className="w-full bg-gray-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300" + > + Предпросмотр медиа + + + {sight[language].right.map((article, index) => ( + { + handleSelectArticle(index); + }} + > + {article.heading} + + ))} + + + + { + createNewRightArticle(); + handleClose(); + }} + > + Создать новую + + { + handleClose(); + }} + > + Выбрать существующую статью + + + + + {activeArticleIndex !== null && ( + <> + + + + + + + {/* Левая колонка: Редактирование */} + + + + updateRightArticleInfo( + activeArticleIndex, + language, + e.target.value, + sight[language].right[activeArticleIndex].body + ) + } + variant="outlined" + fullWidth + /> + + + updateRightArticleInfo( + activeArticleIndex, + language, + sight[language].right[activeArticleIndex] + .heading, + value + ) + } + /> + + {/* Блок МЕДИА для статьи */} + {/* + + МЕДИА + + {data.left.media ? ( + + Selected media + + ) : ( + + Нет медиа + + )} + + {data.left.media && ( + + )} + */} + + + )} + + + + + {activeArticleIndex !== null && ( + + + {false ? ( + + Загрузка... + + ) : ( + <> + + + + + + + {sight[language].right[activeArticleIndex] + .heading || "Выберите статью"} + + + + + {sight[language].right[activeArticleIndex].body ? ( + + ) : ( + + Предпросмотр статьи появится здесь + + )} + + + )} + + + )} + + + + + + + + {/* + */} + + ); + } +); diff --git a/src/widgets/SightTabs/InformationTab/index.tsx b/src/widgets/SightTabs/InformationTab/index.tsx index 3b771a5..cdcfcfb 100644 --- a/src/widgets/SightTabs/InformationTab/index.tsx +++ b/src/widgets/SightTabs/InformationTab/index.tsx @@ -27,6 +27,8 @@ import { Info, ImagePlus } from "lucide-react"; import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; + // Мокап для всплывающей подсказки export const InformationTab = observer( @@ -37,12 +39,10 @@ export const InformationTab = observer( const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); const { language } = languageStore; - const { sight, updateSightInfo } = editSightStore; - const data = sight[language]; - const common = sight.common; + const { sight, updateSightInfo, updateSight } = editSightStore; - const [, setCity] = useState(common.city_id ?? 0); + const [, setCity] = useState(sight.common.city_id ?? 0); const [coordinates, setCoordinates] = useState(`0 0`); const token = localStorage.getItem("token"); @@ -54,21 +54,13 @@ export const InformationTab = observer( >(null); const [isAddMediaOpen, setIsAddMediaOpen] = useState(false); - const handleMenuOpen = ( - event: React.MouseEvent, - type: "thumbnail" | "watermark_lu" | "watermark_rd" - ) => { - setMenuAnchorEl(event.currentTarget); - setActiveMenuType(type); - }; - useEffect(() => { // Показывать только при инициализации (не менять при ошибках пользователя) - if (common.latitude !== 0 || common.longitude !== 0) { - setCoordinates(`${common.latitude} ${common.longitude}`); + if (sight.common.latitude !== 0 || sight.common.longitude !== 0) { + setCoordinates(`${sight.common.latitude} ${sight.common.longitude}`); } // если координаты обнулились — оставить поле как есть - }, [common.latitude, common.longitude]); + }, [sight.common.latitude, sight.common.longitude]); const handleMenuClose = () => { setMenuAnchorEl(null); @@ -135,7 +127,7 @@ export const InformationTab = observer( > { handleChange(language as Language, { name: e.target.value, @@ -147,7 +139,7 @@ export const InformationTab = observer( { handleChange(language as Language, { address: e.target.value, @@ -160,18 +152,15 @@ export const InformationTab = observer( city.id === common.city_id) ?? null + cities.find((city) => city.id === sight.common.city_id) ?? + null } getOptionLabel={(option) => option.name} onChange={(_, value) => { setCity(value?.id ?? 0); - handleChange( - language as Language, - { - city_id: value?.id ?? 0, - }, - true - ); + handleChange(language as Language, { + city_id: value?.id ?? 0, + }); }} renderInput={(params) => ( @@ -195,15 +184,23 @@ export const InformationTab = observer( const isValidLon = !isNaN(lon); if (isValidLat && isValidLon) { - handleChange(language as Language, { - latitude: lat, - longitude: lon, - }); + handleChange( + language as Language, + { + latitude: lat, + longitude: lon, + }, + true + ); } else { - handleChange(language as Language, { - latitude: 0, - longitude: 0, - }); + handleChange( + language as Language, + { + latitude: 0, + longitude: 0, + }, + true + ); } }} fullWidth @@ -251,17 +248,18 @@ export const InformationTab = observer( - {common.thumbnail ? ( + {sight.common.thumbnail ? ( Логотип { setIsPreviewMediaOpen(true); - setMediaId(common.thumbnail); + setMediaId(sight.common.thumbnail ?? ""); }} /> ) : ( )} - { setIsPreviewMediaOpen(true); - setMediaId(common.watermark_lu); + setMediaId(sight.common.watermark_lu ?? ""); }} > - {common.watermark_lu ? ( + {sight.common.watermark_lu ? ( Знак л.в { setIsMediaModalOpen(true); - setMediaId(common.watermark_lu); + setMediaId(sight.common.watermark_lu ?? ""); }} /> ) : ( )} - { setIsMediaModalOpen(true); - setMediaId(common.watermark_rd); + setMediaId(sight.common.watermark_rd ?? ""); }} > - {common.watermark_rd ? ( + {sight.common.watermark_rd ? ( Знак п.в { setIsPreviewMediaOpen(true); - setMediaId(common.watermark_rd); + setMediaId(sight.common.watermark_rd ?? ""); }} /> ) : ( )} - @@ -467,8 +450,9 @@ export const InformationTab = observer( - )} - - - + - - {/* Левая колонка: Редактирование */} - - - updateSightInfo( - languageStore.language, - { - left: { - heading: e.target.value, - body: data.left.body, - }, - }, - false - ) - } - variant="outlined" - fullWidth - /> - - - updateSightInfo( - languageStore.language, - { - left: { - heading: data.left.heading, - body: value, - }, - }, - false - ) - } - /> - - {/* Блок МЕДИА для статьи */} - {/* - - МЕДИА - - {data.left.media ? ( - - Selected media - - ) : ( - - Нет медиа - - )} - - {data.left.media && ( - - )} - */} - - - {/* Правая колонка: Предпросмотр */} - - - {/* {data.left.media?.filename ? ( - Левая статья + + {sight.common.left_article ? ( + <> + + + + ) : ( + <> + + + + )} + + + + {sight.common.left_article > 0 && ( + + + + updateSightInfo(languageStore.language, { + left: { + heading: e.target.value, + body: sight[languageStore.language].left.body, + media: data.left.media, + }, + }) + } + variant="outlined" + fullWidth + /> + + + updateSightInfo(languageStore.language, { + left: { + heading: sight[languageStore.language].left.heading, + body: value, + media: data.left.media, + }, + }) + } + /> + + { + setFileToUpload(files[0]); + setUploadMediaOpen(true); + }} + /> + + + + - Превью медиа - - ) : ( - - )} */} + > + {data.left.media.length > 0 ? ( + + ) : ( + + )} + - - - + + + {data?.left?.heading || "Название информации"} + + - {/* Заголовок в превью */} - - - {data?.left?.heading || "Название информации"} - + {data?.left?.body && ( + + + + )} + + + )} - {/* Текст статьи в превью */} - - - - + + - - - - - - + + setUploadMediaOpen(false)} + afterUpload={async (media) => { + setUploadMediaOpen(false); + setFileToUpload(null); + await createLinkWithArticle(media); + }} + /> - + + ); } ); diff --git a/src/widgets/SightTabs/RightWidgetTab/index.tsx b/src/widgets/SightTabs/RightWidgetTab/index.tsx index c04442c..b8ab094 100644 --- a/src/widgets/SightTabs/RightWidgetTab/index.tsx +++ b/src/widgets/SightTabs/RightWidgetTab/index.tsx @@ -1,345 +1,288 @@ import { Box, Button, - List, - ListItemButton, - ListItemText, Paper, Typography, Menu, MenuItem, + TextField, } from "@mui/material"; import { - articlesStore, BackButton, + createSightStore, + editSightStore, + languageStore, SelectArticleModal, TabPanel, } from "@shared"; -import { SightEdit } from "@widgets"; -import { Plus } from "lucide-react"; +import { + LanguageSwitcher, + ReactMarkdownComponent, + ReactMarkdownEditor, +} from "@widgets"; +import { ImagePlus, Plus } from "lucide-react"; import { observer } from "mobx-react-lite"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; -// --- Mock Data (can be moved to a separate file or fetched from an API) --- -const mockRightWidgetBlocks = [ - { id: "preview_media", name: "Превью-медиа", type: "special" }, - { id: "article_1", name: "1. История", type: "article" }, - { id: "article_2", name: "2. Факты", type: "article" }, - { - id: "article_3", - name: "3. Блокада (Пример длинного названия)", - type: "article", - }, -]; - -const mockSelectedBlockData = { - id: "article_1", - heading: "История основания Санкт-Петербурга", - body: "## Начало\nГород был основан 27 мая 1703 года Петром I...", - media: [], -}; - -// --- ArticleListSidebar Component --- -interface ArticleBlock { - id: string; - name: string; - type: string; - linkedArticleId?: string; // Added for linked articles -} - -interface ArticleListSidebarProps { - blocks: ArticleBlock[]; - selectedBlockId: string | null; - onSelectBlock: (blockId: string) => void; - onCreateNew: () => void; - onSelectExisting: () => void; -} - -const ArticleListSidebar = ({ - blocks, - selectedBlockId, - onSelectBlock, - onCreateNew, - onSelectExisting, -}: ArticleListSidebarProps) => { - const [menuAnchorEl, setMenuAnchorEl] = useState(null); - - const handleMenuOpen = (event: React.MouseEvent) => { - setMenuAnchorEl(event.currentTarget); - }; - - const handleMenuClose = () => { - setMenuAnchorEl(null); - }; - - return ( - - - {blocks.map((block) => ( - onSelectBlock(block.id)} - sx={{ - borderRadius: 1, - mb: 0.5, - backgroundColor: - selectedBlockId === block.id ? "primary.light" : "transparent", - "&.Mui-selected": { - backgroundColor: "primary.main", - color: "primary.contrastText", - "&:hover": { - backgroundColor: "primary.dark", - }, - }, - "&:hover": { - backgroundColor: - selectedBlockId !== block.id ? "action.hover" : undefined, - }, - }} - > - - - ))} - - - - - Создать новую - Выбрать существующую - - - ); -}; - -// --- ArticleEditorPane Component --- -interface ArticleData { - id: string; - heading: string; - body: string; - media: any[]; // Define a proper type for media if available -} - -interface ArticleEditorPaneProps { - articleData: ArticleData | null; -} - -const ArticleEditorPane = ({ articleData }: ArticleEditorPaneProps) => { - if (!articleData) { - return ( - - - Выберите блок для редактирования - - - ); - } - - return ( - - - - - МЕДИА - - - Нет медиа - - - - - ); -}; - -// --- RightWidgetTab (Parent) Component --- export const RightWidgetTab = observer( ({ value, index }: { value: number; index: number }) => { - const [rightWidgetBlocks, setRightWidgetBlocks] = useState( - mockRightWidgetBlocks - ); - const [selectedBlockId, setSelectedBlockId] = useState( - mockRightWidgetBlocks[1]?.id || null + const [anchorEl, setAnchorEl] = useState(null); + const { createNewRightArticle, updateRightArticleInfo } = createSightStore; + const { sight, getRightArticles, updateSight } = editSightStore; + const { language } = languageStore; + + useEffect(() => { + if (sight.common.id) { + getRightArticles(sight.common.id); + } + }, [sight.common.id]); + + const [activeArticleIndex, setActiveArticleIndex] = useState( + null ); const [isSelectModalOpen, setIsSelectModalOpen] = useState(false); - const handleSelectBlock = (blockId: string) => { - setSelectedBlockId(blockId); - console.log("Selected block:", blockId); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + const handleSelectArticle = (index: number) => { + setActiveArticleIndex(index); }; const handleCreateNew = () => { - const newBlockId = `article_${Date.now()}`; - setRightWidgetBlocks((prevBlocks) => [ - ...prevBlocks, - { - id: newBlockId, - name: `${ - prevBlocks.filter((b) => b.type === "article").length + 1 - }. Новый блок`, - type: "article", - }, - ]); - setSelectedBlockId(newBlockId); + createNewRightArticle(); + handleClose(); }; const handleSelectExisting = () => { setIsSelectModalOpen(true); + handleClose(); }; const handleCloseSelectModal = () => { setIsSelectModalOpen(false); }; - const handleSelectArticle = (articleId: string) => { - // @ts-ignore - const article = articlesStore.articles.find((a) => a.id === articleId); - if (article) { - const newBlockId = `article_linked_${article.id}_${Date.now()}`; - setRightWidgetBlocks((prevBlocks) => [ - ...prevBlocks, - { - id: newBlockId, - name: `${ - prevBlocks.filter((b) => b.type === "article").length + 1 - }. ${article.service_name}`, - type: "article", - linkedArticleId: article.id, - }, - ]); - setSelectedBlockId(newBlockId); - } + const handleArticleSelect = () => { + // TODO: Implement article selection logic handleCloseSelectModal(); }; - const handleSave = () => { - console.log("Saving right widget..."); - // Implement save logic here, e.g., send data to an API + const handleSave = async () => { + await updateSight(); + toast.success("Достопримечательность сохранена"); }; - // Determine the current block data to pass to the editor pane - const currentBlockToEdit = selectedBlockId - ? selectedBlockId === mockSelectedBlockData.id - ? mockSelectedBlockData - : { - id: selectedBlockId, - heading: - rightWidgetBlocks.find((b) => b.id === selectedBlockId)?.name || - "Заголовок...", - body: "Содержимое...", - media: [], - } - : null; - - // Get list of already linked article IDs - const linkedArticleIds = rightWidgetBlocks - .filter((block) => block.linkedArticleId) - .map((block) => block.linkedArticleId as string); - return ( + - + + + + + setMediaType("preview")} + className="w-full bg-gray-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300" + > + Предпросмотр медиа + - + {sight[language].right.map((article, index) => ( + handleSelectArticle(index)} + > + {article.heading} + + ))} + + + + + Создать новую + + + Выбрать существующую статью + + + + + + {activeArticleIndex !== null && ( + <> + + + + + + + + updateRightArticleInfo( + activeArticleIndex, + language, + e.target.value, + sight[language].right[activeArticleIndex].body + ) + } + variant="outlined" + fullWidth + /> + + + updateRightArticleInfo( + activeArticleIndex, + language, + sight[language].right[activeArticleIndex] + .heading, + value + ) + } + /> + {/* {}} + /> */} + + + + )} + + + + + + {activeArticleIndex !== null && ( + + + + + + + + + {sight[language].right[activeArticleIndex].heading || + "Выберите статью"} + + + + + {sight[language].right[activeArticleIndex].body ? ( + + ) : ( + + Предпросмотр статьи появится здесь + + )} + + + + )} + ); diff --git a/src/widgets/SightTabs/index.ts b/src/widgets/SightTabs/index.ts index 1866c7f..20dd419 100644 --- a/src/widgets/SightTabs/index.ts +++ b/src/widgets/SightTabs/index.ts @@ -1,3 +1,6 @@ export * from "./InformationTab"; export * from "./LeftWidgetTab"; export * from "./RightWidgetTab"; +export * from "./CreateInformationTab"; +export * from "./CreateLeftTab"; +export * from "./CreateRightTab"; diff --git a/src/widgets/index.ts b/src/widgets/index.ts index 5db03b3..4ca557b 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -8,3 +8,5 @@ export * from "./LanguageSwitcher"; export * from "./DevicesTable"; export * from "./SightsTable"; export * from "./MediaViewer"; +export * from "./MediaArea"; +export * from "./ModelViewer3D"; diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 1735a1a..089bd8d 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"errors":true,"version":"5.8.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/sightpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/citystore/index.tsx","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/devicestable/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 84388b2..d15ebca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1323,6 +1323,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +attr-accept@^2.2.4: + version "2.2.5" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e" + integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ== + axios@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.9.0.tgz#25534e3b72b54540077d33046f77e3b8d7081901" @@ -1908,6 +1913,13 @@ file-entry-cache@^8.0.0: dependencies: flat-cache "^4.0.0" +file-selector@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4" + integrity sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig== + dependencies: + tslib "^2.7.0" + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" @@ -3098,6 +3110,15 @@ react-dom@^19.1.0: dependencies: scheduler "^0.26.0" +react-dropzone@^14.3.8: + version "14.3.8" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.8.tgz#a7eab118f8a452fe3f8b162d64454e81ba830582" + integrity sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug== + dependencies: + attr-accept "^2.2.4" + file-selector "^2.1.0" + prop-types "^15.8.1" + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" @@ -3501,7 +3522,7 @@ ts-api-utils@^2.1.0: resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== -tslib@^2.4.0, tslib@^2.8.0: +tslib@^2.4.0, tslib@^2.7.0, tslib@^2.8.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==