feat: DND for Articles
This commit is contained in:
		| @@ -586,6 +586,12 @@ class CreateSightStore { | |||||||
|       throw error; |       throw error; | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   updateRightArticles = async (articles: any[], language: Language) => { | ||||||
|  |     runInAction(() => { | ||||||
|  |       this.sight[language].right = articles; | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const createSightStore = new CreateSightStore(); | export const createSightStore = new CreateSightStore(); | ||||||
|   | |||||||
| @@ -121,35 +121,32 @@ class EditSightStore { | |||||||
|     let responseEn = await languageInstance("en").get(`/sight/${id}/article`); |     let responseEn = await languageInstance("en").get(`/sight/${id}/article`); | ||||||
|     let responseZh = await languageInstance("zh").get(`/sight/${id}/article`); |     let responseZh = await languageInstance("zh").get(`/sight/${id}/article`); | ||||||
|  |  | ||||||
|     // Function to fetch media for a given set of articles |     // Create a map of article IDs to their media | ||||||
|     const fetchMediaForArticles = async (articles: any[]) => { |     const mediaMap = new Map(); | ||||||
|       const articlesWithMedia = []; |     for (const article of responseRu.data) { | ||||||
|       for (const article of articles) { |  | ||||||
|       const responseMedia = await authInstance.get( |       const responseMedia = await authInstance.get( | ||||||
|         `/article/${article.id}/media` |         `/article/${article.id}/media` | ||||||
|       ); |       ); | ||||||
|         articlesWithMedia.push({ |       mediaMap.set(article.id, responseMedia.data); | ||||||
|           ...article, |  | ||||||
|           media: responseMedia.data, |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|       return articlesWithMedia; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     // Fetch media for articles in each language |     // Function to add media to articles | ||||||
|     const ruArticlesWithMedia = await fetchMediaForArticles(responseRu.data); |     const addMediaToArticles = (articles: any[]) => { | ||||||
|     const enArticlesWithMedia = await fetchMediaForArticles(responseEn.data); |       return articles.map((article) => ({ | ||||||
|     const zhArticlesWithMedia = await fetchMediaForArticles(responseZh.data); |         ...article, | ||||||
|  |         media: mediaMap.get(article.id), | ||||||
|  |       })); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     const data = { |     const data = { | ||||||
|       ru: { |       ru: { | ||||||
|         right: ruArticlesWithMedia, |         right: addMediaToArticles(responseRu.data), | ||||||
|       }, |       }, | ||||||
|       en: { |       en: { | ||||||
|         right: enArticlesWithMedia, |         right: addMediaToArticles(responseEn.data), | ||||||
|       }, |       }, | ||||||
|       zh: { |       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].heading = heading; | ||||||
|     this.sight[language].right[index].body = body; |     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(); | export const editSightStore = new EditSightStore(); | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ export function MediaViewer({ | |||||||
|           alt={media?.filename} |           alt={media?.filename} | ||||||
|           style={{ |           style={{ | ||||||
|             width: "100%", |             width: "100%", | ||||||
|             height: "100%", |  | ||||||
|             objectFit: "cover", |             objectFit: "cover", | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
| @@ -45,9 +45,10 @@ export function MediaViewer({ | |||||||
|             media?.id |             media?.id | ||||||
|           }/download?token=${token}`} |           }/download?token=${token}`} | ||||||
|           style={{ |           style={{ | ||||||
|  |             width: "100%", | ||||||
|             height: "100%", |             height: "100%", | ||||||
|             objectFit: "contain", |             objectFit: "cover", | ||||||
|             borderRadius: 30, |             borderRadius: 8, | ||||||
|           }} |           }} | ||||||
|           controls |           controls | ||||||
|           autoPlay |           autoPlay | ||||||
| @@ -63,7 +64,7 @@ export function MediaViewer({ | |||||||
|           style={{ |           style={{ | ||||||
|             width: "100%", |             width: "100%", | ||||||
|             height: "100%", |             height: "100%", | ||||||
|             objectFit: "contain", |             objectFit: "cover", | ||||||
|             borderRadius: 8, |             borderRadius: 8, | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
| @@ -77,7 +78,7 @@ export function MediaViewer({ | |||||||
|           style={{ |           style={{ | ||||||
|             width: "100%", |             width: "100%", | ||||||
|             height: "100%", |             height: "100%", | ||||||
|             objectFit: "contain", |             objectFit: "cover", | ||||||
|             borderRadius: 8, |             borderRadius: 8, | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|   | |||||||
| @@ -31,6 +31,7 @@ import { useState, useEffect } from "react"; // Added useEffect | |||||||
| import { MediaViewer } from "../../MediaViewer/index"; | import { MediaViewer } from "../../MediaViewer/index"; | ||||||
| import { toast } from "react-toastify"; | import { toast } from "react-toastify"; | ||||||
| import { authInstance } from "@shared"; | import { authInstance } from "@shared"; | ||||||
|  | import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; | ||||||
|  |  | ||||||
| type MediaItemShared = { | type MediaItemShared = { | ||||||
|   // Define if not already available from @shared |   // Define if not already available from @shared | ||||||
| @@ -59,6 +60,7 @@ export const CreateRightTab = observer( | |||||||
|       linkExistingRightArticle, |       linkExistingRightArticle, | ||||||
|       createSight, |       createSight, | ||||||
|       clearCreateSight, // For resetting form |       clearCreateSight, // For resetting form | ||||||
|  |       updateRightArticles, | ||||||
|     } = createSightStore; |     } = createSightStore; | ||||||
|     const { language } = languageStore; |     const { language } = languageStore; | ||||||
|  |  | ||||||
| @@ -210,6 +212,36 @@ export const CreateRightTab = observer( | |||||||
|       setMediaTarget(null); // Reset target |       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 ( |     return ( | ||||||
|       <TabPanel value={value} index={index}> |       <TabPanel value={value} index={index}> | ||||||
|         <LanguageSwitcher /> |         <LanguageSwitcher /> | ||||||
| @@ -245,22 +277,56 @@ export const CreateRightTab = observer( | |||||||
|                     > |                     > | ||||||
|                       <Typography>Предпросмотр медиа</Typography> |                       <Typography>Предпросмотр медиа</Typography> | ||||||
|                     </Box> |                     </Box> | ||||||
|  |                     <Box> | ||||||
|                     {sight[language].right.map((article, artIdx) => ( |                       <DragDropContext onDragEnd={handleDragEnd}> | ||||||
|  |                         <Droppable droppableId="articles"> | ||||||
|  |                           {(provided) => ( | ||||||
|                             <Box |                             <Box | ||||||
|                         key={article.id || artIdx} // article.id should be preferred |                               ref={provided.innerRef} | ||||||
|                         className={`w-full p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300 ${ |                               {...provided.droppableProps} | ||||||
|                           type === "article" && activeArticleIndex === artIdx |                               className="flex flex-col gap-2" | ||||||
|                             ? "bg-blue-300 font-semibold" |  | ||||||
|                             : "bg-gray-200" |  | ||||||
|                         }`} |  | ||||||
|                         onClick={() => handleDisplayArticleFromList(artIdx)} |  | ||||||
|                             > |                             > | ||||||
|                         <Typography noWrap title={article.heading}> |                               {sight[language].right.length > 0 | ||||||
|                           {article.heading || "Без названия"} |                                 ? sight[language].right.map( | ||||||
|  |                                     (article, index) => ( | ||||||
|  |                                       <Draggable | ||||||
|  |                                         key={article.id.toString()} | ||||||
|  |                                         draggableId={article.id.toString()} | ||||||
|  |                                         index={index} | ||||||
|  |                                       > | ||||||
|  |                                         {(provided, snapshot) => ( | ||||||
|  |                                           <Box | ||||||
|  |                                             ref={provided.innerRef} | ||||||
|  |                                             {...provided.draggableProps} | ||||||
|  |                                             className={`w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300  ${ | ||||||
|  |                                               snapshot.isDragging | ||||||
|  |                                                 ? "shadow-lg" | ||||||
|  |                                                 : "" | ||||||
|  |                                             }`} | ||||||
|  |                                             onClick={() => { | ||||||
|  |                                               handleDisplayArticleFromList( | ||||||
|  |                                                 index | ||||||
|  |                                               ); | ||||||
|  |                                               setType("article"); | ||||||
|  |                                             }} | ||||||
|  |                                           > | ||||||
|  |                                             <Box {...provided.dragHandleProps}> | ||||||
|  |                                               <Typography> | ||||||
|  |                                                 {article.heading} | ||||||
|                                               </Typography> |                                               </Typography> | ||||||
|                                             </Box> |                                             </Box> | ||||||
|                     ))} |                                           </Box> | ||||||
|  |                                         )} | ||||||
|  |                                       </Draggable> | ||||||
|  |                                     ) | ||||||
|  |                                   ) | ||||||
|  |                                 : null} | ||||||
|  |                               {provided.placeholder} | ||||||
|  |                             </Box> | ||||||
|  |                           )} | ||||||
|  |                         </Droppable> | ||||||
|  |                       </DragDropContext> | ||||||
|  |                     </Box> | ||||||
|                   </Box> |                   </Box> | ||||||
|                   <button |                   <button | ||||||
|                     className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center hover:bg-blue-600" |                     className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center hover:bg-blue-600" | ||||||
| @@ -298,7 +364,7 @@ export const CreateRightTab = observer( | |||||||
|                   <Box className="w-[80%] border border-gray-300 p-3 flex flex-col gap-2 overflow-hidden"> |                   <Box className="w-[80%] border border-gray-300 p-3 flex flex-col gap-2 overflow-hidden"> | ||||||
|                     <Box className="flex justify-end gap-2 mb-1 flex-shrink-0"> |                     <Box className="flex justify-end gap-2 mb-1 flex-shrink-0"> | ||||||
|                       <Button |                       <Button | ||||||
|                         variant="outlined" |                         variant="contained" | ||||||
|                         color="primary" |                         color="primary" | ||||||
|                         size="small" |                         size="small" | ||||||
|                         startIcon={<Unlink color="white" size={18} />} |                         startIcon={<Unlink color="white" size={18} />} | ||||||
| @@ -381,9 +447,11 @@ export const CreateRightTab = observer( | |||||||
|                     </Box> |                     </Box> | ||||||
|                   </Box> |                   </Box> | ||||||
|                 ) : type === "media" ? ( |                 ) : 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 flex items-center justify-center"> |                   <Box className="w-[80%] border border-gray-300 rounded-2xl relative flex items-center justify-center"> | ||||||
|  |                     {sight.preview_media && ( | ||||||
|  |                       <> | ||||||
|  |                         {type === "media" && ( | ||||||
|  |                           <Box className="w-[80%] h-full  rounded-2xl relative flex items-center justify-center"> | ||||||
|                             {previewMedia && ( |                             {previewMedia && ( | ||||||
|                               <> |                               <> | ||||||
|                                 <Box className="absolute top-4 right-4  z-10"> |                                 <Box className="absolute top-4 right-4  z-10"> | ||||||
| @@ -395,7 +463,7 @@ export const CreateRightTab = observer( | |||||||
|                                   </button> |                                   </button> | ||||||
|                                 </Box> |                                 </Box> | ||||||
|  |  | ||||||
|                             <Box sx={{}}> |                                 <Box className="w-1/2 h-1/2"> | ||||||
|                                   <MediaViewer |                                   <MediaViewer | ||||||
|                                     media={{ |                                     media={{ | ||||||
|                                       id: previewMedia.id || "", |                                       id: previewMedia.id || "", | ||||||
| @@ -416,6 +484,16 @@ export const CreateRightTab = observer( | |||||||
|                             )} |                             )} | ||||||
|                           </Box> |                           </Box> | ||||||
|                         )} |                         )} | ||||||
|  |                       </> | ||||||
|  |                     )} | ||||||
|  |                     {!previewMedia && ( | ||||||
|  |                       <MediaAreaForSight | ||||||
|  |                         onFinishUpload={(mediaId) => { | ||||||
|  |                           linkPreviewMedia(mediaId); | ||||||
|  |                         }} | ||||||
|  |                         onFilesDrop={() => {}} | ||||||
|  |                       /> | ||||||
|  |                     )} | ||||||
|                   </Box> |                   </Box> | ||||||
|                 ) : ( |                 ) : ( | ||||||
|                   <Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center"> |                   <Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center"> | ||||||
| @@ -429,31 +507,52 @@ export const CreateRightTab = observer( | |||||||
|  |  | ||||||
|             {/* Right Column: Live Preview */} |             {/* Right Column: Live Preview */} | ||||||
|             <Box className="w-[25%] mr-10"> |             <Box className="w-[25%] mr-10"> | ||||||
|               {type === "article" && currentRightArticle && ( |               {type === "article" && activeArticleIndex !== null && ( | ||||||
|                 <Paper |                 <Paper | ||||||
|                   className="flex-1 flex flex-col rounded-2xl" |                   className="flex-1 flex flex-col  max-w-[500px]" | ||||||
|  |                   sx={{ | ||||||
|  |                     borderRadius: "16px", | ||||||
|  |                     overflow: "hidden", | ||||||
|  |                   }} | ||||||
|                   elevation={2} |                   elevation={2} | ||||||
|                   sx={{ height: "75vh", overflow: "hidden" }} |  | ||||||
|                 > |                 > | ||||||
|                   <Box |                   <Box | ||||||
|                     className="rounded-2xl overflow-hidden" |                     className=" overflow-hidden" | ||||||
|                     sx={{ |                     sx={{ | ||||||
|                       width: "100%", |                       width: "100%", | ||||||
|                       height: "100%", |  | ||||||
|                       background: "#877361", // Theme background |                       background: "#877361", | ||||||
|  |                       borderColor: "grey.300", | ||||||
|                       display: "flex", |                       display: "flex", | ||||||
|                       flexDirection: "column", |                       flexDirection: "column", | ||||||
|                     }} |                     }} | ||||||
|                   > |                   > | ||||||
|                     {currentRightArticle.media && |                     {sight[language].right[activeArticleIndex].media.length > | ||||||
|                     currentRightArticle.media.length > 0 ? ( |                     0 ? ( | ||||||
|                       <MediaViewer media={currentRightArticle.media[0]} /> |                       <Box | ||||||
|  |                         sx={{ | ||||||
|  |                           width: "100%", | ||||||
|  |                           maxHeight: "290px", | ||||||
|  |                           flexShrink: 0, | ||||||
|  |  | ||||||
|  |                           display: "flex", | ||||||
|  |                           alignItems: "center", | ||||||
|  |                           justifyContent: "center", | ||||||
|  |                         }} | ||||||
|  |                       > | ||||||
|  |                         <MediaViewer | ||||||
|  |                           media={ | ||||||
|  |                             sight[language].right[activeArticleIndex].media[0] | ||||||
|  |                           } | ||||||
|  |                         /> | ||||||
|  |                       </Box> | ||||||
|                     ) : ( |                     ) : ( | ||||||
|                       <Box |                       <Box | ||||||
|                         sx={{ |                         sx={{ | ||||||
|                           width: "100%", |                           width: "100%", | ||||||
|                           height: 200, |                           height: 200, | ||||||
|                           flexShrink: 0, |                           flexShrink: 0, | ||||||
|  |  | ||||||
|                           backgroundColor: "rgba(0,0,0,0.1)", |                           backgroundColor: "rgba(0,0,0,0.1)", | ||||||
|                           display: "flex", |                           display: "flex", | ||||||
|                           alignItems: "center", |                           alignItems: "center", | ||||||
| @@ -463,57 +562,85 @@ export const CreateRightTab = observer( | |||||||
|                         <ImagePlus size={48} color="white" /> |                         <ImagePlus size={48} color="white" /> | ||||||
|                       </Box> |                       </Box> | ||||||
|                     )} |                     )} | ||||||
|  |  | ||||||
|                     <Box |                     <Box | ||||||
|                       sx={{ |                       sx={{ | ||||||
|                         width: "100%", |                         p: 1, | ||||||
|                         minHeight: "70px", // Fixed height for heading container |                         wordBreak: "break-word", | ||||||
|                         background: "#877361", // Consistent with theme |                         fontSize: "24px", | ||||||
|                         display: "flex", |                         fontWeight: 700, | ||||||
|                         flexShrink: 0, |                         lineHeight: "120%", | ||||||
|                         flex: 1, |                         backdropFilter: "blur(12px)", | ||||||
|                         alignItems: "center", |                         boxShadow: | ||||||
|                         borderBottom: "1px solid rgba(255,255,255,0.1)", |                           "inset 4px 4px 12px 0 rgba(255,255,255,0.12)", | ||||||
|                         px: 2, |                         background: | ||||||
|                         py: 1, |                           "#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)", | ||||||
|                       }} |                       }} | ||||||
|                     > |                     > | ||||||
|                       <Typography |                       <Typography variant="h6" color="white"> | ||||||
|                         variant="h6" |                         {sight[language].right[activeArticleIndex].heading || | ||||||
|                         color="white" |                           "Выберите статью"} | ||||||
|                         noWrap |  | ||||||
|                         title={currentRightArticle.heading} |  | ||||||
|                       > |  | ||||||
|                         {currentRightArticle.heading || "Заголовок"} |  | ||||||
|                       </Typography> |                       </Typography> | ||||||
|                     </Box> |                     </Box> | ||||||
|  |  | ||||||
|                     <Box |                     <Box | ||||||
|                       sx={{ |                       sx={{ | ||||||
|                         px: 2, |                         padding: 1, | ||||||
|                         py: 1, |                         minHeight: "200px", | ||||||
|  |                         maxHeight: "300px", | ||||||
|  |                         overflowY: "scroll", | ||||||
|  |                         background: | ||||||
|  |                           "rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)", | ||||||
|  |  | ||||||
|                         flexGrow: 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 ? ( |                       {sight[language].right[activeArticleIndex].body ? ( | ||||||
|                         <ReactMarkdownComponent |                         <ReactMarkdownComponent | ||||||
|                           value={currentRightArticle.body} |                           value={sight[language].right[activeArticleIndex].body} | ||||||
|                         /> |                         /> | ||||||
|                       ) : ( |                       ) : ( | ||||||
|                         <Typography |                         <Typography | ||||||
|                           color="rgba(255,255,255,0.7)" |                           color="rgba(255,255,255,0.7)" | ||||||
|                           sx={{ textAlign: "center", mt: 4 }} |                           sx={{ textAlign: "center", mt: 4 }} | ||||||
|                         > |                         > | ||||||
|                           Содержимое статьи... |                           Предпросмотр статьи появится здесь | ||||||
|                         </Typography> |                         </Typography> | ||||||
|                       )} |                       )} | ||||||
|                     </Box> |                     </Box> | ||||||
|  |                     <Box | ||||||
|  |                       sx={{ | ||||||
|  |                         p: 2, | ||||||
|  |                         display: "flex", | ||||||
|  |                         justifyContent: "space-between", | ||||||
|  |                         fontSize: "24px", | ||||||
|  |                         fontWeight: 700, | ||||||
|  |                         lineHeight: "120%", | ||||||
|  |                         flexWrap: "wrap", | ||||||
|  |  | ||||||
|  |                         gap: 1, | ||||||
|  |                         backdropFilter: "blur(12px)", | ||||||
|  |                         boxShadow: | ||||||
|  |                           "inset 4px 4px 12px 0 rgba(255,255,255,0.12)", | ||||||
|  |                         background: | ||||||
|  |                           "#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)", | ||||||
|  |                       }} | ||||||
|  |                     > | ||||||
|  |                       {sight[language].right.length > 0 && | ||||||
|  |                         sight[language].right.map((article, index) => ( | ||||||
|  |                           <button | ||||||
|  |                             className={`inline-block text-left text-xs text-white ${ | ||||||
|  |                               activeArticleIndex === index ? "underline" : "" | ||||||
|  |                             }`} | ||||||
|  |                             onClick={() => { | ||||||
|  |                               setActiveArticleIndex(index); | ||||||
|  |                               setType("article"); | ||||||
|  |                             }} | ||||||
|  |                           > | ||||||
|  |                             {article.heading} | ||||||
|  |                           </button> | ||||||
|  |                         ))} | ||||||
|  |                     </Box> | ||||||
|                   </Box> |                   </Box> | ||||||
|                 </Paper> |                 </Paper> | ||||||
|               )} |               )} | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import { | |||||||
|   TextField, |   TextField, | ||||||
| } from "@mui/material"; | } from "@mui/material"; | ||||||
| import { | import { | ||||||
|  |   authInstance, | ||||||
|   BackButton, |   BackButton, | ||||||
|   editSightStore, |   editSightStore, | ||||||
|   languageStore, |   languageStore, | ||||||
| @@ -17,6 +18,7 @@ import { | |||||||
|   UploadMediaDialog, |   UploadMediaDialog, | ||||||
| } from "@shared"; | } from "@shared"; | ||||||
| import { | import { | ||||||
|  |   DeleteModal, | ||||||
|   LanguageSwitcher, |   LanguageSwitcher, | ||||||
|   MediaArea, |   MediaArea, | ||||||
|   MediaAreaForSight, |   MediaAreaForSight, | ||||||
| @@ -28,6 +30,12 @@ import { observer } from "mobx-react-lite"; | |||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { toast } from "react-toastify"; | import { toast } from "react-toastify"; | ||||||
| import { MediaViewer } from "../../MediaViewer/index"; | import { MediaViewer } from "../../MediaViewer/index"; | ||||||
|  | import { | ||||||
|  |   DragDropContext, | ||||||
|  |   Droppable, | ||||||
|  |   Draggable, | ||||||
|  |   DropResult, | ||||||
|  | } from "@hello-pangea/dnd"; | ||||||
|  |  | ||||||
| export const RightWidgetTab = observer( | export const RightWidgetTab = observer( | ||||||
|   ({ value, index }: { value: number; index: number }) => { |   ({ value, index }: { value: number; index: number }) => { | ||||||
| @@ -47,14 +55,21 @@ export const RightWidgetTab = observer( | |||||||
|       createLinkWithRightArticle, |       createLinkWithRightArticle, | ||||||
|       setFileToUpload, |       setFileToUpload, | ||||||
|       createNewRightArticle, |       createNewRightArticle, | ||||||
|  |       updateRightArticles, | ||||||
|     } = editSightStore; |     } = editSightStore; | ||||||
|  |  | ||||||
|     const [previewMedia, setPreviewMedia] = useState<any | null>(null); |     const [previewMedia, setPreviewMedia] = useState<any | null>(null); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|  |       const fetchPreviewMedia = async () => { | ||||||
|         if (sight.common.preview_media) { |         if (sight.common.preview_media) { | ||||||
|         setPreviewMedia(sight.common.preview_media); |           const response = await authInstance.get( | ||||||
|  |             `/media/${sight.common.preview_media}` | ||||||
|  |           ); | ||||||
|  |           setPreviewMedia(response.data); | ||||||
|         } |         } | ||||||
|  |       }; | ||||||
|  |       fetchPreviewMedia(); | ||||||
|     }, [sight.common.preview_media]); |     }, [sight.common.preview_media]); | ||||||
|  |  | ||||||
|     const handleUnlinkPreviewMedia = () => { |     const handleUnlinkPreviewMedia = () => { | ||||||
| @@ -80,8 +95,15 @@ export const RightWidgetTab = observer( | |||||||
|     ); |     ); | ||||||
|     const [isSelectModalOpen, setIsSelectModalOpen] = useState(false); |     const [isSelectModalOpen, setIsSelectModalOpen] = useState(false); | ||||||
|     const [isSelectMediaModalOpen, setIsSelectMediaModalOpen] = useState(false); |     const [isSelectMediaModalOpen, setIsSelectMediaModalOpen] = useState(false); | ||||||
|  |     const [isDeleteArticleModalOpen, setIsDeleteArticleModalOpen] = | ||||||
|  |       useState(false); | ||||||
|     const open = Boolean(anchorEl); |     const open = Boolean(anchorEl); | ||||||
|  |  | ||||||
|  |     const handleDeleteArticle = () => { | ||||||
|  |       deleteRightArticle(sight[language].right[activeArticleIndex || 0].id); | ||||||
|  |       setActiveArticleIndex(null); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { |     const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { | ||||||
|       setAnchorEl(event.currentTarget); |       setAnchorEl(event.currentTarget); | ||||||
|     }; |     }; | ||||||
| @@ -133,6 +155,36 @@ export const RightWidgetTab = observer( | |||||||
|       console.log(sight[language].right); |       console.log(sight[language].right); | ||||||
|     }, [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 ( |     return ( | ||||||
|       <TabPanel value={value} index={index}> |       <TabPanel value={value} index={index}> | ||||||
|         <LanguageSwitcher /> |         <LanguageSwitcher /> | ||||||
| @@ -160,19 +212,54 @@ export const RightWidgetTab = observer( | |||||||
|                     > |                     > | ||||||
|                       <Typography>Предпросмотр медиа</Typography> |                       <Typography>Предпросмотр медиа</Typography> | ||||||
|                     </Box> |                     </Box> | ||||||
|                     {sight[language].right.length > 0 && |                     <Box> | ||||||
|                       sight[language].right.map((article, index) => ( |                       <DragDropContext onDragEnd={handleDragEnd}> | ||||||
|  |                         <Droppable droppableId="articles"> | ||||||
|  |                           {(provided) => ( | ||||||
|                             <Box |                             <Box | ||||||
|                           key={index} |                               ref={provided.innerRef} | ||||||
|                           className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300" |                               {...provided.droppableProps} | ||||||
|  |                               className="flex flex-col gap-2" | ||||||
|  |                             > | ||||||
|  |                               {sight[language].right.length > 0 | ||||||
|  |                                 ? sight[language].right.map( | ||||||
|  |                                     (article, index) => ( | ||||||
|  |                                       <Draggable | ||||||
|  |                                         key={article.id.toString()} | ||||||
|  |                                         draggableId={article.id.toString()} | ||||||
|  |                                         index={index} | ||||||
|  |                                       > | ||||||
|  |                                         {(provided, snapshot) => ( | ||||||
|  |                                           <Box | ||||||
|  |                                             ref={provided.innerRef} | ||||||
|  |                                             {...provided.draggableProps} | ||||||
|  |                                             className={`w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300  ${ | ||||||
|  |                                               snapshot.isDragging | ||||||
|  |                                                 ? "shadow-lg" | ||||||
|  |                                                 : "" | ||||||
|  |                                             }`} | ||||||
|                                             onClick={() => { |                                             onClick={() => { | ||||||
|                                               handleSelectArticle(index); |                                               handleSelectArticle(index); | ||||||
|                                               setType("article"); |                                               setType("article"); | ||||||
|                                             }} |                                             }} | ||||||
|                                           > |                                           > | ||||||
|                           <Typography>{article.heading}</Typography> |                                             <Box {...provided.dragHandleProps}> | ||||||
|  |                                               <Typography> | ||||||
|  |                                                 {article.heading} | ||||||
|  |                                               </Typography> | ||||||
|  |                                             </Box> | ||||||
|  |                                           </Box> | ||||||
|  |                                         )} | ||||||
|  |                                       </Draggable> | ||||||
|  |                                     ) | ||||||
|  |                                   ) | ||||||
|  |                                 : null} | ||||||
|  |                               {provided.placeholder} | ||||||
|  |                             </Box> | ||||||
|  |                           )} | ||||||
|  |                         </Droppable> | ||||||
|  |                       </DragDropContext> | ||||||
|                     </Box> |                     </Box> | ||||||
|                       ))} |  | ||||||
|                   </Box> |                   </Box> | ||||||
|                   <button |                   <button | ||||||
|                     className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center" |                     className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center" | ||||||
| @@ -200,7 +287,7 @@ export const RightWidgetTab = observer( | |||||||
|                 </Box> |                 </Box> | ||||||
|  |  | ||||||
|                 {type === "article" && ( |                 {type === "article" && ( | ||||||
|                   <Box className="w-[80%] border border-gray-300 rounded-2xl p-3"> |                   <Box className="w-[80%] border border-gray-300  p-3"> | ||||||
|                     {activeArticleIndex !== null && ( |                     {activeArticleIndex !== null && ( | ||||||
|                       <> |                       <> | ||||||
|                         <Box className="flex justify-end gap-2 mb-3"> |                         <Box className="flex justify-end gap-2 mb-3"> | ||||||
| @@ -222,10 +309,7 @@ export const RightWidgetTab = observer( | |||||||
|                             color="error" |                             color="error" | ||||||
|                             startIcon={<Trash2 size={18} />} |                             startIcon={<Trash2 size={18} />} | ||||||
|                             onClick={() => { |                             onClick={() => { | ||||||
|                               deleteRightArticle( |                               setIsDeleteArticleModalOpen(true); | ||||||
|                                 sight[language].right[activeArticleIndex].id |  | ||||||
|                               ); |  | ||||||
|                               setActiveArticleIndex(null); |  | ||||||
|                             }} |                             }} | ||||||
|                           > |                           > | ||||||
|                             Удалить |                             Удалить | ||||||
| @@ -300,9 +384,8 @@ export const RightWidgetTab = observer( | |||||||
|                   <Box className="w-[80%] border border-gray-300 rounded-2xl relative"> |                   <Box className="w-[80%] border border-gray-300 rounded-2xl relative"> | ||||||
|                     {sight.common.preview_media && ( |                     {sight.common.preview_media && ( | ||||||
|                       <> |                       <> | ||||||
|                         <Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 relative flex justify-center items-center"> |  | ||||||
|                         {type === "media" && ( |                         {type === "media" && ( | ||||||
|                             <Box className="w-[80%] border border-gray-300 rounded-2xl relative flex items-center justify-center"> |                           <Box className="w-[80%] h-full  rounded-2xl relative flex items-center justify-center"> | ||||||
|                             {previewMedia && ( |                             {previewMedia && ( | ||||||
|                               <> |                               <> | ||||||
|                                 <Box className="absolute top-4 right-4  z-10"> |                                 <Box className="absolute top-4 right-4  z-10"> | ||||||
| @@ -314,7 +397,7 @@ export const RightWidgetTab = observer( | |||||||
|                                   </button> |                                   </button> | ||||||
|                                 </Box> |                                 </Box> | ||||||
|  |  | ||||||
|                                   <Box sx={{}}> |                                 <Box className="w-1/2 h-1/2"> | ||||||
|                                   <MediaViewer |                                   <MediaViewer | ||||||
|                                     media={{ |                                     media={{ | ||||||
|                                       id: previewMedia.id || "", |                                       id: previewMedia.id || "", | ||||||
| @@ -335,7 +418,6 @@ export const RightWidgetTab = observer( | |||||||
|                             )} |                             )} | ||||||
|                           </Box> |                           </Box> | ||||||
|                         )} |                         )} | ||||||
|                         </Box> |  | ||||||
|                       </> |                       </> | ||||||
|                     )} |                     )} | ||||||
|  |  | ||||||
| @@ -369,14 +451,18 @@ export const RightWidgetTab = observer( | |||||||
|               <Box className="w-[25%] mr-10"> |               <Box className="w-[25%] mr-10"> | ||||||
|                 {activeArticleIndex !== null && ( |                 {activeArticleIndex !== null && ( | ||||||
|                   <Paper |                   <Paper | ||||||
|                     className="flex-1 flex flex-col rounded-2xl" |                     className="flex-1 flex flex-col  max-w-[500px]" | ||||||
|  |                     sx={{ | ||||||
|  |                       borderRadius: "16px", | ||||||
|  |                       overflow: "hidden", | ||||||
|  |                     }} | ||||||
|                     elevation={2} |                     elevation={2} | ||||||
|                   > |                   > | ||||||
|                     <Box |                     <Box | ||||||
|                       className="rounded-2xl overflow-hidden" |                       className=" overflow-hidden" | ||||||
|                       sx={{ |                       sx={{ | ||||||
|                         width: "100%", |                         width: "100%", | ||||||
|                         height: "75vh", |  | ||||||
|                         background: "#877361", |                         background: "#877361", | ||||||
|                         borderColor: "grey.300", |                         borderColor: "grey.300", | ||||||
|                         display: "flex", |                         display: "flex", | ||||||
| @@ -385,11 +471,23 @@ export const RightWidgetTab = observer( | |||||||
|                     > |                     > | ||||||
|                       {sight[language].right[activeArticleIndex].media.length > |                       {sight[language].right[activeArticleIndex].media.length > | ||||||
|                       0 ? ( |                       0 ? ( | ||||||
|  |                         <Box | ||||||
|  |                           sx={{ | ||||||
|  |                             width: "100%", | ||||||
|  |                             maxHeight: "290px", | ||||||
|  |                             flexShrink: 0, | ||||||
|  |  | ||||||
|  |                             display: "flex", | ||||||
|  |                             alignItems: "center", | ||||||
|  |                             justifyContent: "center", | ||||||
|  |                           }} | ||||||
|  |                         > | ||||||
|                           <MediaViewer |                           <MediaViewer | ||||||
|                             media={ |                             media={ | ||||||
|                               sight[language].right[activeArticleIndex].media[0] |                               sight[language].right[activeArticleIndex].media[0] | ||||||
|                             } |                             } | ||||||
|                           /> |                           /> | ||||||
|  |                         </Box> | ||||||
|                       ) : ( |                       ) : ( | ||||||
|                         <Box |                         <Box | ||||||
|                           sx={{ |                           sx={{ | ||||||
| @@ -409,15 +507,16 @@ export const RightWidgetTab = observer( | |||||||
|  |  | ||||||
|                       <Box |                       <Box | ||||||
|                         sx={{ |                         sx={{ | ||||||
|                           width: "100%", |                           p: 1, | ||||||
|                           height: "70px", |                           wordBreak: "break-word", | ||||||
|                           background: "#877361", |                           fontSize: "24px", | ||||||
|                           display: "flex", |                           fontWeight: 700, | ||||||
|                           flexShrink: 0, |                           lineHeight: "120%", | ||||||
|  |                           backdropFilter: "blur(12px)", | ||||||
|                           alignItems: "center", |                           boxShadow: | ||||||
|                           borderBottom: "1px solid rgba(255,255,255,0.1)", |                             "inset 4px 4px 12px 0 rgba(255,255,255,0.12)", | ||||||
|                           px: 2, |                           background: | ||||||
|  |                             "#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)", | ||||||
|                         }} |                         }} | ||||||
|                       > |                       > | ||||||
|                         <Typography variant="h6" color="white"> |                         <Typography variant="h6" color="white"> | ||||||
| @@ -428,14 +527,14 @@ export const RightWidgetTab = observer( | |||||||
|  |  | ||||||
|                       <Box |                       <Box | ||||||
|                         sx={{ |                         sx={{ | ||||||
|                           px: 2, |                           padding: 1, | ||||||
|  |                           minHeight: "200px", | ||||||
|  |                           maxHeight: "300px", | ||||||
|  |                           overflowY: "scroll", | ||||||
|  |                           background: | ||||||
|  |                             "rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)", | ||||||
|  |  | ||||||
|                           flexGrow: 1, |                           flexGrow: 1, | ||||||
|                           flex: 1, |  | ||||||
|                           minHeight: "300px", |  | ||||||
|                           overflowY: "auto", |  | ||||||
|                           backgroundColor: "#877361", |  | ||||||
|                           color: "white", |  | ||||||
|                           py: 1, |  | ||||||
|                         }} |                         }} | ||||||
|                       > |                       > | ||||||
|                         {sight[language].right[activeArticleIndex].body ? ( |                         {sight[language].right[activeArticleIndex].body ? ( | ||||||
| @@ -453,6 +552,39 @@ export const RightWidgetTab = observer( | |||||||
|                           </Typography> |                           </Typography> | ||||||
|                         )} |                         )} | ||||||
|                       </Box> |                       </Box> | ||||||
|  |                       <Box | ||||||
|  |                         sx={{ | ||||||
|  |                           p: 2, | ||||||
|  |                           display: "flex", | ||||||
|  |                           justifyContent: "space-between", | ||||||
|  |                           fontSize: "24px", | ||||||
|  |                           fontWeight: 700, | ||||||
|  |                           lineHeight: "120%", | ||||||
|  |                           flexWrap: "wrap", | ||||||
|  |  | ||||||
|  |                           gap: 1, | ||||||
|  |                           backdropFilter: "blur(12px)", | ||||||
|  |                           boxShadow: | ||||||
|  |                             "inset 4px 4px 12px 0 rgba(255,255,255,0.12)", | ||||||
|  |                           background: | ||||||
|  |                             "#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)", | ||||||
|  |                         }} | ||||||
|  |                       > | ||||||
|  |                         {sight[language].right.length > 0 && | ||||||
|  |                           sight[language].right.map((article, index) => ( | ||||||
|  |                             <button | ||||||
|  |                               className={`inline-block text-left text-xs text-white ${ | ||||||
|  |                                 activeArticleIndex === index ? "underline" : "" | ||||||
|  |                               }`} | ||||||
|  |                               onClick={() => { | ||||||
|  |                                 handleSelectArticle(index); | ||||||
|  |                                 setType("article"); | ||||||
|  |                               }} | ||||||
|  |                             > | ||||||
|  |                               {article.heading} | ||||||
|  |                             </button> | ||||||
|  |                           ))} | ||||||
|  |                       </Box> | ||||||
|                     </Box> |                     </Box> | ||||||
|                   </Paper> |                   </Paper> | ||||||
|                 )} |                 )} | ||||||
| @@ -496,10 +628,20 @@ export const RightWidgetTab = observer( | |||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|  |         <DeleteModal | ||||||
|  |           open={isDeleteArticleModalOpen} | ||||||
|  |           onCancel={() => setIsDeleteArticleModalOpen(false)} | ||||||
|  |           onDelete={() => { | ||||||
|  |             handleDeleteArticle(); | ||||||
|  |             setIsDeleteArticleModalOpen(false); | ||||||
|  |           }} | ||||||
|  |         /> | ||||||
|  |  | ||||||
|         <SelectArticleModal |         <SelectArticleModal | ||||||
|           open={isSelectModalOpen} |           open={isSelectModalOpen} | ||||||
|           onClose={handleCloseSelectModal} |           onClose={handleCloseSelectModal} | ||||||
|           onSelectArticle={handleArticleSelect} |           onSelectArticle={handleArticleSelect} | ||||||
|  |           linkedArticleIds={sight[language].right.map((article) => article.id)} | ||||||
|         /> |         /> | ||||||
|         <SelectMediaDialog |         <SelectMediaDialog | ||||||
|           open={isSelectMediaModalOpen} |           open={isSelectMediaModalOpen} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user