289 lines
8.6 KiB
TypeScript
289 lines
8.6 KiB
TypeScript
import { Box, TextField, Typography, Paper } from "@mui/material";
|
||
import { Edit } from "@refinedev/mui";
|
||
import { useForm } from "@refinedev/react-hook-form";
|
||
import { Controller, FieldValues } from "react-hook-form";
|
||
import { useParams } from "react-router";
|
||
import React, { useState, useEffect, useMemo } from "react";
|
||
import ReactMarkdown from "react-markdown";
|
||
import { useList } from "@refinedev/core";
|
||
|
||
import { MarkdownEditor, LinkedItems } from "@components";
|
||
import { MediaItem, mediaFields } from "./types";
|
||
import "easymde/dist/easymde.min.css";
|
||
import { EVERY_LANGUAGE, Languages, languageStore, META_LANGUAGE } from "@stores";
|
||
import { observer } from "mobx-react-lite";
|
||
import { LanguageSelector, MediaView } from "@ui";
|
||
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
|
||
|
||
export const ArticleEdit = observer(() => {
|
||
const { language, setLanguageAction } = languageStore;
|
||
|
||
const [articleData, setArticleData] = useState({
|
||
heading: EVERY_LANGUAGE(""),
|
||
body: EVERY_LANGUAGE("")
|
||
});
|
||
const { id: articleId } = useParams<{ id: string }>();
|
||
const [preview, setPreview] = useState("");
|
||
const [headingPreview, setHeadingPreview] = useState("");
|
||
const simpleMDEOptions = useMemo(
|
||
() => ({
|
||
placeholder: "Введите контент в формате Markdown...",
|
||
spellChecker: false,
|
||
}),
|
||
[]
|
||
);
|
||
|
||
const {
|
||
saveButtonProps,
|
||
refineCore: { onFinish },
|
||
register,
|
||
control,
|
||
handleSubmit,
|
||
watch,
|
||
formState: { errors },
|
||
setValue,
|
||
} = useForm<{ heading: string; body: string }>({
|
||
refineCoreProps: META_LANGUAGE(language)
|
||
});
|
||
|
||
const bodyContent = watch("body");
|
||
const headingContent = watch("heading");
|
||
|
||
useEffect(() => {
|
||
setValue("heading", articleData.heading[language] ?? "");
|
||
setHeadingPreview(articleData.heading[language] ?? "");
|
||
setValue("body", articleData.body[language] ?? "");
|
||
setPreview(articleData.body[language] ?? "");
|
||
}, [language, articleData, setValue]);
|
||
|
||
function updateTranslations(update: boolean = true) {
|
||
const newArticleData = {
|
||
...articleData,
|
||
heading: {
|
||
...articleData.heading,
|
||
[language]: watch("heading") ?? "",
|
||
},
|
||
body: {
|
||
...articleData.body,
|
||
[language]: watch("body") ?? "",
|
||
}
|
||
}
|
||
if(update) setArticleData(newArticleData);
|
||
return newArticleData;
|
||
}
|
||
|
||
const handleLanguageChange = (lang: Languages) => {
|
||
updateTranslations();
|
||
setLanguageAction(lang);
|
||
};
|
||
|
||
const handleFormSubmit = handleSubmit((values: FieldValues) => {
|
||
const newTranslations = updateTranslations(false);
|
||
console.log(newTranslations);
|
||
return onFinish({
|
||
translations: newTranslations
|
||
});
|
||
});
|
||
|
||
useEffect(() => {
|
||
setPreview(bodyContent ?? "");
|
||
}, [bodyContent]);
|
||
|
||
useEffect(() => {
|
||
setHeadingPreview(headingContent ?? "");
|
||
}, [headingContent]);
|
||
|
||
const { data: mediaData } = useList<MediaItem>({
|
||
resource: `article/${articleId}/media`,
|
||
});
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
setLanguageAction("ru");
|
||
};
|
||
}, [setLanguageAction]);
|
||
|
||
return (
|
||
<Edit saveButtonProps={{
|
||
...saveButtonProps,
|
||
onClick: handleFormSubmit
|
||
}}
|
||
>
|
||
<Box sx={{ display: "flex", gap: 2 }}>
|
||
{/* Форма редактирования */}
|
||
<Box sx={{ display: "flex", flex: 1, flexDirection: "column", gap: 2 }}>
|
||
|
||
<LanguageSelector action={handleLanguageChange} />
|
||
<Box
|
||
component="form"
|
||
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||
autoComplete="off"
|
||
>
|
||
<TextField
|
||
{...register("heading", {
|
||
required: "Это поле является обязательным",
|
||
})}
|
||
error={!!errors?.heading}
|
||
helperText={errors?.heading?.message as string}
|
||
margin="normal"
|
||
fullWidth
|
||
slotProps={{inputLabel: {shrink: true}}}
|
||
type="text"
|
||
label="Заголовок *"
|
||
name="heading"
|
||
/>
|
||
|
||
<Controller
|
||
control={control}
|
||
name="body"
|
||
//rules={{ required: "Это поле является обязательным" }}
|
||
defaultValue=""
|
||
render={({ field: { onChange, value } }) => (
|
||
<MemoizedSimpleMDE
|
||
value={value} // markdown
|
||
onChange={onChange}
|
||
options={simpleMDEOptions}
|
||
className="my-markdown-editor"
|
||
/>
|
||
)}
|
||
/>
|
||
|
||
{articleId && (
|
||
<LinkedItems<MediaItem>
|
||
type="edit"
|
||
parentId={articleId}
|
||
parentResource="article"
|
||
childResource="media"
|
||
fields={mediaFields}
|
||
title="медиа"
|
||
/>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Блок предпросмотра */}
|
||
<Paper
|
||
sx={{
|
||
flex: 1,
|
||
p: 2,
|
||
maxHeight: "calc(100vh - 200px)",
|
||
overflowY: "auto",
|
||
position: "sticky",
|
||
top: 16,
|
||
borderRadius: 2,
|
||
border: "1px solid",
|
||
borderColor: "primary.main",
|
||
bgcolor: (theme) =>
|
||
theme.palette.mode === "dark" ? "background.paper" : "#fff",
|
||
}}
|
||
>
|
||
<Typography variant="h6" gutterBottom color="primary">
|
||
Предпросмотр
|
||
</Typography>
|
||
|
||
{/* Заголовок статьи */}
|
||
<Typography
|
||
variant="h4"
|
||
gutterBottom
|
||
sx={{
|
||
color: (theme) =>
|
||
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||
mb: 3,
|
||
}}
|
||
>
|
||
{headingPreview}
|
||
</Typography>
|
||
|
||
{/* Markdown контент */}
|
||
<Box
|
||
sx={{
|
||
"& img": {
|
||
maxWidth: "100%",
|
||
height: "auto",
|
||
borderRadius: 1,
|
||
},
|
||
"& h1, & h2, & h3, & h4, & h5, & h6": {
|
||
color: "primary.main",
|
||
mt: 2,
|
||
mb: 1,
|
||
},
|
||
"& p": {
|
||
mb: 2,
|
||
color: (theme) =>
|
||
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||
},
|
||
"& a": {
|
||
color: "primary.main",
|
||
textDecoration: "none",
|
||
"&:hover": {
|
||
textDecoration: "underline",
|
||
},
|
||
},
|
||
"& blockquote": {
|
||
borderLeft: "4px solid",
|
||
borderColor: "primary.main",
|
||
pl: 2,
|
||
my: 2,
|
||
color: "text.secondary",
|
||
},
|
||
"& code": {
|
||
bgcolor: (theme) =>
|
||
theme.palette.mode === "dark" ? "grey.900" : "grey.100",
|
||
p: 0.5,
|
||
borderRadius: 0.5,
|
||
color: "primary.main",
|
||
},
|
||
}}
|
||
>
|
||
<ReactMarkdown>{preview}</ReactMarkdown>
|
||
</Box>
|
||
|
||
{/* Привязанные медиа */}
|
||
{mediaData?.data && mediaData.data.length > 0 && (
|
||
<Box sx={{ mb: 3 }}>
|
||
<Typography variant="subtitle1" gutterBottom color="primary">
|
||
Привязанные медиа:
|
||
</Typography>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
gap: 1,
|
||
flexWrap: "wrap",
|
||
mb: 2,
|
||
}}
|
||
>
|
||
{mediaData.data.map((media) => (
|
||
<Box
|
||
key={media.id}
|
||
sx={{
|
||
width: 120,
|
||
height: 120,
|
||
borderRadius: 1,
|
||
overflow: "hidden",
|
||
border: "1px solid",
|
||
borderColor: "primary.main",
|
||
}}
|
||
>
|
||
<MediaView media={media} />
|
||
{/* <img
|
||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||
media.id
|
||
}/download?token=${localStorage.getItem(TOKEN_KEY)}`}
|
||
alt={media.media_name}
|
||
style={{
|
||
width: "100%",
|
||
height: "100%",
|
||
objectFit: "cover",
|
||
}}
|
||
/> */}
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
</Paper>
|
||
</Box>
|
||
</Edit>
|
||
);
|
||
});
|