Files
WhiteNightsAdminPanel/src/widgets/SightTabs/LeftWidgetTab/index.tsx
2025-10-22 02:55:04 +03:00

467 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @widgets/LeftWidgetTab.tsx
import { Box, Button, TextField, Paper, Typography } from "@mui/material";
import {
BackButton,
TabPanel,
languageStore,
SelectMediaDialog,
editSightStore,
SelectArticleModal,
UploadMediaDialog,
Language,
articlesStore,
} from "@shared";
import {
LanguageSwitcher,
ReactMarkdownComponent,
ReactMarkdownEditor,
MediaArea,
MediaViewer,
DeleteModal,
} from "@widgets";
import { Trash2, ImagePlus, Unlink, Plus, Save, Search } from "lucide-react";
import { useState, useCallback } from "react";
import { observer } from "mobx-react-lite";
import { toast } from "react-toastify";
export const LeftWidgetTab = observer(
({ value, index }: { value: number; index: number }) => {
const {
sight,
updateSightInfo,
unlinkLeftArticle,
updateSight,
deleteLeftArticle,
createLeftArticle,
deleteMedia,
uploadMediaOpen,
setUploadMediaOpen,
setFileToUpload,
createLinkWithArticle,
} = editSightStore;
const { language } = languageStore;
const data = sight[language];
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const token = localStorage.getItem("token");
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
useState(false);
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
useState(false);
const handleDeleteLeftArticle = useCallback(() => {
deleteLeftArticle(sight.common.left_article);
setIsDeleteModalOpen(false);
}, [deleteLeftArticle, sight.common.left_article]);
const handleMediaSelected = useCallback(
async (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
await createLinkWithArticle(media);
setIsSelectMediaDialogOpen(false);
},
[createLinkWithArticle]
);
const handleCloseMediaDialog = useCallback(() => {
setIsSelectMediaDialogOpen(false);
}, []);
const handleCloseArticleDialog = useCallback(() => {
setIsSelectArticleDialogOpen(false);
}, []);
const handleSelectArticle = useCallback(
async (
articleId: number
// heading: string,
// body: string,
// media: { id: string; media_type: number; filename: string }[]
) => {
setIsSelectArticleDialogOpen(false);
const ruArticle = await articlesStore.getArticle(articleId, "ru");
const enArticle = await articlesStore.getArticle(articleId, "en");
const zhArticle = await articlesStore.getArticle(articleId, "zh");
updateSightInfo("ru", {
left: {
heading: ruArticle.data.heading,
body: ruArticle.data.body,
media: ruArticle.data.media || [],
},
});
updateSightInfo("en", {
left: {
heading: enArticle.data.heading,
body: enArticle.data.body,
media: enArticle.data.media || [],
},
});
updateSightInfo("zh", {
left: {
heading: zhArticle.data.heading,
body: zhArticle.data.body,
media: zhArticle.data.media || [],
},
});
updateSightInfo(
languageStore.language,
{
left_article: articleId,
},
true
);
},
[]
);
return (
<>
<TabPanel value={value} index={index}>
<LanguageSwitcher />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 3,
paddingBottom: "70px",
position: "relative",
}}
>
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
<BackButton />
<h1 className="text-3xl break-words">{sight[language].name}</h1>
</div>
<Paper
elevation={2}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
paddingX: 2.5,
paddingY: 1.5,
borderRadius: 2,
border: "1px solid",
borderColor: "divider",
}}
>
<Typography variant="h6">Левая статья</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
{sight.common.left_article ? (
<>
<Button
variant="contained"
color="primary"
size="small"
style={{ transition: "0" }}
startIcon={<Unlink size={18} />}
onClick={() => {
unlinkLeftArticle();
toast.success("Статья откреплена");
}}
>
Открепить
</Button>
<Button
variant="outlined"
color="error"
style={{ transition: "0" }}
startIcon={<Trash2 size={18} />}
size="small"
onClick={() => {
setIsDeleteModalOpen(true);
}}
>
Удалить
</Button>
</>
) : (
<>
<Button
variant="contained"
color="primary"
size="small"
startIcon={<Search color="white" size={18} />}
onClick={() => setIsSelectArticleDialogOpen(true)}
>
Выбрать статью
</Button>
<Button
variant="contained"
color="primary"
size="small"
style={{ transition: "0" }}
startIcon={<Plus color="white" size={18} />}
onClick={() => {
createLeftArticle();
toast.success("Статья создана");
}}
>
Создать статью
</Button>
</>
)}
</Box>
</Paper>
{sight.common.left_article > 0 && (
<Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
<Box
sx={{
flex: 2,
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<TextField
label="Название информации"
value={data?.left?.heading}
onChange={(e) =>
updateSightInfo(languageStore.language, {
left: {
heading: e.target.value,
body: sight[languageStore.language].left.body,
media: data.left.media,
},
})
}
variant="outlined"
fullWidth
/>
<ReactMarkdownEditor
value={data?.left?.body}
onChange={(value: any) =>
updateSightInfo(languageStore.language, {
left: {
heading: sight[languageStore.language].left.heading,
body: value,
media: data.left.media,
},
})
}
/>
<MediaArea
articleId={sight.common.left_article}
mediaIds={data.left.media}
deleteMedia={deleteMedia}
setSelectMediaDialogOpen={setIsSelectMediaDialogOpen}
onFilesDrop={(files) => {
setFileToUpload(files[0]);
setUploadMediaOpen(true);
}}
/>
</Box>
<Box
sx={{
flex: 1,
display: "flex",
flexDirection: "column",
maxWidth: "320px",
gap: 0.5,
}}
>
<Paper
elevation={3}
sx={{
width: "100%",
minWidth: 320,
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
overflowY: "auto",
display: "flex",
flexDirection: "column",
borderRadius: "10px",
}}
>
<Box
sx={{
overflow: "hidden",
width: "100%",
minHeight: 100,
padding: "3px",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
{data.left.media.length > 0 ? (
<>
<MediaViewer
media={{
id: data.left.media[0].id,
media_type: data.left.media[0].media_type,
filename: data.left.media[0].filename,
}}
fullWidth
/>
{sight.common.watermark_lu && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
sight.common.watermark_lu
}/download?token=${token}`}
alt="preview"
className="absolute top-4 left-4 z-10"
style={{
width: "30px",
height: "30px",
objectFit: "contain",
}}
/>
)}
{sight.common.watermark_rd && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
sight.common.watermark_rd
}/download?token=${token}`}
alt="preview"
className="absolute bottom-4 right-4 z-10"
style={{
width: "30px",
height: "30px",
objectFit: "contain",
}}
/>
)}
</>
) : (
<ImagePlus size={48} color="white" />
)}
</Box>
<Box
sx={{
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
color: "white",
margin: "5px 0px 5px 0px",
display: "flex",
flexDirection: "column",
gap: 1,
padding: 1,
}}
>
<Typography
variant="h5"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
}}
>
{data?.left?.heading || "Название информации"}
</Typography>
<Typography
variant="h6"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "18px",
lineHeight: "120%",
}}
>
{sight[language as Language].address}
</Typography>
</Box>
{data?.left?.body && (
<Box
sx={{
padding: 1,
maxHeight: "300px",
overflowY: "auto",
width: "100%",
"&::-webkit-scrollbar": {
display: "none",
},
"&": {
scrollbarWidth: "none",
},
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
}}
>
<ReactMarkdownComponent value={data?.left?.body} />
</Box>
)}
</Paper>
</Box>
</Box>
)}
<Box sx={{ position: "absolute", bottom: 0, right: 0, padding: 2 }}>
<Button
variant="contained"
color="success"
startIcon={<Save color="white" size={18} />}
onClick={async () => {
await updateSight();
toast.success("Достопримечательность сохранена");
}}
>
Сохранить
</Button>
</Box>
</Box>
</TabPanel>
<UploadMediaDialog
open={uploadMediaOpen}
onClose={() => setUploadMediaOpen(false)}
contextObjectName={sight[languageStore.language].name}
contextType="sight"
isArticle={true}
articleName={
sight[languageStore.language].left.heading || "Левая статья"
}
afterUpload={async (media) => {
setUploadMediaOpen(false);
setFileToUpload(null);
await createLinkWithArticle(media);
}}
/>
<SelectMediaDialog
open={isSelectMediaDialogOpen}
onClose={handleCloseMediaDialog}
onSelectMedia={handleMediaSelected}
/>
<SelectArticleModal
open={isSelectArticleDialogOpen}
onClose={handleCloseArticleDialog}
onSelectArticle={handleSelectArticle}
/>
<DeleteModal
open={isDeleteModalOpen}
onDelete={handleDeleteLeftArticle}
onCancel={() => setIsDeleteModalOpen(false)}
/>
</>
);
}
);