Добавлено новое поле route_name: Текстовые поля на двух страницах Поле в списке маршрутов Добавлено выбор видео на двух страниц вместе с редактором статей в виде модального окна Модальное окно позволяет создать статью, выбрать готовую, отредактировать выбранную сразу на трех языках Микаэл: Пожалуйста, перепроверь код, вдруг чего найдешь как улучшить + захости локально и потыкай пж: создай с 0 маршрут и прикрепи к нему созданную / какую-нибудь статью с видео, можешь попробовать загрузить либо взять готовое после того как создашь, попробуй потыкать и поменять чего-нибудь (проще обьясню: представь, что ты Руслан) Reviewed-on: #16 Reviewed-by: Микаэл Оганесян <15lu.akari@unprism.ru> Co-authored-by: fisenko <kkzemeow@gmail.com> Co-committed-by: fisenko <kkzemeow@gmail.com>
1071 lines
35 KiB
TypeScript
1071 lines
35 KiB
TypeScript
import {
|
||
articlesStore,
|
||
languageStore,
|
||
authInstance,
|
||
SelectMediaDialog,
|
||
UploadMediaDialog,
|
||
Language,
|
||
} from "@shared";
|
||
import { observer } from "mobx-react-lite";
|
||
import { useEffect, useState } from "react";
|
||
import { runInAction } from "mobx";
|
||
import {
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
Button,
|
||
TextField,
|
||
List,
|
||
ListItemButton,
|
||
ListItemText,
|
||
Box,
|
||
Tabs,
|
||
Tab,
|
||
Typography,
|
||
InputAdornment,
|
||
Paper,
|
||
} from "@mui/material";
|
||
import { Search, Plus, ImagePlus, Save } from "lucide-react";
|
||
import {
|
||
ReactMarkdownEditor,
|
||
ReactMarkdownComponent,
|
||
MediaViewer,
|
||
MediaArea,
|
||
} from "@widgets";
|
||
import { toast } from "react-toastify";
|
||
|
||
interface ArticleSelectOrCreateDialogProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
onSelectArticle: (articleId: number) => void;
|
||
}
|
||
|
||
export const ArticleSelectOrCreateDialog = observer(
|
||
({ open, onClose, onSelectArticle }: ArticleSelectOrCreateDialogProps) => {
|
||
const { articles, getArticles, getArticle, getArticleMedia } =
|
||
articlesStore;
|
||
const [modalLanguage, setModalLanguage] = useState<Language>("ru");
|
||
const [searchQuery, setSearchQuery] = useState("");
|
||
const [tabValue, setTabValue] = useState(0);
|
||
const [isCreating, setIsCreating] = useState(false);
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
|
||
const [selectedArticleId, setSelectedArticleId] = useState<number | null>(
|
||
null
|
||
);
|
||
const [editedArticleData, setEditedArticleData] = useState({
|
||
ru: { heading: "", body: "" },
|
||
en: { heading: "", body: "" },
|
||
zh: { heading: "", body: "" },
|
||
});
|
||
const [editedArticleMedia, setEditedArticleMedia] = useState<
|
||
{
|
||
id: string;
|
||
filename: string;
|
||
media_type: number;
|
||
}[]
|
||
>([]);
|
||
|
||
const [newArticleData, setNewArticleData] = useState({
|
||
ru: { heading: "", body: "" },
|
||
en: { heading: "", body: "" },
|
||
zh: { heading: "", body: "" },
|
||
});
|
||
const [createdArticleMedia, setCreatedArticleMedia] = useState<
|
||
{
|
||
id: string;
|
||
filename: string;
|
||
media_name?: string;
|
||
media_type: number;
|
||
}[]
|
||
>([]);
|
||
const [tempArticleId, setTempArticleId] = useState<number | null>(null);
|
||
|
||
const [isUploadMediaDialogOpen, setIsUploadMediaDialogOpen] =
|
||
useState(false);
|
||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||
useState(false);
|
||
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||
|
||
const currentArticleId = selectedArticleId || tempArticleId;
|
||
const currentArticleData = selectedArticleId
|
||
? editedArticleData
|
||
: newArticleData;
|
||
const currentMedia = selectedArticleId
|
||
? editedArticleMedia
|
||
: createdArticleMedia;
|
||
const isEditMode =
|
||
selectedArticleId !== null || tempArticleId !== null || tabValue === 1;
|
||
|
||
useEffect(() => {
|
||
if (open) {
|
||
setModalLanguage("ru");
|
||
(async () => {
|
||
await Promise.all([
|
||
getArticles("ru"),
|
||
getArticles("en"),
|
||
getArticles("zh"),
|
||
]);
|
||
})();
|
||
setSearchQuery("");
|
||
setTabValue(0);
|
||
setIsCreating(false);
|
||
setIsSaving(false);
|
||
setSelectedArticleId(null);
|
||
setTempArticleId(null);
|
||
setEditedArticleData({
|
||
ru: { heading: "", body: "" },
|
||
en: { heading: "", body: "" },
|
||
zh: { heading: "", body: "" },
|
||
});
|
||
setNewArticleData({
|
||
ru: { heading: "", body: "" },
|
||
en: { heading: "", body: "" },
|
||
zh: { heading: "", body: "" },
|
||
});
|
||
setEditedArticleMedia([]);
|
||
setCreatedArticleMedia([]);
|
||
}
|
||
}, [open, getArticles]);
|
||
|
||
useEffect(() => {
|
||
if (!open) {
|
||
languageStore.setLanguage("ru");
|
||
}
|
||
}, [open]);
|
||
|
||
const loadArticleForEdit = async (articleId: number) => {
|
||
try {
|
||
const [ruArticle, enArticle, zhArticle, mediaResponse] =
|
||
await Promise.all([
|
||
articlesStore.getArticle(articleId, "ru"),
|
||
articlesStore.getArticle(articleId, "en"),
|
||
articlesStore.getArticle(articleId, "zh"),
|
||
authInstance.get(`/article/${articleId}/media`),
|
||
]);
|
||
|
||
setEditedArticleData({
|
||
ru: {
|
||
heading: ruArticle.data.heading,
|
||
body: ruArticle.data.body,
|
||
},
|
||
en: {
|
||
heading: enArticle.data.heading,
|
||
body: enArticle.data.body,
|
||
},
|
||
zh: {
|
||
heading: zhArticle.data.heading,
|
||
body: zhArticle.data.body,
|
||
},
|
||
});
|
||
|
||
setEditedArticleMedia(
|
||
(mediaResponse.data || []).map((m: any) => ({
|
||
id: m.id,
|
||
filename: m.filename,
|
||
media_type: m.media_type,
|
||
}))
|
||
);
|
||
|
||
setSelectedArticleId(articleId);
|
||
await getArticleMedia(articleId);
|
||
} catch (error) {
|
||
console.error("Error loading article:", error);
|
||
toast.error("Ошибка при загрузке статьи");
|
||
}
|
||
};
|
||
|
||
const handleArticleSelect = async (articleId: number) => {
|
||
await loadArticleForEdit(articleId);
|
||
};
|
||
|
||
const handleSaveArticle = async () => {
|
||
if (!currentArticleId) return;
|
||
|
||
try {
|
||
setIsSaving(true);
|
||
await authInstance.patch(`/article/${currentArticleId}`, {
|
||
translations: {
|
||
heading: {
|
||
ru: currentArticleData.ru.heading,
|
||
en: currentArticleData.en.heading,
|
||
zh: currentArticleData.zh.heading,
|
||
},
|
||
body: {
|
||
ru: currentArticleData.ru.body,
|
||
en: currentArticleData.en.body,
|
||
zh: currentArticleData.zh.body,
|
||
},
|
||
},
|
||
});
|
||
|
||
await loadArticleForEdit(currentArticleId);
|
||
|
||
toast.success("Статья успешно сохранена");
|
||
} catch (error) {
|
||
console.error("Error saving article:", error);
|
||
toast.error("Ошибка при сохранении статьи");
|
||
} finally {
|
||
setIsSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleSelectAndClose = () => {
|
||
if (currentArticleId) {
|
||
onSelectArticle(currentArticleId);
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
const handleCreateArticle = async () => {
|
||
try {
|
||
setIsCreating(true);
|
||
|
||
const hasData = Object.values(newArticleData).some(
|
||
(langData) => langData.heading.trim() || langData.body.trim()
|
||
);
|
||
|
||
if (!hasData) {
|
||
toast.error("Заполните хотя бы одно поле для любого языка");
|
||
setIsCreating(false);
|
||
return;
|
||
}
|
||
|
||
const response = await authInstance.post("/article", {
|
||
translations: {
|
||
heading: {
|
||
ru: newArticleData.ru.heading || "Новый заголовок (RU)",
|
||
en: newArticleData.en.heading || "New Heading (EN)",
|
||
zh: newArticleData.zh.heading || "Новый заголовок (ZH)",
|
||
},
|
||
body: {
|
||
ru: newArticleData.ru.body || "Новый текст (RU)",
|
||
en: newArticleData.en.body || "New Text (EN)",
|
||
zh: newArticleData.zh.body || "Новый текст (ZH)",
|
||
},
|
||
},
|
||
});
|
||
|
||
const { id } = response.data;
|
||
setTempArticleId(id);
|
||
|
||
const ruHeading = newArticleData.ru.heading || "Новый заголовок (RU)";
|
||
const enHeading = newArticleData.en.heading || "New Heading (EN)";
|
||
const zhHeading = newArticleData.zh.heading || "Новый заголовок (ZH)";
|
||
const ruBody = newArticleData.ru.body || "Новый текст (RU)";
|
||
const enBody = newArticleData.en.body || "New Text (EN)";
|
||
const zhBody = newArticleData.zh.body || "Новый текст (ZH)";
|
||
|
||
runInAction(() => {
|
||
articlesStore.articleList.ru.data.unshift({
|
||
id,
|
||
heading: ruHeading,
|
||
body: ruBody,
|
||
service_name: ruHeading,
|
||
} as any);
|
||
articlesStore.articleList.en.data.unshift({
|
||
id,
|
||
heading: enHeading,
|
||
body: enBody,
|
||
service_name: enHeading,
|
||
} as any);
|
||
articlesStore.articleList.zh.data.unshift({
|
||
id,
|
||
heading: zhHeading,
|
||
body: zhBody,
|
||
service_name: zhHeading,
|
||
} as any);
|
||
articlesStore.articleList.ru.loaded = true;
|
||
articlesStore.articleList.en.loaded = true;
|
||
articlesStore.articleList.zh.loaded = true;
|
||
if (articlesStore.articles) {
|
||
articlesStore.articles.ru.unshift({
|
||
id,
|
||
heading: ruHeading,
|
||
body: ruBody,
|
||
service_name: ruHeading,
|
||
} as any);
|
||
articlesStore.articles.en.unshift({
|
||
id,
|
||
heading: enHeading,
|
||
body: enBody,
|
||
service_name: enHeading,
|
||
} as any);
|
||
articlesStore.articles.zh.unshift({
|
||
id,
|
||
heading: zhHeading,
|
||
body: zhBody,
|
||
service_name: zhHeading,
|
||
} as any);
|
||
}
|
||
});
|
||
|
||
if (createdArticleMedia.length > 0) {
|
||
try {
|
||
for (let i = 0; i < createdArticleMedia.length; i++) {
|
||
await authInstance.post(`/article/${id}/media`, {
|
||
media_id: createdArticleMedia[i].id,
|
||
media_order: i + 1,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error("Error linking media:", error);
|
||
toast.warning(
|
||
"Статья создана, но не удалось добавить некоторые медиа"
|
||
);
|
||
}
|
||
}
|
||
|
||
const mediaResponse = await authInstance.get(`/article/${id}/media`);
|
||
if (mediaResponse.data && mediaResponse.data.length > 0) {
|
||
setCreatedArticleMedia(
|
||
mediaResponse.data.map((m: any) => ({
|
||
id: m.id,
|
||
filename: m.filename,
|
||
media_name: m.media_name,
|
||
media_type: m.media_type,
|
||
}))
|
||
);
|
||
}
|
||
|
||
await getArticle(id);
|
||
await getArticleMedia(id);
|
||
|
||
toast.success("Статья успешно создана");
|
||
|
||
onSelectArticle(id);
|
||
onClose();
|
||
} catch (error) {
|
||
console.error("Error creating article:", error);
|
||
toast.error("Ошибка при создании статьи");
|
||
} finally {
|
||
setIsCreating(false);
|
||
}
|
||
};
|
||
|
||
const handleMediaSelect = async (media: {
|
||
id: string;
|
||
filename: string;
|
||
media_name?: string;
|
||
media_type: number;
|
||
}) => {
|
||
if (!currentArticleId) {
|
||
if (tabValue === 1) {
|
||
setCreatedArticleMedia((prev) => [...prev, media]);
|
||
}
|
||
setIsSelectMediaDialogOpen(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const currentMediaCount = currentMedia.length;
|
||
await authInstance.post(`/article/${currentArticleId}/media`, {
|
||
media_id: media.id,
|
||
media_order: currentMediaCount + 1,
|
||
});
|
||
|
||
const mediaResponse = await authInstance.get(
|
||
`/article/${currentArticleId}/media`
|
||
);
|
||
const mediaList = mediaResponse.data.map((m: any) => ({
|
||
id: m.id,
|
||
filename: m.filename,
|
||
media_type: m.media_type,
|
||
}));
|
||
|
||
if (selectedArticleId) {
|
||
setEditedArticleMedia(mediaList);
|
||
} else {
|
||
setCreatedArticleMedia(
|
||
mediaResponse.data.map((m: any) => ({
|
||
id: m.id,
|
||
filename: m.filename,
|
||
media_name: m.media_name,
|
||
media_type: m.media_type,
|
||
}))
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error("Error linking media:", error);
|
||
toast.error("Ошибка при добавлении медиа");
|
||
}
|
||
setIsSelectMediaDialogOpen(false);
|
||
};
|
||
|
||
const handleDeleteMedia = async (mediaId: string) => {
|
||
if (!currentArticleId) {
|
||
if (tabValue === 1) {
|
||
setCreatedArticleMedia((prev) =>
|
||
prev.filter((m) => m.id !== mediaId)
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await authInstance.delete(
|
||
`/article/${currentArticleId}/media/${mediaId}`
|
||
);
|
||
const response = await authInstance.get(
|
||
`/article/${currentArticleId}/media`
|
||
);
|
||
const mediaList = response.data.map((m: any) => ({
|
||
id: m.id,
|
||
filename: m.filename,
|
||
media_type: m.media_type,
|
||
}));
|
||
|
||
if (selectedArticleId) {
|
||
setEditedArticleMedia(mediaList);
|
||
} else {
|
||
setCreatedArticleMedia(
|
||
response.data.map((m: any) => ({
|
||
id: m.id,
|
||
filename: m.filename,
|
||
media_name: m.media_name,
|
||
media_type: m.media_type,
|
||
}))
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error("Error deleting media:", error);
|
||
toast.error("Ошибка при удалении медиа");
|
||
}
|
||
};
|
||
|
||
const handleMediaFilesDrop = (files: File[]) => {
|
||
if (files.length > 0) {
|
||
setFileToUpload(files[0]);
|
||
setIsUploadMediaDialogOpen(true);
|
||
}
|
||
};
|
||
|
||
const handleMediaUpload = async (media: {
|
||
id: string;
|
||
filename: string;
|
||
media_name?: string;
|
||
media_type: number;
|
||
}) => {
|
||
if (!currentArticleId) {
|
||
await handleMediaSelect(media);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const currentMediaCount = currentMedia.length;
|
||
await authInstance.post(`/article/${currentArticleId}/media`, {
|
||
media_id: media.id,
|
||
media_order: currentMediaCount + 1,
|
||
});
|
||
|
||
const mediaResponse = await authInstance.get(
|
||
`/article/${currentArticleId}/media`
|
||
);
|
||
const mediaList = mediaResponse.data.map((m: any) => ({
|
||
id: m.id,
|
||
filename: m.filename,
|
||
media_type: m.media_type,
|
||
}));
|
||
|
||
if (selectedArticleId) {
|
||
setEditedArticleMedia(mediaList);
|
||
} else {
|
||
setCreatedArticleMedia(
|
||
mediaResponse.data.map((m: any) => ({
|
||
id: m.id,
|
||
filename: m.filename,
|
||
media_name: m.media_name,
|
||
media_type: m.media_type,
|
||
}))
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error("Error linking media:", error);
|
||
toast.error("Ошибка при добавлении медиа");
|
||
}
|
||
setIsUploadMediaDialogOpen(false);
|
||
setFileToUpload(null);
|
||
};
|
||
|
||
const handleBack = () => {
|
||
if (selectedArticleId) {
|
||
setSelectedArticleId(null);
|
||
setEditedArticleData({
|
||
ru: { heading: "", body: "" },
|
||
en: { heading: "", body: "" },
|
||
zh: { heading: "", body: "" },
|
||
});
|
||
setEditedArticleMedia([]);
|
||
} else if (tempArticleId) {
|
||
setTempArticleId(null);
|
||
setNewArticleData({
|
||
ru: { heading: "", body: "" },
|
||
en: { heading: "", body: "" },
|
||
zh: { heading: "", body: "" },
|
||
});
|
||
setCreatedArticleMedia([]);
|
||
setTabValue(0);
|
||
} else if (tabValue === 1) {
|
||
setTabValue(0);
|
||
setNewArticleData({
|
||
ru: { heading: "", body: "" },
|
||
en: { heading: "", body: "" },
|
||
zh: { heading: "", body: "" },
|
||
});
|
||
setCreatedArticleMedia([]);
|
||
}
|
||
setModalLanguage("ru");
|
||
languageStore.setLanguage("ru");
|
||
};
|
||
|
||
const filteredArticles = articles[modalLanguage].filter((article) =>
|
||
article?.service_name?.toLowerCase().includes(searchQuery.toLowerCase())
|
||
);
|
||
|
||
// Preview-by-click logic with request serialization to avoid concurrent requests
|
||
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
|
||
const [queuedPreviewId, setQueuedPreviewId] = useState<number | null>(null);
|
||
const clickTimerRef = (typeof window !== "undefined"
|
||
? (window as any)
|
||
: {}) as {
|
||
current?: any;
|
||
} as React.MutableRefObject<NodeJS.Timeout | null>;
|
||
if (clickTimerRef.current === undefined) {
|
||
(clickTimerRef as any).current = null;
|
||
}
|
||
|
||
const runPreviewFetch = async (articleId: number) => {
|
||
if (isPreviewLoading) {
|
||
setQueuedPreviewId(articleId);
|
||
return;
|
||
}
|
||
setIsPreviewLoading(true);
|
||
try {
|
||
await Promise.all([
|
||
getArticle(articleId, modalLanguage),
|
||
getArticleMedia(articleId),
|
||
]);
|
||
} finally {
|
||
setIsPreviewLoading(false);
|
||
if (queuedPreviewId && queuedPreviewId !== articleId) {
|
||
const nextId = queuedPreviewId;
|
||
setQueuedPreviewId(null);
|
||
// Run the next queued preview
|
||
runPreviewFetch(nextId);
|
||
} else {
|
||
setQueuedPreviewId(null);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleListItemClick = (articleId: number) => {
|
||
// Delay to allow double-click to cancel preview
|
||
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
||
clickTimerRef.current = setTimeout(() => {
|
||
if (tabValue === 0 && !selectedArticleId && !tempArticleId) {
|
||
runPreviewFetch(articleId);
|
||
}
|
||
}, 200);
|
||
};
|
||
|
||
const handleListItemDoubleClick = (articleId: number) => {
|
||
// Cancel pending single-click preview and proceed to select
|
||
if (clickTimerRef.current) {
|
||
clearTimeout(clickTimerRef.current);
|
||
(clickTimerRef as any).current = null;
|
||
}
|
||
handleArticleSelect(articleId);
|
||
};
|
||
|
||
const previewData = {
|
||
heading: currentArticleData[modalLanguage].heading || "",
|
||
body: currentArticleData[modalLanguage].body || "",
|
||
};
|
||
|
||
const previewMedia =
|
||
currentMedia.length > 0
|
||
? {
|
||
id: currentMedia[0].id,
|
||
media_type: currentMedia[0].media_type,
|
||
filename: currentMedia[0].filename,
|
||
}
|
||
: null;
|
||
|
||
const selectionPreviewHeading =
|
||
(articlesStore.articleData as any)?.[modalLanguage]?.heading ||
|
||
(articlesStore.articleData as any)?.heading ||
|
||
"";
|
||
const selectionPreviewBody =
|
||
(articlesStore.articleData as any)?.[modalLanguage]?.body ||
|
||
(articlesStore.articleData as any)?.body ||
|
||
"";
|
||
|
||
return (
|
||
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
|
||
<DialogTitle>
|
||
{tabValue === 0 && !isEditMode
|
||
? "Выберите существующую статью"
|
||
: selectedArticleId
|
||
? "Редактирование статьи"
|
||
: "Создать новую статью"}
|
||
</DialogTitle>
|
||
<DialogContent
|
||
dividers
|
||
sx={{ height: "600px", display: "flex", flexDirection: "column" }}
|
||
>
|
||
{tabValue === 0 && !isEditMode ? (
|
||
<>
|
||
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}>
|
||
<Tabs
|
||
value={tabValue}
|
||
onChange={(_e, newValue) => setTabValue(newValue)}
|
||
>
|
||
<Tab label="Выбрать существующую" />
|
||
<Tab label="Создать новую" />
|
||
</Tabs>
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{ display: "flex", gap: 2, flex: 1, overflow: "hidden" }}
|
||
>
|
||
<Paper className="w-[66%] flex flex-col">
|
||
<Box
|
||
sx={{
|
||
p: 2,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
flex: 1,
|
||
overflow: "hidden",
|
||
}}
|
||
>
|
||
<TextField
|
||
fullWidth
|
||
placeholder="Поиск статей..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
sx={{ mb: 2 }}
|
||
InputProps={{
|
||
startAdornment: (
|
||
<InputAdornment position="start">
|
||
<Search size={20} />
|
||
</InputAdornment>
|
||
),
|
||
}}
|
||
/>
|
||
<List
|
||
sx={{
|
||
flexGrow: 1,
|
||
overflowY: "auto",
|
||
border: "1px solid",
|
||
borderColor: "divider",
|
||
borderRadius: 1,
|
||
}}
|
||
>
|
||
{filteredArticles.length === 0 ? (
|
||
<Box sx={{ p: 2, textAlign: "center" }}>
|
||
<Typography variant="body2" color="text.secondary">
|
||
{searchQuery
|
||
? "Статьи не найдены"
|
||
: "Нет доступных статей"}
|
||
</Typography>
|
||
</Box>
|
||
) : (
|
||
filteredArticles.map((article) => (
|
||
<ListItemButton
|
||
key={article.id}
|
||
onClick={() => handleListItemClick(article.id)}
|
||
onDoubleClick={() =>
|
||
handleListItemDoubleClick(article.id)
|
||
}
|
||
sx={{
|
||
borderRadius: 1,
|
||
mb: 0.5,
|
||
"&:hover": {
|
||
backgroundColor: "action.hover",
|
||
},
|
||
}}
|
||
>
|
||
<ListItemText primary={article.service_name} />
|
||
</ListItemButton>
|
||
))
|
||
)}
|
||
</List>
|
||
</Box>
|
||
</Paper>
|
||
|
||
<Paper
|
||
elevation={3}
|
||
sx={{
|
||
flex: 1,
|
||
maxWidth: "320px",
|
||
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",
|
||
},
|
||
}}
|
||
>
|
||
{tabValue === 0 && !isEditMode ? (
|
||
articlesStore.articleMedia ? (
|
||
<MediaViewer
|
||
media={articlesStore.articleMedia}
|
||
fullWidth
|
||
/>
|
||
) : (
|
||
<ImagePlus size={48} color="white" />
|
||
)
|
||
) : previewMedia ? (
|
||
<MediaViewer media={previewMedia} fullWidth />
|
||
) : (
|
||
<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%",
|
||
}}
|
||
>
|
||
{tabValue === 0 && !isEditMode
|
||
? selectionPreviewHeading || "Название информации"
|
||
: previewData.heading || "Название информации"}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{(tabValue === 0 && !isEditMode
|
||
? selectionPreviewBody
|
||
: previewData.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={
|
||
tabValue === 0 && !isEditMode
|
||
? selectionPreviewBody
|
||
: previewData.body
|
||
}
|
||
/>
|
||
</Box>
|
||
)}
|
||
</Paper>
|
||
</Box>
|
||
</>
|
||
) : (
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
gap: 3,
|
||
flex: 1,
|
||
overflow: "hidden",
|
||
position: "relative",
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
flex: 2,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
gap: 2,
|
||
overflow: "auto",
|
||
position: "relative",
|
||
paddingBottom: "60px",
|
||
}}
|
||
>
|
||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||
<Tabs
|
||
value={modalLanguage}
|
||
onChange={(_e, newValue) => setModalLanguage(newValue)}
|
||
>
|
||
<Tab label="Русский" value="ru" />
|
||
<Tab label="English" value="en" />
|
||
<Tab label="中文" value="zh" />
|
||
</Tabs>
|
||
</Box>
|
||
|
||
<TextField
|
||
label="Название информации"
|
||
value={currentArticleData[modalLanguage].heading}
|
||
onChange={(e) => {
|
||
if (selectedArticleId) {
|
||
setEditedArticleData({
|
||
...editedArticleData,
|
||
[modalLanguage]: {
|
||
...editedArticleData[modalLanguage],
|
||
heading: e.target.value,
|
||
},
|
||
});
|
||
} else {
|
||
setNewArticleData({
|
||
...newArticleData,
|
||
[modalLanguage]: {
|
||
...newArticleData[modalLanguage],
|
||
heading: e.target.value,
|
||
},
|
||
});
|
||
}
|
||
}}
|
||
variant="outlined"
|
||
fullWidth
|
||
/>
|
||
|
||
<ReactMarkdownEditor
|
||
value={currentArticleData[modalLanguage].body}
|
||
onChange={(value: any) => {
|
||
if (selectedArticleId) {
|
||
setEditedArticleData({
|
||
...editedArticleData,
|
||
[modalLanguage]: {
|
||
...editedArticleData[modalLanguage],
|
||
body: value,
|
||
},
|
||
});
|
||
} else {
|
||
setNewArticleData({
|
||
...newArticleData,
|
||
[modalLanguage]: {
|
||
...newArticleData[modalLanguage],
|
||
body: value,
|
||
},
|
||
});
|
||
}
|
||
}}
|
||
/>
|
||
|
||
<MediaArea
|
||
articleId={currentArticleId || 0}
|
||
mediaIds={currentMedia}
|
||
deleteMedia={(_articleId, mediaId) =>
|
||
handleDeleteMedia(mediaId)
|
||
}
|
||
setSelectMediaDialogOpen={setIsSelectMediaDialogOpen}
|
||
onFilesDrop={handleMediaFilesDrop}
|
||
/>
|
||
</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",
|
||
},
|
||
}}
|
||
>
|
||
{previewMedia ? (
|
||
<MediaViewer media={previewMedia} fullWidth />
|
||
) : (
|
||
<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%",
|
||
}}
|
||
>
|
||
{previewData.heading || "Название информации"}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{previewData.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={previewData.body} />
|
||
</Box>
|
||
)}
|
||
</Paper>
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
{!(tabValue === 0 && !isEditMode) && (
|
||
<Button onClick={handleBack} sx={{ mr: "auto" }}>
|
||
Назад
|
||
</Button>
|
||
)}
|
||
<Button
|
||
onClick={() => {
|
||
languageStore.setLanguage("ru");
|
||
onClose();
|
||
}}
|
||
>
|
||
Отмена
|
||
</Button>
|
||
|
||
{tabValue === 1 && !tempArticleId && !selectedArticleId && (
|
||
<Button
|
||
variant="contained"
|
||
onClick={handleCreateArticle}
|
||
disabled={isCreating}
|
||
startIcon={<Plus size={16} />}
|
||
>
|
||
{isCreating ? "Создание..." : "Создать статью"}
|
||
</Button>
|
||
)}
|
||
{(tempArticleId || selectedArticleId) && (
|
||
<>
|
||
{selectedArticleId && (
|
||
<Button
|
||
variant="outlined"
|
||
onClick={handleSaveArticle}
|
||
disabled={isSaving}
|
||
startIcon={<Save size={16} />}
|
||
>
|
||
Сохранить
|
||
</Button>
|
||
)}
|
||
<Button
|
||
variant="contained"
|
||
onClick={() => {
|
||
handleSelectAndClose();
|
||
languageStore.setLanguage("ru");
|
||
}}
|
||
startIcon={<Plus size={16} />}
|
||
>
|
||
Выбрать и закрыть
|
||
</Button>
|
||
</>
|
||
)}
|
||
</DialogActions>
|
||
|
||
<SelectMediaDialog
|
||
open={isSelectMediaDialogOpen}
|
||
onClose={() => setIsSelectMediaDialogOpen(false)}
|
||
onSelectMedia={handleMediaSelect}
|
||
/>
|
||
|
||
<UploadMediaDialog
|
||
open={isUploadMediaDialogOpen}
|
||
onClose={() => {
|
||
setIsUploadMediaDialogOpen(false);
|
||
setFileToUpload(null);
|
||
}}
|
||
contextObjectName="Статья"
|
||
contextType="sight"
|
||
isArticle={true}
|
||
articleName={currentArticleData[modalLanguage].heading || "Статья"}
|
||
initialFile={fileToUpload || undefined}
|
||
afterUpload={handleMediaUpload}
|
||
/>
|
||
</Dialog>
|
||
);
|
||
}
|
||
);
|