feat: DND for Articles

This commit is contained in:
2025-06-04 22:01:20 +03:00
parent 89488d9921
commit 1973ff4304
5 changed files with 477 additions and 194 deletions

View File

@ -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 (
<TabPanel value={value} index={index}>
<LanguageSwitcher />
@ -245,22 +277,56 @@ export const CreateRightTab = observer(
>
<Typography>Предпросмотр медиа</Typography>
</Box>
{sight[language].right.map((article, artIdx) => (
<Box
key={article.id || artIdx} // article.id should be preferred
className={`w-full p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300 ${
type === "article" && activeArticleIndex === artIdx
? "bg-blue-300 font-semibold"
: "bg-gray-200"
}`}
onClick={() => handleDisplayArticleFromList(artIdx)}
>
<Typography noWrap title={article.heading}>
{article.heading || "Без названия"}
</Typography>
</Box>
))}
<Box>
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="articles">
{(provided) => (
<Box
ref={provided.innerRef}
{...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={() => {
handleDisplayArticleFromList(
index
);
setType("article");
}}
>
<Box {...provided.dragHandleProps}>
<Typography>
{article.heading}
</Typography>
</Box>
</Box>
)}
</Draggable>
)
)
: null}
{provided.placeholder}
</Box>
)}
</Droppable>
</DragDropContext>
</Box>
</Box>
<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"
@ -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="flex justify-end gap-2 mb-1 flex-shrink-0">
<Button
variant="outlined"
variant="contained"
color="primary"
size="small"
startIcon={<Unlink color="white" size={18} />}
@ -381,40 +447,52 @@ export const CreateRightTab = observer(
</Box>
</Box>
) : type === "media" ? (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 relative flex justify-center items-center">
{type === "media" && (
<Box className="w-[80%] border border-gray-300 rounded-2xl relative flex items-center justify-center">
{previewMedia && (
<>
<Box className="absolute top-4 right-4 z-10">
<button
className="w-10 h-10 flex items-center justify-center z-10"
onClick={handleUnlinkPreviewMedia}
>
<X size={20} color="red" />
</button>
</Box>
<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 && (
<>
<Box className="absolute top-4 right-4 z-10">
<button
className="w-10 h-10 flex items-center justify-center z-10"
onClick={handleUnlinkPreviewMedia}
>
<X size={20} color="red" />
</button>
</Box>
<Box sx={{}}>
<MediaViewer
media={{
id: previewMedia.id || "",
media_type: previewMedia.media_type,
filename: previewMedia.filename || "",
<Box className="w-1/2 h-1/2">
<MediaViewer
media={{
id: previewMedia.id || "",
media_type: previewMedia.media_type,
filename: previewMedia.filename || "",
}}
/>
</Box>
</>
)}
{!previewMedia && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
</Box>
</>
)}
</Box>
)}
{!previewMedia && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
)}
</Box>
</>
)}
{!previewMedia && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
)}
</Box>
) : (
@ -429,31 +507,52 @@ export const CreateRightTab = observer(
{/* Right Column: Live Preview */}
<Box className="w-[25%] mr-10">
{type === "article" && currentRightArticle && (
{type === "article" && activeArticleIndex !== null && (
<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}
sx={{ height: "75vh", overflow: "hidden" }}
>
<Box
className="rounded-2xl overflow-hidden"
className=" overflow-hidden"
sx={{
width: "100%",
height: "100%",
background: "#877361", // Theme background
background: "#877361",
borderColor: "grey.300",
display: "flex",
flexDirection: "column",
}}
>
{currentRightArticle.media &&
currentRightArticle.media.length > 0 ? (
<MediaViewer media={currentRightArticle.media[0]} />
{sight[language].right[activeArticleIndex].media.length >
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
sx={{
width: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
@ -463,57 +562,85 @@ export const CreateRightTab = observer(
<ImagePlus size={48} color="white" />
</Box>
)}
<Box
sx={{
width: "100%",
minHeight: "70px", // Fixed height for heading container
background: "#877361", // Consistent with theme
display: "flex",
flexShrink: 0,
flex: 1,
alignItems: "center",
borderBottom: "1px solid rgba(255,255,255,0.1)",
px: 2,
py: 1,
p: 1,
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
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%)",
}}
>
<Typography
variant="h6"
color="white"
noWrap
title={currentRightArticle.heading}
>
{currentRightArticle.heading || "Заголовок"}
<Typography variant="h6" color="white">
{sight[language].right[activeArticleIndex].heading ||
"Выберите статью"}
</Typography>
</Box>
<Box
sx={{
px: 2,
py: 1,
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,
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
value={currentRightArticle.body}
value={sight[language].right[activeArticleIndex].body}
/>
) : (
<Typography
color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }}
>
Содержимое статьи...
Предпросмотр статьи появится здесь
</Typography>
)}
</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>
</Paper>
)}