Add correct edit right widget

This commit is contained in:
2025-06-03 12:31:47 +03:00
parent 5814e65953
commit f0e03c3a1d
10 changed files with 806 additions and 233 deletions

View File

@ -0,0 +1,107 @@
import { Box, Button } from "@mui/material";
import { editSightStore, SelectMediaDialog, UploadMediaDialog } from "@shared";
import { Upload } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useState, DragEvent, useRef } from "react";
export const MediaAreaForSight = observer(
({
onFilesDrop, // 👈 Проп для обработки загруженных файлов
onFinishUpload,
}: {
onFilesDrop?: (files: File[]) => void;
onFinishUpload?: (mediaId: string) => void;
}) => {
const [selectMediaDialogOpen, setSelectMediaDialogOpen] = useState(false);
const [uploadMediaDialogOpen, setUploadMediaDialogOpen] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const { setFileToUpload } = editSightStore;
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
const files = Array.from(e.dataTransfer.files);
if (files.length && onFilesDrop) {
setFileToUpload(files[0]);
}
setUploadMediaDialogOpen(true);
};
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = () => {
setIsDragging(false);
};
const handleClick = () => {
fileInputRef.current?.click();
};
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
if (files.length && onFilesDrop) {
setFileToUpload(files[0]);
onFilesDrop(files);
setUploadMediaDialogOpen(true);
}
// Сбрасываем значение input, чтобы можно было выбрать тот же файл снова
event.target.value = "";
};
return (
<>
<input
type="file"
ref={fileInputRef}
onChange={handleFileSelect}
accept="image/*,video/*,.glb,.gltf"
multiple
style={{ display: "none" }}
/>
<Box className="w-full flex flex-col items-center justify-center border rounded-md p-4">
<div className="w-full flex flex-col items-center justify-center">
<div
className={`w-full h-40 flex flex-col justify-center items-center text-gray-400 border-dashed border-2 rounded-md border-gray-400 p-4 cursor-pointer hover:bg-gray-50 ${
isDragging ? "bg-blue-100 border-blue-400" : ""
}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={handleClick}
>
<Upload size={32} className="mb-2" />
Перетащите медиа файлы сюда или нажмите для выбора
</div>
<div>или</div>
<Button
variant="contained"
color="primary"
onClick={() => setSelectMediaDialogOpen(true)}
>
Выбрать существующие медиа файлы
</Button>
</div>
</Box>
<UploadMediaDialog
open={uploadMediaDialogOpen}
onClose={() => setUploadMediaDialogOpen(false)}
afterUploadSight={onFinishUpload}
/>
<SelectMediaDialog
open={selectMediaDialogOpen}
onClose={() => setSelectMediaDialogOpen(false)}
onSelectForSightMedia={onFinishUpload}
/>
</>
);
}
);

View File

@ -7,7 +7,13 @@ import {
MenuItem,
TextField,
} from "@mui/material";
import { BackButton, createSightStore, languageStore, TabPanel } from "@shared";
import {
BackButton,
createSightStore,
languageStore,
SelectArticleModal,
TabPanel,
} from "@shared";
import {
LanguageSwitcher,
ReactMarkdownComponent,
@ -16,6 +22,7 @@ import {
import { ImagePlus, Plus } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useState } from "react";
import { MediaViewer } from "../../MediaViewer/index";
// --- RightWidgetTab (Parent) Component ---
export const CreateRightTab = observer(
@ -24,10 +31,11 @@ export const CreateRightTab = observer(
const { sight, createNewRightArticle, updateRightArticleInfo } =
createSightStore;
const { language } = languageStore;
const [articleDialogOpen, setArticleDialogOpen] = 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>) => {
setAnchorEl(event.currentTarget);
@ -66,9 +74,9 @@ export const CreateRightTab = observer(
<Box className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto">
<Box
onClick={() => {
// setMediaType("preview");
setType("media");
}}
className="w-full bg-gray-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>
</Box>
@ -79,6 +87,7 @@ export const CreateRightTab = observer(
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");
}}
>
<Typography>{article.heading}</Typography>
@ -113,6 +122,7 @@ export const CreateRightTab = observer(
</MenuItem>
<MenuItem
onClick={() => {
setArticleDialogOpen(true);
handleClose();
}}
>
@ -120,63 +130,65 @@ export const CreateRightTab = observer(
</MenuItem>
</Menu>
</Box>
<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>
{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
)
}
/>
<Button variant="contained" color="success">
Удалить
</Button>
</Box>
{/* Блок МЕДИА для статьи */}
{/* <Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
<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>
@ -241,10 +253,22 @@ export const CreateRightTab = observer(
</Button>
)}
</Paper> */}
</Box>
</>
)}
</Box>
</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 || "",
}}
/>
</Box>
)}
</Box>
</Box>
<Box className="w-[25%] mr-10">
@ -264,7 +288,7 @@ export const CreateRightTab = observer(
flexDirection: "column",
}}
>
{false ? (
{type === "media" ? (
<Box
sx={{
width: "100%",
@ -368,6 +392,12 @@ export const CreateRightTab = observer(
onSelectArticle={handleSelectArticle}
linkedArticleIds={linkedArticleIds}
/> */}
<SelectArticleModal
open={articleDialogOpen}
onClose={() => setArticleDialogOpen(false)}
onSelectArticle={handleSelectArticle}
linkedArticleIds={sight[language].right.map((article) => article.id)}
/>
</TabPanel>
);
}

View File

@ -313,6 +313,7 @@ export const InformationTab = observer(
/>
</Tooltip>
</Box>
<Box
sx={{
position: "relative",

View File

@ -13,37 +13,63 @@ import {
editSightStore,
languageStore,
SelectArticleModal,
SelectMediaDialog,
TabPanel,
UploadMediaDialog,
} from "@shared";
import {
LanguageSwitcher,
MediaArea,
MediaAreaForSight,
ReactMarkdownComponent,
ReactMarkdownEditor,
} from "@widgets";
import { ImagePlus, Plus } from "lucide-react";
import { ImagePlus, Plus, X } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { MediaViewer } from "../../MediaViewer/index";
export const RightWidgetTab = observer(
({ value, index }: { value: number; index: number }) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const { createNewRightArticle, updateRightArticleInfo } = createSightStore;
const { sight, getRightArticles, updateSight } = editSightStore;
const { language } = languageStore;
const {
sight,
updateRightArticleInfo,
getRightArticles,
updateSight,
unlinkPreviewMedia,
linkPreviewMedia,
unlinkRightArticle,
deleteRightArticle,
linkArticle,
deleteRightArticleMedia,
createLinkWithRightArticle,
setFileToUpload,
createNewRightArticle,
} = editSightStore;
const [uploadMediaOpen, setUploadMediaOpen] = useState(false);
const { language } = languageStore;
const [type, setType] = useState<"article" | "media">("media");
useEffect(() => {
if (sight.common.id) {
getRightArticles(sight.common.id);
}
const fetchData = async () => {
if (sight.common.id) {
await getRightArticles(sight.common.id);
}
};
fetchData();
console.log(sight[language].right);
}, [sight.common.id]);
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
null
);
const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
const [isSelectMediaModalOpen, setIsSelectMediaModalOpen] = useState(false);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
@ -69,16 +95,32 @@ export const RightWidgetTab = observer(
setIsSelectModalOpen(false);
};
const handleArticleSelect = () => {
// TODO: Implement article selection logic
const handleArticleSelect = (id: number) => {
linkArticle(id);
handleCloseSelectModal();
};
const handleMediaSelected = async (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
await createLinkWithRightArticle(
media,
sight[language].right[activeArticleIndex || 0].id
);
};
const handleSave = async () => {
await updateSight();
toast.success("Достопримечательность сохранена");
};
useEffect(() => {
console.log(sight[language].right);
}, [sight[language].right]);
return (
<TabPanel value={value} index={index}>
<LanguageSwitcher />
@ -99,23 +141,26 @@ export const RightWidgetTab = observer(
<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="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
// onClick={() => setMediaType("preview")}
className="w-full bg-gray-200 p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300"
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"
>
<Typography>Предпросмотр медиа</Typography>
</Box>
{sight[language].right.map((article, index) => (
<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)}
>
<Typography>{article.heading}</Typography>
</Box>
))}
{sight[language].right.length > 0 &&
sight[language].right.map((article, index) => (
<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");
}}
>
<Typography>{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"
@ -142,147 +187,225 @@ export const RightWidgetTab = observer(
</Menu>
</Box>
<Box className="w-[80%] 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,
maxHeight: "70%",
}}
>
<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
)
}
/>
{/* <MediaArea
articleId={1}
mediaIds={[]}
deleteMedia={() => {}}
/> */}
{type === "article" && (
<Box className="w-[80%] border border-gray-300 rounded-2xl p-3">
{activeArticleIndex !== null && (
<>
<Box className="flex justify-end gap-2 mb-3">
<Button
variant="contained"
color="primary"
onClick={() => {
unlinkRightArticle(
sight[language].right[activeArticleIndex].id
);
setActiveArticleIndex(null);
}}
>
Открепить
</Button>
<Button
variant="contained"
color="success"
onClick={() => {
deleteRightArticle(
sight[language].right[activeArticleIndex].id
);
setActiveArticleIndex(null);
}}
>
Удалить
</Button>
</Box>
</Box>
</>
)}
</Box>
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
<Box
sx={{
flex: 2,
display: "flex",
flexDirection: "column",
gap: 2,
maxHeight: "70%",
}}
>
<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
)
}
/>
<MediaArea
articleId={
sight[language].right[activeArticleIndex].id
}
mediaIds={
sight[language].right[activeArticleIndex].media
}
onFilesDrop={(files) => {
setFileToUpload(files[0]);
setUploadMediaOpen(true);
}}
deleteMedia={deleteRightArticleMedia}
setSelectMediaDialogOpen={() => {
setIsSelectMediaModalOpen(true);
}}
/>
</Box>
</Box>
</>
)}
</Box>
)}
{type === "media" && (
<Box className="w-[80%] border border-gray-300 rounded-2xl relative">
{sight.common.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.common.preview_media || "",
media_type: 1,
filename: sight.common.preview_media || "",
}}
/>
</>
)}
{!sight.common.preview_media && (
<MediaAreaForSight
onFinishUpload={(mediaId) => {
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
/>
)}
</Box>
)}
</Box>
</Box>
<Box className="w-[25%] mr-10">
{activeArticleIndex !== null && (
<Paper
className="flex-1 flex flex-col rounded-2xl"
elevation={2}
>
<Box
className="rounded-2xl overflow-hidden"
sx={{
width: "100%",
height: "75vh",
background: "#877361",
borderColor: "grey.300",
display: "flex",
flexDirection: "column",
}}
{type === "article" && (
<Box className="w-[25%] mr-10">
{activeArticleIndex !== null && (
<Paper
className="flex-1 flex flex-col rounded-2xl"
elevation={2}
>
<Box
className="rounded-2xl overflow-hidden"
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",
height: "75vh",
background: "#877361",
borderColor: "grey.300",
display: "flex",
flexShrink: 0,
alignItems: "center",
borderBottom: "1px solid rgba(255,255,255,0.1)",
px: 2,
flexDirection: "column",
}}
>
<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}
{sight[language].right[activeArticleIndex].media.length >
0 ? (
<MediaViewer
media={
sight[language].right[activeArticleIndex].media[0]
}
/>
) : (
<Typography
color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }}
<Box
sx={{
width: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
Предпросмотр статьи появится здесь
</Typography>
<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>
</Box>
</Paper>
)}
</Box>
</Paper>
)}
</Box>
)}
</Box>
<Box
@ -303,11 +426,29 @@ export const RightWidgetTab = observer(
</Box>
</Box>
<UploadMediaDialog
open={uploadMediaOpen}
onClose={() => setUploadMediaOpen(false)}
afterUpload={async (media) => {
setUploadMediaOpen(false);
setFileToUpload(null);
await createLinkWithRightArticle(
media,
sight[language].right[activeArticleIndex || 0].id
);
}}
/>
<SelectArticleModal
open={isSelectModalOpen}
onClose={handleCloseSelectModal}
onSelectArticle={handleArticleSelect}
/>
<SelectMediaDialog
open={isSelectMediaModalOpen}
onClose={() => setIsSelectMediaModalOpen(false)}
onSelectMedia={handleMediaSelected}
/>
</TabPanel>
);
}

View File

@ -10,3 +10,4 @@ export * from "./SightsTable";
export * from "./MediaViewer";
export * from "./MediaArea";
export * from "./ModelViewer3D";
export * from "./MediaAreaForSight";