fix: Language cache sight

This commit is contained in:
2025-05-31 21:17:27 +03:00
parent 2e6917406e
commit 0d9bbb140f
28 changed files with 2760 additions and 1013 deletions

View File

@@ -1,6 +1,6 @@
import { articlesStore } from "@shared";
import { articlesStore, authStore, Language } from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import {
Dialog,
DialogTitle,
@@ -31,38 +31,56 @@ export const SelectArticleModal = observer(
open,
onClose,
onSelectArticle,
linkedArticleIds = [], // Default to empty array if not provided
linkedArticleIds = [],
}: SelectArticleModalProps) => {
const { articles, getArticle } = articlesStore; // articles here refers to fetched articles from store
const { articles, getArticle, getArticleMedia } = articlesStore;
const [searchQuery, setSearchQuery] = useState("");
const [hoveredArticleId, setHoveredArticleId] = useState<string | null>(
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
null
);
const hoverTimerRef = useRef<NodeJS.Timeout | 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(() => {
if (hoveredArticleId) {
hoverTimerRef.current = setTimeout(() => {
getArticle(hoveredArticleId);
}, 200);
}
return () => {
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key.toLowerCase() === "enter") {
event.preventDefault();
if (selectedArticleId) {
onSelectArticle(selectedArticleId);
onClose();
}
}
};
}, [hoveredArticleId, getArticle]);
const handleArticleHover = (articleId: string) => {
setHoveredArticleId(articleId);
};
window.addEventListener("keydown", handleKeyPress);
return () => {
window.removeEventListener("keydown", handleKeyPress);
};
}, [selectedArticleId, onSelectArticle, onClose]);
const handleArticleLeave = () => {
setHoveredArticleId(null);
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
const handleArticleClick = async (articleId: string) => {
if (selectedArticleId === articleId) return;
setSelectedArticleId(articleId);
setIsLoading(true);
try {
await Promise.all([getArticle(articleId), getArticleMedia(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);
}
};
@@ -72,21 +90,38 @@ export const SelectArticleModal = observer(
article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
);
const token = localStorage.getItem("token");
return (
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
<Dialog
open={open}
onClose={onClose}
maxWidth="lg"
fullWidth
PaperProps={{
sx: {
minHeight: "80vh",
maxHeight: "90vh",
},
}}
>
<DialogTitle>Выберите существующую статью</DialogTitle>
<DialogContent
className="flex gap-4"
dividers // Adds a divider below the title and above the actions
sx={{ height: "600px", display: "flex", flexDirection: "row" }} // Fixed height for DialogContent
dividers
sx={{
height: "600px",
display: "flex",
flexDirection: "row",
p: 2,
}}
>
<Paper className="w-[66%] flex flex-col">
<Paper className="w-[66%] flex flex-col" elevation={2}>
<TextField
fullWidth
placeholder="Поиск статей..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
sx={{ mb: 2, mt: 1 }}
sx={{ mb: 2, mt: 1, px: 2 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
@@ -95,27 +130,51 @@ export const SelectArticleModal = observer(
),
}}
/>
<List sx={{ flexGrow: 1, overflowY: "auto" }}>
{filteredArticles.map((article) => (
<ListItemButton
key={article.id}
onClick={() => onSelectArticle(article.id)}
onMouseEnter={() => handleArticleHover(article.id)}
onMouseLeave={handleArticleLeave}
sx={{
borderRadius: 1,
mb: 0.5,
"&:hover": {
backgroundColor: "action.hover",
},
}}
<List sx={{ flexGrow: 1, overflowY: "auto", px: 2 }}>
{filteredArticles.length === 0 ? (
<Typography
variant="body2"
color="text.secondary"
sx={{ p: 2, textAlign: "center" }}
>
<ListItemText primary={article.service_name} />
</ListItemButton>
))}
{searchQuery ? "Статьи не найдены" : "Нет доступных статей"}
</Typography>
) : (
filteredArticles.map((article) => (
<ListItemButton
key={article.id}
onClick={() => handleArticleClick(article.id)}
onDoubleClick={() => onSelectArticle(article.id)}
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 className="flex-1 flex flex-col">
<Paper className="flex-1 flex flex-col" elevation={2}>
<Box
className="rounded-2xl overflow-hidden"
sx={{
@@ -127,60 +186,109 @@ export const SelectArticleModal = observer(
flexDirection: "column",
}}
>
{/* Media Preview Area */}
<Box
sx={{
width: "100%",
height: 200,
flexShrink: 0,
backgroundColor: "grey.300",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<ImagePlus size={48} color="grey" />
</Box>
{isLoading ? (
<Box
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Typography color="white">Загрузка...</Typography>
</Box>
) : (
<>
{articlesStore.articleMedia && (
<Box sx={{ p: 2, backgroundColor: "rgba(0,0,0,0.1)" }}>
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
articlesStore.articleMedia.id
}/download?token=${token}`}
alt={articlesStore.articleMedia.filename}
style={{
maxWidth: "100%",
height: "auto",
maxHeight: "300px",
objectFit: "contain",
borderRadius: 8,
}}
/>
</Box>
)}
{!articlesStore.articleMedia && (
<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>
)}
{/* Title Area */}
<Box
sx={{
width: "100%",
height: "70px",
background: "#877361",
display: "flex",
flexShrink: 0,
alignItems: "center",
borderBottom: "1px solid",
px: 2,
}}
>
<Typography variant="h6" color="white">
{articlesStore.articleData?.heading ||
"Нет данных для предпросмотра"}
</Typography>
</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">
{articlesStore.articleData?.heading || "Выберите статью"}
</Typography>
</Box>
{/* Body Preview Area */}
<Box
sx={{
px: 2,
flexGrow: 1,
overflowY: "auto",
backgroundColor: "#877361", // To make markdown readable
color: "white",
py: 1,
}}
>
<ReactMarkdownComponent
value={articlesStore.articleData?.body || ""}
/>
</Box>
<Box
sx={{
px: 2,
flexGrow: 1,
overflowY: "auto",
backgroundColor: "#877361",
color: "white",
py: 1,
}}
>
{articlesStore.articleData?.body ? (
<ReactMarkdownComponent
value={articlesStore.articleData.body}
/>
) : (
<Typography
color="rgba(255,255,255,0.7)"
sx={{ textAlign: "center", mt: 4 }}
>
Предпросмотр статьи появится здесь
</Typography>
)}
</Box>
</>
)}
</Box>
</Paper>
</DialogContent>
<DialogActions>
<DialogActions sx={{ p: 2 }}>
<Button onClick={onClose}>Отмена</Button>
<Button
variant="contained"
onClick={() =>
selectedArticleId && onSelectArticle(selectedArticleId)
}
disabled={!selectedArticleId || isLoading}
>
Выбрать
</Button>
</DialogActions>
</Dialog>
);