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

@ -6,7 +6,6 @@ const authInstance = axios.create({
}); });
authInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { authInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
console.log(config);
config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`; config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`;
config.headers["X-Language"] = languageStore.language ?? "ru"; config.headers["X-Language"] = languageStore.language ?? "ru";
return config; return config;

View File

@ -21,12 +21,13 @@ import { MediaViewer } from "@widgets";
interface SelectMediaDialogProps { interface SelectMediaDialogProps {
open: boolean; // Corrected prop name open: boolean; // Corrected prop name
onClose: () => void; onClose: () => void;
onSelectMedia: (media: { onSelectMedia?: (media: {
id: string; id: string;
filename: string; filename: string;
media_name?: string; media_name?: string;
media_type: number; media_type: number;
}) => void; // Renamed from onSelectArticle }) => void; // Renamed from onSelectArticle
onSelectForSightMedia?: (mediaId: string) => void;
linkedMediaIds?: string[]; // Renamed from linkedArticleIds, assuming it refers to media already in use linkedMediaIds?: string[]; // Renamed from linkedArticleIds, assuming it refers to media already in use
} }
@ -35,6 +36,7 @@ export const SelectMediaDialog = observer(
open, // Corrected prop name open, // Corrected prop name
onClose, onClose,
onSelectMedia, // Renamed prop onSelectMedia, // Renamed prop
onSelectForSightMedia,
linkedMediaIds = [], // Default to empty array if not provided, renamed linkedMediaIds = [], // Default to empty array if not provided, renamed
}: SelectMediaDialogProps) => { }: SelectMediaDialogProps) => {
const { media, getMedia } = mediaStore; const { media, getMedia } = mediaStore;
@ -55,8 +57,12 @@ export const SelectMediaDialog = observer(
if (hoveredMediaId) { if (hoveredMediaId) {
const mediaItem = media.find((m) => m.id === hoveredMediaId); const mediaItem = media.find((m) => m.id === hoveredMediaId);
if (mediaItem) { if (mediaItem) {
if (onSelectForSightMedia) {
onSelectForSightMedia(mediaItem.id);
} else if (onSelectMedia) {
onSelectMedia(mediaItem); onSelectMedia(mediaItem);
} }
}
onClose(); onClose();
} }
} }
@ -114,7 +120,11 @@ export const SelectMediaDialog = observer(
key={mediaItem.id} key={mediaItem.id}
onClick={() => setHoveredMediaId(mediaItem.id)} // Call onSelectMedia onClick={() => setHoveredMediaId(mediaItem.id)} // Call onSelectMedia
onDoubleClick={() => { onDoubleClick={() => {
if (onSelectForSightMedia) {
onSelectForSightMedia(mediaItem.id);
} else if (onSelectMedia) {
onSelectMedia(mediaItem); onSelectMedia(mediaItem);
}
onClose(); onClose();
}} }}
sx={{ sx={{

View File

@ -24,16 +24,22 @@ import { ModelViewer3D } from "@widgets";
interface UploadMediaDialogProps { interface UploadMediaDialogProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
afterUpload: (media: { afterUpload?: (media: {
id: string; id: string;
filename: string; filename: string;
media_name?: string; media_name?: string;
media_type: number; media_type: number;
}) => void; }) => void;
afterUploadSight?: (id: string) => void;
} }
export const UploadMediaDialog = observer( export const UploadMediaDialog = observer(
({ open, onClose, afterUpload }: UploadMediaDialogProps) => { ({
open,
onClose,
afterUpload,
afterUploadSight,
}: UploadMediaDialogProps) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
@ -103,8 +109,12 @@ export const UploadMediaDialog = observer(
mediaName mediaName
); );
if (media) { if (media) {
if (afterUploadSight) {
await afterUploadSight(media.id);
} else if (afterUpload) {
await afterUpload(media); await afterUpload(media);
} }
}
setSuccess(true); setSuccess(true);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to save media"); setError(err instanceof Error ? err.message : "Failed to save media");

View File

@ -22,7 +22,7 @@ type SightLanguageInfo = {
media_type: number; media_type: number;
}[]; }[];
}; };
right: { heading: string; body: string }[]; right: { id: number; heading: string; body: string; media: [] }[];
}; };
type SightCommonInfo = { type SightCommonInfo = {
@ -90,19 +90,43 @@ class CreateSightStore {
makeAutoObservable(this); makeAutoObservable(this);
} }
createNewRightArticle = () => { createNewRightArticle = async () => {
this.sight.ru.right.push({ const articleId = await languageInstance("ru").post("/article", {
heading: "Введите русский заголовок", heading: "Введите русский заголовок",
body: "Введите русский текст", body: "Введите русский текст",
}); });
this.sight.en.right.push({ const { id } = articleId.data;
await languageInstance("en").patch(`/article/${id}`, {
heading: "Enter the English heading", heading: "Enter the English heading",
body: "Enter the English text", body: "Enter the English text",
}); });
this.sight.zh.right.push({ await languageInstance("zh").patch(`/article/${id}`, {
heading: "Введите китайский заголовок", heading: "Введите китайский заголовок",
body: "Введите китайский текст", body: "Введите китайский текст",
}); });
await authInstance.post(`/sight/${this.sight.id}/article`, {
article_id: id,
page_num: this.sight.ru.right.length + 1,
});
this.sight.ru.right.push({
id: id,
heading: "Введите русский заголовок",
body: "Введите русский текст",
media: [],
});
this.sight.en.right.push({
id: id,
heading: "Enter the English heading",
body: "Enter the English text",
media: [],
});
this.sight.zh.right.push({
id: id,
heading: "Введите китайский заголовок",
body: "Введите китайский текст",
media: [],
});
}; };
updateLeftInfo = (language: Language, heading: string, body: string) => { updateLeftInfo = (language: Language, heading: string, body: string) => {
@ -444,6 +468,32 @@ class CreateSightStore {
filename: media.filename, filename: media.filename,
}); });
}; };
unlinkRightAritcle = async (id: number) => {
this.sight.ru.right = this.sight.ru.right.filter(
(article) => article.id !== id
);
this.sight.en.right = this.sight.en.right.filter(
(article) => article.id !== id
);
this.sight.zh.right = this.sight.zh.right.filter(
(article) => article.id !== id
);
};
deleteRightArticle = async (id: number) => {
await authInstance.delete(`/article/${id}`);
this.sight.ru.right = this.sight.ru.right.filter(
(article) => article.id !== id
);
this.sight.en.right = this.sight.en.right.filter(
(article) => article.id !== id
);
this.sight.zh.right = this.sight.zh.right.filter(
(article) => article.id !== id
);
};
} }
export const createSightStore = new CreateSightStore(); export const createSightStore = new CreateSightStore();

View File

@ -11,7 +11,12 @@ export type SightLanguageInfo = {
body: string; body: string;
media: { id: string; media_type: number; filename: string }[]; media: { id: string; media_type: number; filename: string }[];
}; };
right: { heading: string; body: string }[]; right: {
id: number;
heading: string;
body: string;
media: { id: string; media_type: number; filename: string }[];
}[];
}; };
export type SightCommonInfo = { export type SightCommonInfo = {
@ -111,21 +116,42 @@ class EditSightStore {
}; };
getRightArticles = async (id: number) => { getRightArticles = async (id: number) => {
const responseRu = await languageInstance("ru").get(`/sight/${id}/article`); let responseRu = await languageInstance("ru").get(`/sight/${id}/article`);
const responseEn = await languageInstance("en").get(`/sight/${id}/article`); let responseEn = await languageInstance("en").get(`/sight/${id}/article`);
const 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
const fetchMediaForArticles = async (articles: any[]) => {
const articlesWithMedia = [];
for (const article of articles) {
const responseMedia = await authInstance.get(
`/article/${article.id}/media`
);
articlesWithMedia.push({
...article,
media: responseMedia.data,
});
}
return articlesWithMedia;
};
// Fetch media for articles in each language
const ruArticlesWithMedia = await fetchMediaForArticles(responseRu.data);
const enArticlesWithMedia = await fetchMediaForArticles(responseEn.data);
const zhArticlesWithMedia = await fetchMediaForArticles(responseZh.data);
const data = { const data = {
ru: { ru: {
right: responseRu.data, right: ruArticlesWithMedia,
}, },
en: { en: {
right: responseEn.data, right: enArticlesWithMedia,
}, },
zh: { zh: {
right: responseZh.data, right: zhArticlesWithMedia,
}, },
}; };
runInAction(() => { runInAction(() => {
this.sight = { this.sight = {
...this.sight, ...this.sight,
@ -137,7 +163,6 @@ class EditSightStore {
...this.sight.en, ...this.sight.en,
right: data.en.right, right: data.en.right,
}, },
zh: { zh: {
...this.sight.zh, ...this.sight.zh,
right: data.zh.right, right: data.zh.right,
@ -279,8 +304,13 @@ class EditSightStore {
left_article: createdLeftArticleId, left_article: createdLeftArticleId,
}); });
if (this.sight.common.left_article == 0) { for (const language of ["ru", "en", "zh"] as Language[]) {
return; for (const article of this.sight[language].right) {
await languageInstance(language).patch(`/article/${article.id}`, {
heading: article.heading,
body: article.body,
});
}
} }
// await languageInstance("ru").patch( // await languageInstance("ru").patch(
@ -375,6 +405,38 @@ class EditSightStore {
); );
}; };
unlinkRightArticle = async (article_id: number) => {
await authInstance.delete(`/sight/${this.sight.common.id}/article`, {
data: {
article_id: article_id,
},
});
this.sight.ru.right = this.sight.ru.right.filter(
(article) => article.id !== article_id
);
this.sight.en.right = this.sight.en.right.filter(
(article) => article.id !== article_id
);
this.sight.zh.right = this.sight.zh.right.filter(
(article) => article.id !== article_id
);
};
deleteRightArticle = async (article_id: number) => {
this.sight.ru.right = this.sight.ru.right.filter(
(article) => article.id !== article_id
);
this.sight.en.right = this.sight.en.right.filter(
(article) => article.id !== article_id
);
this.sight.zh.right = this.sight.zh.right.filter(
(article) => article.id !== article_id
);
await authInstance.delete(`/article/${article_id}`);
};
uploadMediaOpen = false; uploadMediaOpen = false;
setUploadMediaOpen = (open: boolean) => { setUploadMediaOpen = (open: boolean) => {
this.uploadMediaOpen = open; this.uploadMediaOpen = open;
@ -442,6 +504,168 @@ class EditSightStore {
filename: media.filename, filename: media.filename,
}); });
}; };
unlinkPreviewMedia = async () => {
this.sight.common.preview_media = null;
};
linkPreviewMedia = async (mediaId: string) => {
this.sight.common.preview_media = mediaId;
};
linkArticle = async (article_id: number) => {
const response = await languageInstance("ru").get(`/article/${article_id}`);
const responseEn = await languageInstance("en").get(
`/article/${article_id}`
);
const responseZh = await languageInstance("zh").get(
`/article/${article_id}`
);
const mediaIds = await authInstance.get(`/article/${article_id}/media`);
runInAction(() => {
this.sight.ru.right.push({
id: article_id,
heading: response.data.heading,
body: response.data.body,
media: mediaIds.data,
});
this.sight.en.right.push({
id: article_id,
heading: responseEn.data.heading,
body: responseEn.data.body,
media: mediaIds.data,
});
this.sight.zh.right.push({
id: article_id,
heading: responseZh.data.heading,
body: responseZh.data.body,
media: mediaIds.data,
});
});
};
deleteRightArticleMedia = async (article_id: number, media_id: string) => {
await authInstance.delete(`/article/${article_id}/media`, {
data: {
media_id: media_id,
},
});
this.sight.ru.right = this.sight.ru.right.map((article) => {
if (article.id === article_id) {
article.media = article.media.filter((media) => media.id !== media_id);
}
return article;
});
this.sight.en.right = this.sight.en.right.map((article) => {
if (article.id === article_id) {
article.media = article.media.filter((media) => media.id !== media_id);
}
return article;
});
this.sight.zh.right = this.sight.zh.right.map((article) => {
if (article.id === article_id) {
article.media = article.media.filter((media) => media.id !== media_id);
}
return article;
});
};
createNewRightArticle = async () => {
const articleId = await languageInstance("ru").post("/article", {
heading: "Введите русский заголовок",
body: "Введите русский текст",
});
const { id } = articleId.data;
await languageInstance("en").patch(`/article/${id}`, {
heading: "Enter the English heading",
body: "Enter the English text",
});
await languageInstance("zh").patch(`/article/${id}`, {
heading: "Введите китайский заголовок",
body: "Введите китайский текст",
});
await authInstance.post(`/sight/${this.sight.common.id}/article`, {
article_id: id,
page_num: this.sight.ru.right.length + 1,
});
this.sight.ru.right.push({
id: id,
heading: "Введите русский заголовок",
body: "Введите русский текст",
media: [],
});
this.sight.en.right.push({
id: id,
heading: "Enter the English heading",
body: "Enter the English text",
media: [],
});
this.sight.zh.right.push({
id: id,
heading: "Введите китайский заголовок",
body: "Введите китайский текст",
media: [],
});
};
createLinkWithRightArticle = async (
media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
},
article_id: number
) => {
await authInstance.post(`/article/${article_id}/media`, {
media_id: media.id,
media_order: 1,
});
this.sight.ru.right = this.sight.ru.right.map((article) => {
if (article.id === article_id) {
article.media.unshift({
id: media.id,
media_type: media.media_type,
filename: media.filename,
});
}
return article;
});
this.sight.en.right = this.sight.en.right.map((article) => {
if (article.id === article_id) {
article.media.unshift({
id: media.id,
media_type: media.media_type,
filename: media.filename,
});
}
return article;
});
this.sight.zh.right = this.sight.zh.right.map((article) => {
if (article.id === article_id) {
article.media.unshift({
id: media.id,
media_type: media.media_type,
filename: media.filename,
});
}
return article;
});
};
updateRightArticleInfo = (
index: number,
language: Language,
heading: string,
body: string
) => {
this.sight[language].right[index].heading = heading;
this.sight[language].right[index].body = body;
};
} }
export const editSightStore = new EditSightStore(); export const editSightStore = new EditSightStore();

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, MenuItem,
TextField, TextField,
} from "@mui/material"; } from "@mui/material";
import { BackButton, createSightStore, languageStore, TabPanel } from "@shared"; import {
BackButton,
createSightStore,
languageStore,
SelectArticleModal,
TabPanel,
} from "@shared";
import { import {
LanguageSwitcher, LanguageSwitcher,
ReactMarkdownComponent, ReactMarkdownComponent,
@ -16,6 +22,7 @@ import {
import { ImagePlus, Plus } from "lucide-react"; import { ImagePlus, Plus } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useState } from "react"; import { useState } from "react";
import { MediaViewer } from "../../MediaViewer/index";
// --- RightWidgetTab (Parent) Component --- // --- RightWidgetTab (Parent) Component ---
export const CreateRightTab = observer( export const CreateRightTab = observer(
@ -24,10 +31,11 @@ export const CreateRightTab = observer(
const { sight, createNewRightArticle, updateRightArticleInfo } = const { sight, createNewRightArticle, updateRightArticleInfo } =
createSightStore; createSightStore;
const { language } = languageStore; const { language } = languageStore;
const [articleDialogOpen, setArticleDialogOpen] = useState(false);
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>( const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
null null
); );
const [type, setType] = useState<"article" | "media">("media");
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget); 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 className="flex flex-col gap-3 max-h-[60vh] overflow-y-auto">
<Box <Box
onClick={() => { 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> <Typography>Предпросмотр медиа</Typography>
</Box> </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" className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300"
onClick={() => { onClick={() => {
handleSelectArticle(index); handleSelectArticle(index);
setType("article");
}} }}
> >
<Typography>{article.heading}</Typography> <Typography>{article.heading}</Typography>
@ -113,6 +122,7 @@ export const CreateRightTab = observer(
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
setArticleDialogOpen(true);
handleClose(); handleClose();
}} }}
> >
@ -120,6 +130,7 @@ export const CreateRightTab = observer(
</MenuItem> </MenuItem>
</Menu> </Menu>
</Box> </Box>
{type === "article" && (
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3"> <Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3">
{activeArticleIndex !== null && ( {activeArticleIndex !== null && (
<> <>
@ -146,7 +157,8 @@ export const CreateRightTab = observer(
<TextField <TextField
label="Название информации" label="Название информации"
value={ value={
sight[language].right[activeArticleIndex].heading sight[language].right[activeArticleIndex]
.heading
} }
onChange={(e) => onChange={(e) =>
updateRightArticleInfo( updateRightArticleInfo(
@ -245,6 +257,18 @@ export const CreateRightTab = observer(
</> </>
)} )}
</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> </Box>
<Box className="w-[25%] mr-10"> <Box className="w-[25%] mr-10">
@ -264,7 +288,7 @@ export const CreateRightTab = observer(
flexDirection: "column", flexDirection: "column",
}} }}
> >
{false ? ( {type === "media" ? (
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
@ -368,6 +392,12 @@ export const CreateRightTab = observer(
onSelectArticle={handleSelectArticle} onSelectArticle={handleSelectArticle}
linkedArticleIds={linkedArticleIds} linkedArticleIds={linkedArticleIds}
/> */} /> */}
<SelectArticleModal
open={articleDialogOpen}
onClose={() => setArticleDialogOpen(false)}
onSelectArticle={handleSelectArticle}
linkedArticleIds={sight[language].right.map((article) => article.id)}
/>
</TabPanel> </TabPanel>
); );
} }

View File

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

View File

@ -13,37 +13,63 @@ import {
editSightStore, editSightStore,
languageStore, languageStore,
SelectArticleModal, SelectArticleModal,
SelectMediaDialog,
TabPanel, TabPanel,
UploadMediaDialog,
} from "@shared"; } from "@shared";
import { import {
LanguageSwitcher, LanguageSwitcher,
MediaArea,
MediaAreaForSight,
ReactMarkdownComponent, ReactMarkdownComponent,
ReactMarkdownEditor, ReactMarkdownEditor,
} from "@widgets"; } from "@widgets";
import { ImagePlus, Plus } from "lucide-react"; import { ImagePlus, Plus, X } from "lucide-react";
import { observer } from "mobx-react-lite"; 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";
export const RightWidgetTab = observer( export const RightWidgetTab = observer(
({ value, index }: { value: number; index: number }) => { ({ value, index }: { value: number; index: number }) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); 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(() => { useEffect(() => {
const fetchData = async () => {
if (sight.common.id) { if (sight.common.id) {
getRightArticles(sight.common.id); await getRightArticles(sight.common.id);
} }
};
fetchData();
console.log(sight[language].right);
}, [sight.common.id]); }, [sight.common.id]);
const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>( const [activeArticleIndex, setActiveArticleIndex] = useState<number | null>(
null null
); );
const [isSelectModalOpen, setIsSelectModalOpen] = useState(false); const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
const [isSelectMediaModalOpen, setIsSelectMediaModalOpen] = useState(false);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
}; };
@ -69,16 +95,32 @@ export const RightWidgetTab = observer(
setIsSelectModalOpen(false); setIsSelectModalOpen(false);
}; };
const handleArticleSelect = () => { const handleArticleSelect = (id: number) => {
// TODO: Implement article selection logic linkArticle(id);
handleCloseSelectModal(); 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 () => { const handleSave = async () => {
await updateSight(); await updateSight();
toast.success("Достопримечательность сохранена"); toast.success("Достопримечательность сохранена");
}; };
useEffect(() => {
console.log(sight[language].right);
}, [sight[language].right]);
return ( return (
<TabPanel value={value} index={index}> <TabPanel value={value} index={index}>
<LanguageSwitcher /> <LanguageSwitcher />
@ -101,17 +143,20 @@ export const RightWidgetTab = observer(
<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={() => setMediaType("preview")} onClick={() => 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> <Typography>Предпросмотр медиа</Typography>
</Box> </Box>
{sight[language].right.length > 0 &&
{sight[language].right.map((article, index) => ( sight[language].right.map((article, index) => (
<Box <Box
key={index} key={index}
className="w-full bg-gray-200 p-4 rounded-2xl text-sm cursor-pointer hover:bg-gray-300 transition-all duration-300" 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)} onClick={() => {
handleSelectArticle(index);
setType("article");
}}
> >
<Typography>{article.heading}</Typography> <Typography>{article.heading}</Typography>
</Box> </Box>
@ -142,14 +187,33 @@ export const RightWidgetTab = observer(
</Menu> </Menu>
</Box> </Box>
{type === "article" && (
<Box className="w-[80%] border border-gray-300 rounded-2xl p-3"> <Box className="w-[80%] border border-gray-300 rounded-2xl p-3">
{activeArticleIndex !== null && ( {activeArticleIndex !== null && (
<> <>
<Box className="flex justify-end gap-2 mb-3"> <Box className="flex justify-end gap-2 mb-3">
<Button variant="contained" color="primary"> <Button
variant="contained"
color="primary"
onClick={() => {
unlinkRightArticle(
sight[language].right[activeArticleIndex].id
);
setActiveArticleIndex(null);
}}
>
Открепить Открепить
</Button> </Button>
<Button variant="contained" color="success"> <Button
variant="contained"
color="success"
onClick={() => {
deleteRightArticle(
sight[language].right[activeArticleIndex].id
);
setActiveArticleIndex(null);
}}
>
Удалить Удалить
</Button> </Button>
</Box> </Box>
@ -166,7 +230,8 @@ export const RightWidgetTab = observer(
<TextField <TextField
label="Название информации" label="Название информации"
value={ value={
sight[language].right[activeArticleIndex].heading sight[language].right[activeArticleIndex]
.heading
} }
onChange={(e) => onChange={(e) =>
updateRightArticleInfo( updateRightArticleInfo(
@ -194,19 +259,65 @@ export const RightWidgetTab = observer(
) )
} }
/> />
{/* <MediaArea
articleId={1} <MediaArea
mediaIds={[]} articleId={
deleteMedia={() => {}} 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> </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> </Box>
{type === "article" && (
<Box className="w-[25%] mr-10"> <Box className="w-[25%] mr-10">
{activeArticleIndex !== null && ( {activeArticleIndex !== null && (
<Paper <Paper
@ -224,6 +335,14 @@ export const RightWidgetTab = observer(
flexDirection: "column", flexDirection: "column",
}} }}
> >
{sight[language].right[activeArticleIndex].media.length >
0 ? (
<MediaViewer
media={
sight[language].right[activeArticleIndex].media[0]
}
/>
) : (
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
@ -237,6 +356,7 @@ export const RightWidgetTab = observer(
> >
<ImagePlus size={48} color="white" /> <ImagePlus size={48} color="white" />
</Box> </Box>
)}
<Box <Box
sx={{ sx={{
@ -268,7 +388,9 @@ export const RightWidgetTab = observer(
> >
{sight[language].right[activeArticleIndex].body ? ( {sight[language].right[activeArticleIndex].body ? (
<ReactMarkdownComponent <ReactMarkdownComponent
value={sight[language].right[activeArticleIndex].body} value={
sight[language].right[activeArticleIndex].body
}
/> />
) : ( ) : (
<Typography <Typography
@ -283,6 +405,7 @@ export const RightWidgetTab = observer(
</Paper> </Paper>
)} )}
</Box> </Box>
)}
</Box> </Box>
<Box <Box
@ -303,11 +426,29 @@ export const RightWidgetTab = observer(
</Box> </Box>
</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 <SelectArticleModal
open={isSelectModalOpen} open={isSelectModalOpen}
onClose={handleCloseSelectModal} onClose={handleCloseSelectModal}
onSelectArticle={handleArticleSelect} onSelectArticle={handleArticleSelect}
/> />
<SelectMediaDialog
open={isSelectMediaModalOpen}
onClose={() => setIsSelectMediaModalOpen(false)}
onSelectMedia={handleMediaSelected}
/>
</TabPanel> </TabPanel>
); );
} }

View File

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