feat: DND for Articles
This commit is contained in:
parent
89488d9921
commit
1973ff4304
@ -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`
|
);
|
||||||
);
|
mediaMap.set(article.id, responseMedia.data);
|
||||||
articlesWithMedia.push({
|
}
|
||||||
...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}>
|
||||||
<Box
|
<Droppable droppableId="articles">
|
||||||
key={article.id || artIdx} // article.id should be preferred
|
{(provided) => (
|
||||||
className={`w-full p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300 ${
|
<Box
|
||||||
type === "article" && activeArticleIndex === artIdx
|
ref={provided.innerRef}
|
||||||
? "bg-blue-300 font-semibold"
|
{...provided.droppableProps}
|
||||||
: "bg-gray-200"
|
className="flex flex-col gap-2"
|
||||||
}`}
|
>
|
||||||
onClick={() => handleDisplayArticleFromList(artIdx)}
|
{sight[language].right.length > 0
|
||||||
>
|
? sight[language].right.map(
|
||||||
<Typography noWrap title={article.heading}>
|
(article, index) => (
|
||||||
{article.heading || "Без названия"}
|
<Draggable
|
||||||
</Typography>
|
key={article.id.toString()}
|
||||||
</Box>
|
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>
|
||||||
|
</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,40 +447,52 @@ 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">
|
<Box className="w-[80%] border border-gray-300 rounded-2xl relative flex items-center justify-center">
|
||||||
{type === "media" && (
|
{sight.preview_media && (
|
||||||
<Box className="w-[80%] border border-gray-300 rounded-2xl relative flex items-center justify-center">
|
<>
|
||||||
{previewMedia && (
|
{type === "media" && (
|
||||||
<>
|
<Box className="w-[80%] h-full rounded-2xl relative flex items-center justify-center">
|
||||||
<Box className="absolute top-4 right-4 z-10">
|
{previewMedia && (
|
||||||
<button
|
<>
|
||||||
className="w-10 h-10 flex items-center justify-center z-10"
|
<Box className="absolute top-4 right-4 z-10">
|
||||||
onClick={handleUnlinkPreviewMedia}
|
<button
|
||||||
>
|
className="w-10 h-10 flex items-center justify-center z-10"
|
||||||
<X size={20} color="red" />
|
onClick={handleUnlinkPreviewMedia}
|
||||||
</button>
|
>
|
||||||
</Box>
|
<X size={20} color="red" />
|
||||||
|
</button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box sx={{}}>
|
<Box className="w-1/2 h-1/2">
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
id: previewMedia.id || "",
|
id: previewMedia.id || "",
|
||||||
media_type: previewMedia.media_type,
|
media_type: previewMedia.media_type,
|
||||||
filename: previewMedia.filename || "",
|
filename: previewMedia.filename || "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!previewMedia && (
|
||||||
|
<MediaAreaForSight
|
||||||
|
onFinishUpload={(mediaId) => {
|
||||||
|
linkPreviewMedia(mediaId);
|
||||||
}}
|
}}
|
||||||
|
onFilesDrop={() => {}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
)}
|
||||||
</>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{!previewMedia && (
|
</>
|
||||||
<MediaAreaForSight
|
)}
|
||||||
onFinishUpload={(mediaId) => {
|
{!previewMedia && (
|
||||||
linkPreviewMedia(mediaId);
|
<MediaAreaForSight
|
||||||
}}
|
onFinishUpload={(mediaId) => {
|
||||||
onFilesDrop={() => {}}
|
linkPreviewMedia(mediaId);
|
||||||
/>
|
}}
|
||||||
)}
|
onFilesDrop={() => {}}
|
||||||
</Box>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
@ -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(() => {
|
||||||
if (sight.common.preview_media) {
|
const fetchPreviewMedia = async () => {
|
||||||
setPreviewMedia(sight.common.preview_media);
|
if (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 />
|
||||||
@ -153,26 +205,61 @@ export const RightWidgetTab = observer(
|
|||||||
<Box className="flex flex-col w-[75%] gap-2">
|
<Box className="flex flex-col w-[75%] gap-2">
|
||||||
<Box className="w-full flex gap-2">
|
<Box className="w-full flex gap-2">
|
||||||
<Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl overflow-y-auto gap-3 border border-gray-300 p-3">
|
<Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl overflow-y-auto gap-3 border border-gray-300 p-3">
|
||||||
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto ">
|
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto">
|
||||||
<Box
|
<Box
|
||||||
onClick={() => setType("media")}
|
onClick={() => 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"
|
||||||
>
|
>
|
||||||
<Typography>Предпросмотр медиа</Typography>
|
<Typography>Предпросмотр медиа</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{sight[language].right.length > 0 &&
|
<Box>
|
||||||
sight[language].right.map((article, index) => (
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
<Box
|
<Droppable droppableId="articles">
|
||||||
key={index}
|
{(provided) => (
|
||||||
className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300"
|
<Box
|
||||||
onClick={() => {
|
ref={provided.innerRef}
|
||||||
handleSelectArticle(index);
|
{...provided.droppableProps}
|
||||||
setType("article");
|
className="flex flex-col gap-2"
|
||||||
}}
|
>
|
||||||
>
|
{sight[language].right.length > 0
|
||||||
<Typography>{article.heading}</Typography>
|
? sight[language].right.map(
|
||||||
</Box>
|
(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={() => {
|
||||||
|
handleSelectArticle(index);
|
||||||
|
setType("article");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box {...provided.dragHandleProps}>
|
||||||
|
<Typography>
|
||||||
|
{article.heading}
|
||||||
|
</Typography>
|
||||||
|
</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"
|
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,42 +384,40 @@ 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%] h-full rounded-2xl relative flex items-center justify-center">
|
||||||
<Box className="w-[80%] border border-gray-300 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">
|
<button
|
||||||
<button
|
className="w-10 h-10 flex items-center justify-center z-10"
|
||||||
className="w-10 h-10 flex items-center justify-center z-10"
|
onClick={handleUnlinkPreviewMedia}
|
||||||
onClick={handleUnlinkPreviewMedia}
|
>
|
||||||
>
|
<X size={20} color="red" />
|
||||||
<X size={20} color="red" />
|
</button>
|
||||||
</button>
|
</Box>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{}}>
|
<Box className="w-1/2 h-1/2">
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
id: previewMedia.id || "",
|
id: previewMedia.id || "",
|
||||||
media_type: previewMedia.media_type,
|
media_type: previewMedia.media_type,
|
||||||
filename: previewMedia.filename || "",
|
filename: previewMedia.filename || "",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!previewMedia && (
|
{!previewMedia && (
|
||||||
<MediaAreaForSight
|
<MediaAreaForSight
|
||||||
onFinishUpload={(mediaId) => {
|
onFinishUpload={(mediaId) => {
|
||||||
linkPreviewMedia(mediaId);
|
linkPreviewMedia(mediaId);
|
||||||
}}
|
}}
|
||||||
onFilesDrop={() => {}}
|
onFilesDrop={() => {}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</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 ? (
|
||||||
<MediaViewer
|
<Box
|
||||||
media={
|
sx={{
|
||||||
sight[language].right[activeArticleIndex].media[0]
|
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={{
|
||||||
@ -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}
|
||||||
|
Loading…
Reference in New Issue
Block a user