diff --git a/src/shared/store/CreateSightStore/index.tsx b/src/shared/store/CreateSightStore/index.tsx index a2f9f95..996daef 100644 --- a/src/shared/store/CreateSightStore/index.tsx +++ b/src/shared/store/CreateSightStore/index.tsx @@ -586,6 +586,12 @@ class CreateSightStore { throw error; } }; + + updateRightArticles = async (articles: any[], language: Language) => { + runInAction(() => { + this.sight[language].right = articles; + }); + }; } export const createSightStore = new CreateSightStore(); diff --git a/src/shared/store/EditSightStore/index.tsx b/src/shared/store/EditSightStore/index.tsx index 5f31932..6021663 100644 --- a/src/shared/store/EditSightStore/index.tsx +++ b/src/shared/store/EditSightStore/index.tsx @@ -121,35 +121,32 @@ class EditSightStore { let responseEn = await languageInstance("en").get(`/sight/${id}/article`); let responseZh = await languageInstance("zh").get(`/sight/${id}/article`); - // Function to fetch media for a given set of articles - const fetchMediaForArticles = async (articles: any[]) => { - const articlesWithMedia = []; - for (const article of articles) { - const responseMedia = await authInstance.get( - `/article/${article.id}/media` - ); - articlesWithMedia.push({ - ...article, - media: responseMedia.data, - }); - } - return articlesWithMedia; - }; + // Create a map of article IDs to their media + const mediaMap = new Map(); + for (const article of responseRu.data) { + const responseMedia = await authInstance.get( + `/article/${article.id}/media` + ); + mediaMap.set(article.id, responseMedia.data); + } - // Fetch media for articles in each language - const ruArticlesWithMedia = await fetchMediaForArticles(responseRu.data); - const enArticlesWithMedia = await fetchMediaForArticles(responseEn.data); - const zhArticlesWithMedia = await fetchMediaForArticles(responseZh.data); + // Function to add media to articles + const addMediaToArticles = (articles: any[]) => { + return articles.map((article) => ({ + ...article, + media: mediaMap.get(article.id), + })); + }; const data = { ru: { - right: ruArticlesWithMedia, + right: addMediaToArticles(responseRu.data), }, en: { - right: enArticlesWithMedia, + right: addMediaToArticles(responseEn.data), }, zh: { - right: zhArticlesWithMedia, + right: addMediaToArticles(responseZh.data), }, }; @@ -673,6 +670,16 @@ class EditSightStore { this.sight[language].right[index].heading = heading; this.sight[language].right[index].body = body; }; + + updateRightArticles = async (articles: any[], language: Language) => { + this.sight[language].right = articles; + const articleIdsInObject = articles.map((article) => ({ + id: article.id, + })); + await authInstance.post(`/sight/${this.sight.common.id}/article/order`, { + articles: articleIdsInObject, + }); + }; } export const editSightStore = new EditSightStore(); diff --git a/src/widgets/MediaViewer/index.tsx b/src/widgets/MediaViewer/index.tsx index da64bdb..d0a1430 100644 --- a/src/widgets/MediaViewer/index.tsx +++ b/src/widgets/MediaViewer/index.tsx @@ -33,7 +33,7 @@ export function MediaViewer({ alt={media?.filename} style={{ width: "100%", - height: "100%", + objectFit: "cover", }} /> @@ -45,9 +45,10 @@ export function MediaViewer({ media?.id }/download?token=${token}`} style={{ + width: "100%", height: "100%", - objectFit: "contain", - borderRadius: 30, + objectFit: "cover", + borderRadius: 8, }} controls autoPlay @@ -63,7 +64,7 @@ export function MediaViewer({ style={{ width: "100%", height: "100%", - objectFit: "contain", + objectFit: "cover", borderRadius: 8, }} /> @@ -77,7 +78,7 @@ export function MediaViewer({ style={{ width: "100%", height: "100%", - objectFit: "contain", + objectFit: "cover", borderRadius: 8, }} /> diff --git a/src/widgets/SightTabs/CreateRightTab/index.tsx b/src/widgets/SightTabs/CreateRightTab/index.tsx index 5fb90e0..5b07086 100644 --- a/src/widgets/SightTabs/CreateRightTab/index.tsx +++ b/src/widgets/SightTabs/CreateRightTab/index.tsx @@ -31,6 +31,7 @@ import { useState, useEffect } from "react"; // Added useEffect import { MediaViewer } from "../../MediaViewer/index"; import { toast } from "react-toastify"; import { authInstance } from "@shared"; +import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; type MediaItemShared = { // Define if not already available from @shared @@ -59,6 +60,7 @@ export const CreateRightTab = observer( linkExistingRightArticle, createSight, clearCreateSight, // For resetting form + updateRightArticles, } = createSightStore; const { language } = languageStore; @@ -210,6 +212,36 @@ export const CreateRightTab = observer( setMediaTarget(null); // Reset target }; + const handleDragEnd = (result: any) => { + const { source, destination } = result; + + // 1. Guard clause: If dropped outside any droppable area, do nothing. + if (!destination) return; + + // Extract source and destination indices + const sourceIndex = source.index; + const destinationIndex = destination.index; + + // 2. Guard clause: If dropped in the same position, do nothing. + if (sourceIndex === destinationIndex) return; + + // 3. Create a new array with reordered articles: + // - Create a shallow copy of the current articles array. + // This is important for immutability and triggering re-renders. + const newRightArticles = [...sight[language].right]; + + // - Remove the dragged article from its original position. + // `splice` returns an array of removed items, so we destructure the first (and only) one. + const [movedArticle] = newRightArticles.splice(sourceIndex, 1); + + // - Insert the moved article into its new position. + newRightArticles.splice(destinationIndex, 0, movedArticle); + + // 4. Update the store with the new order: + // This will typically trigger a re-render of the component with the updated list. + updateRightArticles(newRightArticles, language); + }; + return ( @@ -245,22 +277,56 @@ export const CreateRightTab = observer( > Предпросмотр медиа - - {sight[language].right.map((article, artIdx) => ( - handleDisplayArticleFromList(artIdx)} - > - - {article.heading || "Без названия"} - - - ))} + + + + {(provided) => ( + + {sight[language].right.length > 0 + ? sight[language].right.map( + (article, index) => ( + + {(provided, snapshot) => ( + { + handleDisplayArticleFromList( + index + ); + setType("article"); + }} + > + + + {article.heading} + + + + )} + + ) + ) + : null} + {provided.placeholder} + + )} + + + - + + {sight.preview_media && ( + <> + {type === "media" && ( + + {previewMedia && ( + <> + + + - - + + + + )} + {!previewMedia && ( + { + linkPreviewMedia(mediaId); }} + onFilesDrop={() => {}} /> - - + )} + )} - {!previewMedia && ( - { - linkPreviewMedia(mediaId); - }} - onFilesDrop={() => {}} - /> - )} - + + )} + {!previewMedia && ( + { + linkPreviewMedia(mediaId); + }} + onFilesDrop={() => {}} + /> )} ) : ( @@ -429,31 +507,52 @@ export const CreateRightTab = observer( {/* Right Column: Live Preview */} - {type === "article" && currentRightArticle && ( + {type === "article" && activeArticleIndex !== null && ( - {currentRightArticle.media && - currentRightArticle.media.length > 0 ? ( - + {sight[language].right[activeArticleIndex].media.length > + 0 ? ( + + + ) : ( )} + - - {currentRightArticle.heading || "Заголовок"} + + {sight[language].right[activeArticleIndex].heading || + "Выберите статью"} + - {currentRightArticle.body ? ( + {sight[language].right[activeArticleIndex].body ? ( ) : ( - Содержимое статьи... + Предпросмотр статьи появится здесь )} + + {sight[language].right.length > 0 && + sight[language].right.map((article, index) => ( + + ))} + )} diff --git a/src/widgets/SightTabs/RightWidgetTab/index.tsx b/src/widgets/SightTabs/RightWidgetTab/index.tsx index a813403..f5e8a17 100644 --- a/src/widgets/SightTabs/RightWidgetTab/index.tsx +++ b/src/widgets/SightTabs/RightWidgetTab/index.tsx @@ -8,6 +8,7 @@ import { TextField, } from "@mui/material"; import { + authInstance, BackButton, editSightStore, languageStore, @@ -17,6 +18,7 @@ import { UploadMediaDialog, } from "@shared"; import { + DeleteModal, LanguageSwitcher, MediaArea, MediaAreaForSight, @@ -28,6 +30,12 @@ import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; import { MediaViewer } from "../../MediaViewer/index"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from "@hello-pangea/dnd"; export const RightWidgetTab = observer( ({ value, index }: { value: number; index: number }) => { @@ -47,14 +55,21 @@ export const RightWidgetTab = observer( createLinkWithRightArticle, setFileToUpload, createNewRightArticle, + updateRightArticles, } = editSightStore; const [previewMedia, setPreviewMedia] = useState(null); useEffect(() => { - if (sight.common.preview_media) { - setPreviewMedia(sight.common.preview_media); - } + const fetchPreviewMedia = async () => { + if (sight.common.preview_media) { + const response = await authInstance.get( + `/media/${sight.common.preview_media}` + ); + setPreviewMedia(response.data); + } + }; + fetchPreviewMedia(); }, [sight.common.preview_media]); const handleUnlinkPreviewMedia = () => { @@ -80,8 +95,15 @@ export const RightWidgetTab = observer( ); const [isSelectModalOpen, setIsSelectModalOpen] = useState(false); const [isSelectMediaModalOpen, setIsSelectMediaModalOpen] = useState(false); + const [isDeleteArticleModalOpen, setIsDeleteArticleModalOpen] = + useState(false); const open = Boolean(anchorEl); + const handleDeleteArticle = () => { + deleteRightArticle(sight[language].right[activeArticleIndex || 0].id); + setActiveArticleIndex(null); + }; + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -133,6 +155,36 @@ export const RightWidgetTab = observer( console.log(sight[language].right); }, [sight[language].right]); + const handleDragEnd = (result: DropResult) => { + const { source, destination } = result; + + // 1. Guard clause: If dropped outside any droppable area, do nothing. + if (!destination) return; + + // Extract source and destination indices + const sourceIndex = source.index; + const destinationIndex = destination.index; + + // 2. Guard clause: If dropped in the same position, do nothing. + if (sourceIndex === destinationIndex) return; + + // 3. Create a new array with reordered articles: + // - Create a shallow copy of the current articles array. + // This is important for immutability and triggering re-renders. + const newRightArticles = [...sight[language].right]; + + // - Remove the dragged article from its original position. + // `splice` returns an array of removed items, so we destructure the first (and only) one. + const [movedArticle] = newRightArticles.splice(sourceIndex, 1); + + // - Insert the moved article into its new position. + newRightArticles.splice(destinationIndex, 0, movedArticle); + + // 4. Update the store with the new order: + // This will typically trigger a re-render of the component with the updated list. + updateRightArticles(newRightArticles, language); + }; + return ( @@ -153,26 +205,61 @@ export const RightWidgetTab = observer( - + setType("media")} - 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 bg-green-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300" > Предпросмотр медиа - {sight[language].right.length > 0 && - sight[language].right.map((article, index) => ( - { - handleSelectArticle(index); - setType("article"); - }} - > - {article.heading} - - ))} + + + + {(provided) => ( + + {sight[language].right.length > 0 + ? sight[language].right.map( + (article, index) => ( + + {(provided, snapshot) => ( + { + handleSelectArticle(index); + setType("article"); + }} + > + + + {article.heading} + + + + )} + + ) + ) + : null} + {provided.placeholder} + + )} + + + - + {type === "media" && ( + + {previewMedia && ( + <> + + + - - - - - )} - {!previewMedia && ( - { - linkPreviewMedia(mediaId); - }} - onFilesDrop={() => {}} - /> - )} - - )} - + + + + + )} + {!previewMedia && ( + { + linkPreviewMedia(mediaId); + }} + onFilesDrop={() => {}} + /> + )} + + )} )} @@ -369,14 +451,18 @@ export const RightWidgetTab = observer( {activeArticleIndex !== null && ( {sight[language].right[activeArticleIndex].media.length > 0 ? ( - + + + ) : ( @@ -428,14 +527,14 @@ export const RightWidgetTab = observer( {sight[language].right[activeArticleIndex].body ? ( @@ -453,6 +552,39 @@ export const RightWidgetTab = observer( )} + + {sight[language].right.length > 0 && + sight[language].right.map((article, index) => ( + + ))} + )} @@ -496,10 +628,20 @@ export const RightWidgetTab = observer( }} /> + setIsDeleteArticleModalOpen(false)} + onDelete={() => { + handleDeleteArticle(); + setIsDeleteArticleModalOpen(false); + }} + /> + article.id)} />