WhiteNightsAdminPanel/src/components/CreateSightArticle.tsx
Alexander Lazarenko cf2a116ecb
All checks were successful
release-tag / release-image (push) Successful in 2m17s
Latest version (#12)
Co-authored-by: itoshi <kkzemeow@gmail.com>
Co-authored-by: Spynder <19329095+Spynder@users.noreply.github.com>
Reviewed-on: #12
Co-authored-by: Alexander Lazarenko <kerblif@unprism.ru>
Co-committed-by: Alexander Lazarenko <kerblif@unprism.ru>
2025-05-29 10:12:00 +00:00

398 lines
11 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 {
Typography,
Button,
Box,
Accordion,
AccordionSummary,
AccordionDetails,
useTheme,
TextField,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { axiosInstance } from "../providers/data";
import { useForm, Controller } from "react-hook-form";
import { MarkdownEditor } from "./MarkdownEditor";
import React, { useState, useCallback, useEffect } from "react";
import { useDropzone } from "react-dropzone";
import {
ALLOWED_IMAGE_TYPES,
ALLOWED_VIDEO_TYPES,
} from "../components/media/MediaFormUtils";
import { EVERY_LANGUAGE, Languages } from "@stores";
import { useNotification } from "@refinedev/core";
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
type MediaFile = {
file: File;
preview: string;
uploading: boolean;
mediaId?: number;
};
type Props = {
parentId?: string | number;
parentResource: string;
childResource: string;
title: string;
left?: boolean;
language: Languages;
setHeadingParent?: (heading: string) => void;
setBodyParent?: (body: string) => void;
onSave?: (something: any) => void;
noReset?: boolean;
};
export const CreateSightArticle = ({
parentId,
parentResource,
childResource,
title,
left,
language,
setHeadingParent,
setBodyParent,
onSave,
noReset,
}: Props) => {
const notification = useNotification();
const theme = useTheme();
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
const [workingLanguage, setWorkingLanguage] = useState<Languages>(language);
const {
register: registerItem,
watch,
control: controlItem,
handleSubmit: handleSubmitItem,
reset: resetItem,
setValue,
formState: { errors: itemErrors },
} = useForm({
defaultValues: {
heading: "",
body: "",
},
});
const [articleData, setArticleData] = useState({
heading: EVERY_LANGUAGE(""),
body: EVERY_LANGUAGE(""),
});
function updateTranslations() {
const newArticleData = {
...articleData,
heading: {
...articleData.heading,
[workingLanguage]: watch("heading") ?? "",
},
body: {
...articleData.body,
[workingLanguage]: watch("body") ?? "",
},
};
setArticleData(newArticleData);
return newArticleData;
}
useEffect(() => {
setValue("heading", articleData.heading[workingLanguage] ?? "");
setValue("body", articleData.body[workingLanguage] ?? "");
}, [workingLanguage, articleData, setValue]);
useEffect(() => {
updateTranslations();
setWorkingLanguage(language);
}, [language]);
useEffect(() => {
setHeadingParent?.(watch("heading"));
setBodyParent?.(watch("body"));
}, [watch("heading"), watch("body"), setHeadingParent, setBodyParent]);
const simpleMDEOptions = React.useMemo(
() => ({
placeholder: "Введите контент в формате Markdown...",
spellChecker: false,
}),
[]
);
const onDrop = useCallback((acceptedFiles: File[]) => {
const newFiles = acceptedFiles.map((file) => ({
file,
preview: URL.createObjectURL(file),
uploading: false,
}));
setMediaFiles((prev) => [...prev, ...newFiles]);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
"image/jpeg": [".jpeg", ".jpg"],
"image/png": [".png"],
"image/webp": [".webp"],
"video/mp4": [".mp4"],
"video/webm": [".webm"],
"video/ogg": [".ogg"],
},
multiple: true,
});
const uploadMedia = async (mediaFile: MediaFile) => {
const formData = new FormData();
formData.append("media_name", mediaFile.file.name);
formData.append("filename", mediaFile.file.name);
formData.append(
"type",
mediaFile.file.type.startsWith("image/") ? "1" : "2"
);
formData.append("file", mediaFile.file);
const response = await axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/media`,
formData
);
return response.data.id;
};
const handleCreate = async (data: { heading: string; body: string }) => {
try {
// Создаем статью
const response = await axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/${childResource}`,
{
...data,
translations: updateTranslations(),
}
);
const itemId = response.data.id;
if (parentId) {
// Получаем существующие статьи для определения порядкового номера
const existingItemsResponse = await axiosInstance.get(
`${
import.meta.env.VITE_KRBL_API
}/${parentResource}/${parentId}/${childResource}`
);
const existingItems = existingItemsResponse.data ?? [];
const nextPageNum = existingItems.length + 1;
if (!left) {
await axiosInstance.post(
`${
import.meta.env.VITE_KRBL_API
}/${parentResource}/${parentId}/${childResource}/`,
{
[`${childResource}_id`]: itemId,
page_num: nextPageNum,
}
);
} else {
const response = await axiosInstance.get(
`${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/`
);
const data = response.data;
if (data) {
await axiosInstance.patch(
`${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/`,
{
...data,
left_article: itemId,
}
);
}
}
}
// Загружаем все медиа файлы и получаем их ID
const mediaIds = await Promise.all(
mediaFiles.map(async (mediaFile) => {
return await uploadMedia(mediaFile);
})
);
// Привязываем все медиа к статье
await Promise.all(
mediaIds.map((mediaId, index) =>
axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/article/${itemId}/media/`,
{
media_id: mediaId,
media_order: index + 1,
}
)
)
);
if (noReset) {
setValue("heading", "");
setValue("body", "");
} else {
resetItem();
}
if (onSave) {
onSave(response.data);
if (notification && typeof notification.open === "function") {
notification.open({
message: "Статья успешно создана",
type: "success",
});
}
} else {
window.location.reload();
}
} catch (err: any) {
console.error("Error creating item:", err);
}
};
const removeMedia = (index: number) => {
setMediaFiles((prev) => {
const newFiles = [...prev];
URL.revokeObjectURL(newFiles[index].preview);
newFiles.splice(index, 1);
return newFiles;
});
};
return (
<Box>
<TextField
{...registerItem("heading", {
required: "Это поле является обязательным",
})}
error={!!(itemErrors as any)?.heading}
helperText={(itemErrors as any)?.heading?.message}
margin="normal"
fullWidth
slotProps={{ inputLabel: { shrink: true } }}
type="text"
sx={{
backgroundColor: theme.palette.background.paper,
}}
label="Заголовок *"
/>
<Controller
control={controlItem}
name="body"
rules={{ required: "Это поле является обязательным" }}
defaultValue=""
render={({ field: { onChange, value } }) => (
<MemoizedSimpleMDE
value={value}
onChange={onChange}
options={simpleMDEOptions}
className="my-markdown-editor"
/>
)}
/>
{/* Dropzone для медиа файлов */}
<Box sx={{ mt: 2, mb: 2 }}>
<Box
{...getRootProps()}
sx={{
border: "2px dashed",
borderColor: isDragActive ? "primary.main" : "grey.300",
borderRadius: 1,
p: 2,
textAlign: "center",
cursor: "pointer",
"&:hover": {
borderColor: "primary.main",
},
}}
>
<input {...getInputProps()} />
<Typography>
{isDragActive
? "Перетащите файлы сюда..."
: "Перетащите файлы сюда или кликните для выбора"}
</Typography>
</Box>
{/* Превью загруженных файлов */}
<Box sx={{ mt: 2, display: "flex", flexWrap: "wrap", gap: 1 }}>
{mediaFiles.map((mediaFile, index) => (
<Box
key={mediaFile.preview}
sx={{
position: "relative",
width: 100,
height: 100,
}}
>
{mediaFile.file.type.startsWith("image/") ? (
<img
src={mediaFile.preview}
alt={mediaFile.file.name}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
<Box
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
bgcolor: "grey.200",
}}
>
<Typography variant="caption">
{mediaFile.file.name}
</Typography>
</Box>
)}
<Button
size="small"
color="error"
onClick={() => removeMedia(index)}
sx={{
position: "absolute",
top: 0,
right: 0,
minWidth: "auto",
width: 20,
height: 20,
p: 0,
}}
>
×
</Button>
</Box>
))}
</Box>
</Box>
<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button
variant="contained"
color="primary"
type="submit"
onClick={handleSubmitItem(handleCreate)}
>
Создать
</Button>
<Button
variant="outlined"
onClick={() => {
resetItem();
mediaFiles.forEach((file) => URL.revokeObjectURL(file.preview));
setMediaFiles([]);
}}
>
Очистить
</Button>
</Box>
</Box>
);
};