WhiteNightsAdminPanel/src/shared/modals/SelectArticleDialog/index.tsx
2025-06-04 22:03:03 +03:00

316 lines
9.6 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.

import { articlesStore, authInstance, languageStore } from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
List,
ListItemButton,
ListItemText,
Paper,
Box,
Typography,
InputAdornment,
} from "@mui/material";
import { ImagePlus, Search } from "lucide-react";
import { MediaViewer, ReactMarkdownComponent } from "@widgets";
interface SelectArticleModalProps {
open: boolean;
onClose: () => void;
onSelectArticle: (
articleId: number,
heading: string,
body: string,
media: { id: string; media_type: number; filename: string }[]
) => void;
linkedArticleIds?: number[]; // Add optional prop for linked articles
}
export const SelectArticleModal = observer(
({
open,
onClose,
onSelectArticle,
linkedArticleIds = [],
}: SelectArticleModalProps) => {
const { articles, getArticle, getArticleMedia } = articlesStore;
const [searchQuery, setSearchQuery] = useState("");
const [selectedArticleId, setSelectedArticleId] = useState<number | null>(
null
);
const [isLoading, setIsLoading] = useState(false);
// Reset selection when modal opens/closes
useEffect(() => {
if (open) {
setSelectedArticleId(null);
articlesStore.articleData = null;
articlesStore.articleMedia = null;
}
}, [open]);
useEffect(() => {
const handleKeyPress = async (event: KeyboardEvent) => {
if (event.key.toLowerCase() === "enter") {
event.preventDefault();
if (selectedArticleId) {
const media = await authInstance.get(
`/article/${selectedArticleId}/media`
);
onSelectArticle(
selectedArticleId,
articlesStore.articleData?.heading || "",
articlesStore.articleData?.body || "",
media.data || []
);
onClose();
setSelectedArticleId(null);
}
}
};
window.addEventListener("keydown", handleKeyPress);
return () => {
window.removeEventListener("keydown", handleKeyPress);
};
}, [selectedArticleId, onSelectArticle, onClose]);
const handleArticleClick = async (articleId: number) => {
setSelectedArticleId(articleId);
setIsLoading(true);
try {
await Promise.all([
getArticle(Number(articleId)),
getArticleMedia(Number(articleId)),
]);
} catch (error) {
console.error("Failed to fetch article data:", error);
// Reset article data on error
articlesStore.articleData = null;
articlesStore.articleMedia = null;
} finally {
setIsLoading(false);
}
};
const filteredArticles = articles[languageStore.language].filter(
(article) => !linkedArticleIds.includes(article.id)
);
// .filter((article) =>
// article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
// );
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="lg"
fullWidth
PaperProps={{
sx: {
minHeight: "80vh",
maxHeight: "90vh",
},
}}
>
<DialogTitle>Выберите существующую статью</DialogTitle>
<DialogContent
className="flex gap-4"
dividers
sx={{
height: "600px",
display: "flex",
flexDirection: "row",
alignItems: "center",
p: 2,
}}
>
<Paper className="w-[66%] flex flex-col h-full" elevation={2}>
<TextField
fullWidth
placeholder="Поиск статей..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
sx={{ mb: 2, mt: 1, px: 2 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search size={20} />
</InputAdornment>
),
}}
/>
<List sx={{ flexGrow: 1, overflowY: "auto", px: 2 }}>
{filteredArticles.length === 0 ? (
<Typography
variant="body2"
color="text.secondary"
sx={{ p: 2, textAlign: "center" }}
>
{searchQuery ? "Статьи не найдены" : "Нет доступных статей"}
</Typography>
) : (
// @ts-ignore
filteredArticles.map((article) => (
<ListItemButton
key={article.id}
onClick={() => handleArticleClick(article.id)}
onDoubleClick={async () => {
const media = await authInstance.get(
`/article/${article.id}/media`
);
onSelectArticle(
article.id,
article.heading,
article.body,
media.data
);
}}
selected={selectedArticleId === article.id}
disabled={isLoading}
sx={{
borderRadius: 1,
mb: 0.5,
"&:hover": {
backgroundColor: "action.hover",
},
"&.Mui-selected": {
backgroundColor: "primary.main",
color: "primary.contrastText",
"&:hover": {
backgroundColor: "primary.dark",
},
},
}}
>
<ListItemText
primary={article.service_name}
primaryTypographyProps={{
fontWeight:
selectedArticleId === article.id ? "bold" : "normal",
}}
/>
</ListItemButton>
))
)}
</List>
</Paper>
<Paper
elevation={3}
sx={{
width: "100%",
minWidth: 320,
maxWidth: 310,
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
padding: 0,
margin: "0px auto",
display: "flex",
flexDirection: "column",
}}
>
<Box
sx={{
width: "100%",
height: 175,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "3px",
}}
>
{articlesStore.articleMedia ? (
<MediaViewer
media={{
id: articlesStore.articleMedia.id,
media_type: articlesStore.articleMedia.media_type,
filename: articlesStore.articleMedia.filename,
}}
/>
) : (
<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%",
}}
>
{articlesStore.articleData?.heading || "Название cтатьи"}
</Typography>
</Box>
{articlesStore.articleData?.body && (
<Box
sx={{
padding: 1,
maxHeight: "200px",
overflowY: "scroll",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
}}
>
<ReactMarkdownComponent
value={articlesStore.articleData?.body || "Описание"}
/>
</Box>
)}
</Paper>
</DialogContent>
<DialogActions sx={{ p: 2 }}>
<Button onClick={onClose}>Отмена</Button>
<Button
variant="contained"
onClick={async () => {
if (selectedArticleId) {
const media = await authInstance.get(
`/article/${selectedArticleId}/media`
);
onSelectArticle(
selectedArticleId,
articlesStore.articleData?.heading || "",
articlesStore.articleData?.body || "",
media.data
);
onClose();
setSelectedArticleId(null);
}
}}
disabled={!selectedArticleId || isLoading}
>
Выбрать
</Button>
</DialogActions>
</Dialog>
);
}
);