Update media select in EditSightPage and CreateSightPage

This commit is contained in:
2025-06-03 14:32:42 +03:00
parent f0e03c3a1d
commit 078f051e8a
14 changed files with 1361 additions and 1175 deletions

View File

@ -13,42 +13,179 @@ import {
languageStore,
SelectArticleModal,
TabPanel,
SelectMediaDialog, // Import
UploadMediaDialog, // Import
} from "@shared";
import {
LanguageSwitcher,
MediaArea, // Import
MediaAreaForSight, // Import
ReactMarkdownComponent,
ReactMarkdownEditor,
} from "@widgets";
import { ImagePlus, Plus } from "lucide-react";
import { ImagePlus, Plus, X } from "lucide-react"; // Import X
import { observer } from "mobx-react-lite";
import { useState } from "react";
import { useState, useEffect } from "react"; // Added useEffect
import { MediaViewer } from "../../MediaViewer/index";
import { toast } from "react-toastify";
type MediaItemShared = {
// Define if not already available from @shared
id: string;
filename: string;
media_name?: string;
media_type: number;
};
// --- RightWidgetTab (Parent) Component ---
export const CreateRightTab = observer(
({ value, index }: { value: number; index: number }) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const { sight, createNewRightArticle, updateRightArticleInfo } =
createSightStore;
const {
sight,
createNewRightArticle,
updateRightArticleInfo,
linkPreviewMedia,
unlinkPreviewMedia,
createLinkWithRightArticle,
deleteRightArticleMedia,
setFileToUpload, // From store
setUploadMediaOpen, // From store
uploadMediaOpen, // From store
unlinkRightAritcle, // Corrected spelling
deleteRightArticle,
linkExistingRightArticle,
createSight,
clearCreateSight, // For resetting form
} = createSightStore;
const { language } = languageStore;
const [articleDialogOpen, setArticleDialogOpen] = useState(false);
const [selectArticleDialogOpen, setSelectArticleDialogOpen] =
useState(false);
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
null
);
const [type, setType] = useState<"article" | "media">("media");
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
useState(false);
const [mediaTarget, setMediaTarget] = useState<
"sightPreview" | "rightArticle" | null
>(null);
// Reset activeArticleIndex if language changes and index is out of bounds
useEffect(() => {
if (
activeArticleIndex !== null &&
activeArticleIndex >= sight[language].right.length
) {
setActiveArticleIndex(null);
setType("media"); // Default back to media preview if selected article disappears
}
}, [language, sight[language].right, activeArticleIndex]);
const openMenu = Boolean(anchorEl);
const handleClickMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
const handleCloseMenu = () => {
setAnchorEl(null);
};
const handleSave = () => {
console.log("Saving right widget...");
const handleSave = async () => {
try {
await createSight(language);
toast.success("Достопримечательность успешно создана!");
clearCreateSight(); // Reset form
setActiveArticleIndex(null);
setType("media");
// Potentially navigate away: history.push('/sights-list');
} catch (error) {
console.error("Failed to save sight:", error);
toast.error("Ошибка при создании достопримечательности.");
}
};
const handleSelectArticle = (index: number) => {
setActiveArticleIndex(index);
const handleDisplayArticleFromList = (idx: number) => {
setActiveArticleIndex(idx);
setType("article");
};
const handleCreateNewLocalArticle = async () => {
handleCloseMenu();
try {
const newArticleId = await createNewRightArticle();
// Automatically select the new article if ID is returned
const newIndex = sight[language].right.findIndex(
(a) => a.id === newArticleId
);
if (newIndex > -1) {
setActiveArticleIndex(newIndex);
setType("article");
} else {
// Fallback if findIndex fails (should not happen if store updates correctly)
setActiveArticleIndex(sight[language].right.length - 1);
setType("article");
}
} catch (error) {
toast.error("Не удалось создать новую статью.");
}
};
const handleSelectExistingArticleAndLink = async (
selectedArticleId: number
) => {
try {
await linkExistingRightArticle(selectedArticleId);
setSelectArticleDialogOpen(false); // Close dialog
const newIndex = sight[language].right.findIndex(
(a) => a.id === selectedArticleId
);
if (newIndex > -1) {
setActiveArticleIndex(newIndex);
setType("article");
}
} catch (error) {
toast.error("Не удалось привязать существующую статью.");
}
};
const currentRightArticle =
activeArticleIndex !== null && sight[language].right[activeArticleIndex]
? sight[language].right[activeArticleIndex]
: null;
// Media Handling for Dialogs
const handleOpenUploadMedia = () => {
setUploadMediaOpen(true);
};
const handleOpenSelectMediaDialog = (
target: "sightPreview" | "rightArticle"
) => {
setMediaTarget(target);
setIsSelectMediaDialogOpen(true);
};
const handleMediaSelectedFromDialog = async (media: MediaItemShared) => {
setIsSelectMediaDialogOpen(false);
if (mediaTarget === "sightPreview") {
await linkPreviewMedia(media.id);
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
await createLinkWithRightArticle(media, currentRightArticle.id);
}
setMediaTarget(null);
};
const handleMediaUploaded = async (media: MediaItemShared) => {
// After UploadMediaDialog finishes
setUploadMediaOpen(false);
setFileToUpload(null);
if (mediaTarget === "sightPreview") {
linkPreviewMedia(media.id);
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
await createLinkWithRightArticle(media, currentRightArticle.id);
}
setMediaTarget(null); // Reset target
};
return (
@ -59,7 +196,7 @@ export const CreateRightTab = observer(
display: "flex",
flexDirection: "column",
height: "100%",
minHeight: "calc(100vh - 200px)", // Adjust as needed
minHeight: "calc(100vh - 200px)",
gap: 2,
paddingBottom: "70px", // Space for the save button
position: "relative",
@ -68,336 +205,389 @@ export const CreateRightTab = observer(
<BackButton />
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
{/* Left Column: Navigation & Article List */}
<Box className="flex flex-col w-[75%] 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
onClick={() => {
setType("media");
// setActiveArticleIndex(null); // Optional: deselect article when switching to general media view
}}
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 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300 ${
type === "media"
? "bg-green-300 font-semibold"
: "bg-green-200"
}`}
>
<Typography>Предпросмотр медиа</Typography>
</Box>
{sight[language].right.map((article, index) => (
{sight[language].right.map((article, artIdx) => (
<Box
key={index}
className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300"
onClick={() => {
handleSelectArticle(index);
setType("article");
}}
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>{article.heading}</Typography>
<Typography noWrap title={article.heading}>
{article.heading || "Без названия"}
</Typography>
</Box>
))}
</Box>
<button
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center"
onClick={handleClick}
className="w-10 h-10 bg-blue-500 rounded-full absolute bottom-5 left-5 flex items-center justify-center hover:bg-blue-600"
onClick={handleClickMenu}
aria-controls={openMenu ? "add-article-menu" : undefined}
aria-haspopup="true"
aria-expanded={openMenu ? "true" : undefined}
>
<Plus size={20} color="white" />
</button>
<Menu
id="basic-menu"
id="add-article-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
"aria-labelledby": "basic-button",
}}
sx={{
mt: 1,
}}
open={openMenu}
onClose={handleCloseMenu}
MenuListProps={{ "aria-labelledby": "basic-button" }}
sx={{ mt: 1 }}
>
<MenuItem
onClick={() => {
createNewRightArticle();
handleClose();
}}
>
<MenuItem onClick={handleCreateNewLocalArticle}>
<Typography>Создать новую</Typography>
</MenuItem>
<MenuItem
onClick={() => {
setArticleDialogOpen(true);
handleClose();
setSelectArticleDialogOpen(true);
handleCloseMenu();
}}
>
<Typography>Выбрать существующую статью</Typography>
</MenuItem>
</Menu>
</Box>
{type === "article" && (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3">
{activeArticleIndex !== null && (
<>
<Box className="flex justify-end gap-2 mb-3">
<Button variant="contained" color="primary">
Открепить
</Button>
<Button variant="contained" color="success">
Удалить
</Button>
</Box>
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
{/* Левая колонка: Редактирование */}
<Box
sx={{
flex: 2,
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<TextField
label="Название информации"
value={
sight[language].right[activeArticleIndex]
.heading
}
onChange={(e) =>
updateRightArticleInfo(
activeArticleIndex,
language,
e.target.value,
sight[language].right[activeArticleIndex].body
)
}
variant="outlined"
fullWidth
/>
<ReactMarkdownEditor
value={
sight[language].right[activeArticleIndex].body
}
onChange={(value) =>
updateRightArticleInfo(
activeArticleIndex,
language,
sight[language].right[activeArticleIndex]
.heading,
value
)
}
/>
</Box>
{/* Блок МЕДИА для статьи */}
{/* <Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
<Typography variant="h6" gutterBottom>
МЕДИА
</Typography>
{data.left.media ? (
<Box sx={{ mb: 1 }}>
<img
src={data.left.media.filename}
alt="Selected media"
style={{
maxWidth: "100%",
maxHeight: "150px",
objectFit: "contain",
}}
/>
</Box>
) : (
<Box
sx={{
width: "100%",
height: 100,
backgroundColor: "grey.100",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 1,
mb: 1,
border: "2px dashed",
borderColor: "grey.300",
}}
>
<Typography color="text.secondary">Нет медиа</Typography>
</Box>
)}
<Button
variant="contained"
startIcon={<ImagePlus size={18} />}
onClick={handleOpenMediaDialog}
>
Выбрать/Загрузить медиа
</Button>
{data.left.media && (
{/* Main content area: Article Editor or Sight Media Preview */}
{type === "article" && currentRightArticle ? (
<Box className="w-[80%] border border-gray-300 rounded-2xl p-3 flex flex-col gap-2 overflow-hidden">
<Box className="flex justify-end gap-2 mb-1 flex-shrink-0">
<Button
variant="outlined"
color="warning"
size="small"
onClick={() => {
if (currentRightArticle) {
unlinkRightAritcle(currentRightArticle.id); // Corrected function name
setActiveArticleIndex(null);
setType("media");
}
}}
>
Открепить
</Button>
<Button
variant="contained"
color="error"
size="small"
sx={{ ml: 1 }}
onClick={() =>
updateSightInfo(
languageStore.language,
{
left: {
heading: data.left.heading,
body: data.left.body,
media: null,
},
},
false
onClick={async () => {
if (
currentRightArticle &&
window.confirm(
`Удалить статью "${currentRightArticle.heading}" окончательно?`
)
) {
try {
await deleteRightArticle(currentRightArticle.id);
setActiveArticleIndex(null);
setType("media");
toast.success("Статья удалена");
} catch {
toast.error("Не удалось удалить статью");
}
}
}}
>
Удалить
</Button>
</Box>
<Box
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
gap: 2,
overflowY: "auto",
pt: 7,
}}
>
<TextField
label="Название информации (правый виджет)"
value={currentRightArticle.heading}
onChange={(e) =>
activeArticleIndex !== null &&
updateRightArticleInfo(
activeArticleIndex,
language,
e.target.value,
currentRightArticle.body
)
}
>
Удалить медиа
</Button>
)}
</Paper> */}
</Box>
</>
variant="outlined"
fullWidth
/>
<Box sx={{ minHeight: 200, flexGrow: 1 }}>
<ReactMarkdownEditor
value={currentRightArticle.body}
onChange={(mdValue) =>
activeArticleIndex !== null &&
updateRightArticleInfo(
activeArticleIndex,
language,
currentRightArticle.heading,
mdValue || ""
)
}
/>
</Box>
<MediaArea
articleId={currentRightArticle.id} // Needs a real ID
mediaIds={currentRightArticle.media || []}
onFilesDrop={(files) => {
if (files.length > 0) {
setFileToUpload(files[0]);
setMediaTarget("rightArticle");
handleOpenUploadMedia();
}
}}
deleteMedia={deleteRightArticleMedia}
setSelectMediaDialogOpen={() =>
handleOpenSelectMediaDialog("rightArticle")
}
/>
</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">
{sight.preview_media && (
<>
<Box className="absolute top-4 right-4">
<button
className="w-10 h-10 flex items-center justify-center"
onClick={unlinkPreviewMedia}
>
<X size={20} color="red" />
</button>
</Box>
<MediaViewer
media={{
id: sight.preview_media || "",
media_type: 1,
filename: sight.preview_media || "",
}}
/>
</>
)}
{!sight.preview_media && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
)}
</Box>
)}
</Box>
)}
{type === "media" && (
) : (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center">
<MediaViewer
media={{
id: sight.preview_media || "",
media_type: 1,
filename: sight.preview_media || "",
}}
/>
<Typography variant="h6" color="text.secondary">
Выберите статью слева или секцию "Предпросмотр медиа"
</Typography>
</Box>
)}
</Box>
</Box>
{/* Right Column: Live Preview */}
<Box className="w-[25%] mr-10">
{activeArticleIndex !== null && (
{type === "article" && currentRightArticle && (
<Paper
className="flex-1 flex flex-col rounded-2xl"
elevation={2}
sx={{ height: "75vh", overflow: "hidden" }}
>
<Box
className="rounded-2xl overflow-hidden"
sx={{
width: "100%",
height: "75vh",
background: "#877361",
borderColor: "grey.300",
height: "100%",
background: "#877361", // Theme background
display: "flex",
flexDirection: "column",
}}
>
{type === "media" ? (
{currentRightArticle.media &&
currentRightArticle.media.length > 0 ? (
<MediaViewer media={currentRightArticle.media[0]} />
) : (
<Box
sx={{
width: "100%",
height: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Typography color="white">Загрузка...</Typography>
<ImagePlus size={48} color="white" />
</Box>
) : (
<>
<Box
sx={{
width: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<ImagePlus size={48} color="white" />
</Box>
<Box
sx={{
width: "100%",
minHeight: "70px",
background: "#877361",
display: "flex",
flexShrink: 0,
alignItems: "center",
borderBottom: "1px solid rgba(255,255,255,0.1)",
px: 2,
}}
>
<Typography variant="h6" color="white">
{sight[language].right[activeArticleIndex]
.heading || "Выберите статью"}
</Typography>
</Box>
<Box
sx={{
px: 2,
flexGrow: 1,
overflowY: "auto",
backgroundColor: "#877361",
color: "white",
py: 1,
}}
>
{sight[language].right[activeArticleIndex].body ? (
<ReactMarkdownComponent
value={
sight[language].right[activeArticleIndex].body
}
/>
) : (
<Typography
color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }}
>
Предпросмотр статьи появится здесь
</Typography>
)}
</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,
}}
>
<Typography
variant="h6"
color="white"
noWrap
title={currentRightArticle.heading}
>
{currentRightArticle.heading || "Заголовок"}
</Typography>
</Box>
<Box
sx={{
px: 2,
py: 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 ? (
<ReactMarkdownComponent
value={currentRightArticle.body}
/>
) : (
<Typography
color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }}
>
Содержимое статьи...
</Typography>
)}
</Box>
</Box>
</Paper>
)}
{/* Optional: Preview for sight.preview_media when type === "media" */}
{type === "media" && sight.preview_media && (
<Paper
className="flex-1 flex flex-col rounded-2xl"
elevation={2}
sx={{ height: "75vh", overflow: "hidden" }}
>
<Box
sx={{
width: "100%",
height: "100%",
background: "#877361",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<MediaViewer
media={{
id: sight.preview_media,
filename: sight.preview_media,
media_type: 1,
}}
/>
</Box>
</Paper>
)}
</Box>
</Box>
{/* Sticky Save Button Footer */}
<Box
sx={{
position: "absolute",
bottom: 0,
left: 0, // ensure it spans from left
right: 0,
padding: 2,
backgroundColor: "background.paper", // Ensure button is visible
width: "100%", // Cover the full width to make it a sticky footer
backgroundColor: "background.paper",
borderTop: "1px solid", // Add a subtle top border
borderColor: "divider", // Use theme's divider color
width: "100%",
display: "flex",
justifyContent: "flex-end",
boxShadow: "0 -2px 5px rgba(0,0,0,0.1)", // Optional shadow
}}
>
<Button variant="contained" color="success" onClick={handleSave}>
Сохранить изменения
<Button
variant="contained"
color="success"
onClick={handleSave}
size="large"
>
Сохранить достопримечательность
</Button>
</Box>
</Box>
{/*
{/* Modals */}
<SelectArticleModal
open={openedType === "article"}
onClose={handleCloseSelectModal}
onSelectArticle={handleSelectArticle}
linkedArticleIds={linkedArticleIds}
/> */}
<SelectArticleModal
open={articleDialogOpen}
onClose={() => setArticleDialogOpen(false)}
onSelectArticle={handleSelectArticle}
open={selectArticleDialogOpen}
onClose={() => setSelectArticleDialogOpen(false)}
onSelectArticle={handleSelectExistingArticleAndLink}
// Pass IDs of already linked/added right articles to exclude them from selection
linkedArticleIds={sight[language].right.map((article) => article.id)}
/>
<UploadMediaDialog
open={uploadMediaOpen} // From store
onClose={() => {
setUploadMediaOpen(false);
setFileToUpload(null); // Clear file if dialog is closed without upload
setMediaTarget(null);
}}
afterUpload={handleMediaUploaded} // This will use the mediaTarget
/>
<SelectMediaDialog
open={isSelectMediaDialogOpen}
onClose={() => {
setIsSelectMediaDialogOpen(false);
setMediaTarget(null);
}}
onSelectMedia={handleMediaSelectedFromDialog}
/>
</TabPanel>
);
}