rewrite code with edit article in sight page

This commit is contained in:
Илья Куприец 2025-05-01 00:10:50 +03:00
parent dc483d62de
commit 65532f7074
5 changed files with 880 additions and 322 deletions

View File

@ -33,6 +33,7 @@ type Props = {
parentResource: string; parentResource: string;
childResource: string; childResource: string;
title: string; title: string;
left?: boolean;
}; };
export const CreateSightArticle = ({ export const CreateSightArticle = ({
@ -40,6 +41,7 @@ export const CreateSightArticle = ({
parentResource, parentResource,
childResource, childResource,
title, title,
left,
}: Props) => { }: Props) => {
const theme = useTheme(); const theme = useTheme();
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]); const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
@ -118,16 +120,18 @@ export const CreateSightArticle = ({
const existingItems = existingItemsResponse.data || []; const existingItems = existingItemsResponse.data || [];
const nextPageNum = existingItems.length + 1; const nextPageNum = existingItems.length + 1;
// Привязываем статью к достопримечательности if (!left) {
await axiosInstance.post( // Привязываем статью к достопримечательности если она не левая
`${ await axiosInstance.post(
import.meta.env.VITE_KRBL_API `${
}/${parentResource}/${parentId}/${childResource}/`, import.meta.env.VITE_KRBL_API
{ }/${parentResource}/${parentId}/${childResource}/`,
[`${childResource}_id`]: itemId, {
page_num: nextPageNum, [`${childResource}_id`]: itemId,
} page_num: nextPageNum,
); }
);
}
// Загружаем все медиа файлы и получаем их ID // Загружаем все медиа файлы и получаем их ID
const mediaIds = await Promise.all( const mediaIds = await Promise.all(
@ -174,6 +178,7 @@ export const CreateSightArticle = ({
marginTop: 2, marginTop: 2,
background: theme.palette.background.paper, background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`, borderBottom: `1px solid ${theme.palette.divider}`,
zIndex: 2000,
}} }}
> >
<Typography variant="subtitle1" fontWeight="bold"> <Typography variant="subtitle1" fontWeight="bold">
@ -192,6 +197,10 @@ export const CreateSightArticle = ({
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
type="text" type="text"
sx={{
zIndex: 2000,
backgroundColor: theme.palette.background.paper,
}}
label="Заголовок *" label="Заголовок *"
/> />

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Close } from "@mui/icons-material"; import { languageStore } from "../store/LanguageStore";
import { import {
Stack, Stack,
Typography, Typography,
@ -88,6 +88,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
type, type,
onSave, onSave,
}: LinkedItemsProps<T>) => { }: LinkedItemsProps<T>) => {
const { language } = languageStore;
const { setArticleModalOpenAction, setArticleIdAction } = articleStore; const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
const { setStationModalOpenAction, setStationIdAction } = stationStore; const { setStationModalOpenAction, setStationIdAction } = stationStore;
const [position, setPosition] = useState<number>(1); const [position, setPosition] = useState<number>(1);
@ -152,7 +153,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
setLinkedItems([]); setLinkedItems([]);
}); });
} }
}, [parentId, parentResource, childResource]); }, [parentId, parentResource, childResource, language]);
useEffect(() => { useEffect(() => {
if (type === "edit") { if (type === "edit") {
@ -354,7 +355,10 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
variant="outlined" variant="outlined"
color="error" color="error"
size="small" size="small"
onClick={() => deleteItem(item.id)} onClick={(e) => {
e.stopPropagation();
deleteItem(item.id);
}}
> >
Отвязать Отвязать
</Button> </Button>
@ -430,7 +434,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
<FormControl fullWidth> <FormControl fullWidth>
<TextField <TextField
type="number" type="number"
label="Номер страницы" label="Позиция добавляемой статьи"
name="page_num" name="page_num"
value={pageNum} value={pageNum}
onChange={(e) => { onChange={(e) => {

View File

@ -4,16 +4,30 @@ import { observer } from "mobx-react-lite";
import { useForm } from "@refinedev/react-hook-form"; import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import "easymde/dist/easymde.min.css"; import "easymde/dist/easymde.min.css";
import { memo, useMemo, useEffect } from "react"; import { memo, useMemo, useEffect, useCallback } from "react";
import { MarkdownEditor } from "../../MarkdownEditor"; import { MarkdownEditor } from "../../MarkdownEditor";
import { Edit } from "@refinedev/mui"; import { Edit } from "@refinedev/mui";
import { languageStore } from "../../../store/LanguageStore"; import { languageStore } from "../../../store/LanguageStore";
import { LanguageSwitch } from "../../LanguageSwitch/index"; import { LanguageSwitch } from "../../LanguageSwitch/index";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { useState } from "react"; import { useState } from "react";
import { useDropzone } from "react-dropzone";
import {
ALLOWED_IMAGE_TYPES,
ALLOWED_VIDEO_TYPES,
} from "../../media/MediaFormUtils";
import { axiosInstance } from "../../../providers/data";
import { TOKEN_KEY } from "../../../authProvider";
const MemoizedSimpleMDE = memo(MarkdownEditor); const MemoizedSimpleMDE = memo(MarkdownEditor);
type MediaFile = {
file: File;
preview: string;
uploading: boolean;
mediaId?: number;
};
const style = { const style = {
position: "absolute", position: "absolute",
top: "50%", top: "50%",
@ -45,12 +59,58 @@ export const ArticleEditModal = observer(() => {
articleStore; articleStore;
const { language } = languageStore; const { language } = languageStore;
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
useEffect(() => { useEffect(() => {
return () => { return () => {
setArticleModalOpenAction(false); setArticleModalOpenAction(false);
}; };
}, []); }, []);
// Load existing media files when editing an article
useEffect(() => {
const loadExistingMedia = async () => {
if (selectedArticleId) {
try {
const response = await axiosInstance.get(
`${
import.meta.env.VITE_KRBL_API
}/article/${selectedArticleId}/media`
);
const existingMedia = response.data;
// Convert existing media to MediaFile format
const mediaFiles = await Promise.all(
existingMedia.map(async (media: any) => {
const response = await fetch(
`${import.meta.env.VITE_KRBL_MEDIA}${
media.id
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
);
const blob = await response.blob();
const file = new File([blob], media.filename, {
type: media.media_type === 1 ? "image/jpeg" : "video/mp4",
});
return {
file,
preview: URL.createObjectURL(blob),
uploading: false,
mediaId: media.id,
};
})
);
setMediaFiles(mediaFiles);
} catch (error) {
console.error("Error loading existing media:", error);
}
}
};
loadExistingMedia();
}, [selectedArticleId]);
const { const {
register, register,
control, control,
@ -66,10 +126,37 @@ export const ArticleEditModal = observer(() => {
action: "edit", action: "edit",
redirect: false, redirect: false,
onMutationSuccess: () => { onMutationSuccess: async () => {
setArticleModalOpenAction(false); try {
reset(); // Upload new media files
window.location.reload(); const newMediaFiles = mediaFiles.filter((file) => !file.mediaId);
const mediaIds = await Promise.all(
newMediaFiles.map(async (mediaFile) => {
return await uploadMedia(mediaFile);
})
);
// Associate all media with the article
await Promise.all(
mediaIds.map((mediaId, index) =>
axiosInstance.post(
`${
import.meta.env.VITE_KRBL_API
}/article/${selectedArticleId}/media/`,
{
media_id: mediaId,
media_order: index + 1,
}
)
)
);
setArticleModalOpenAction(false);
reset();
window.location.reload();
} catch (error) {
console.error("Error handling media:", error);
}
}, },
meta: { meta: {
headers: { headers: {
@ -112,6 +199,65 @@ export const ArticleEditModal = observer(() => {
[] []
); );
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/*": ALLOWED_IMAGE_TYPES,
"video/*": ALLOWED_VIDEO_TYPES,
},
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 removeMedia = async (index: number) => {
const mediaFile = mediaFiles[index];
// If it's an existing media file (has mediaId), delete it from the server
if (mediaFile.mediaId) {
try {
await axiosInstance.delete(
`${import.meta.env.VITE_KRBL_API}/media/${mediaFile.mediaId}`
);
} catch (error) {
console.error("Error deleting media:", error);
return; // Don't remove from UI if server deletion failed
}
}
// Remove from UI and cleanup
setMediaFiles((prev) => {
const newFiles = [...prev];
URL.revokeObjectURL(newFiles[index].preview);
newFiles.splice(index, 1);
return newFiles;
});
};
return ( return (
<Modal <Modal
open={articleModalOpen} open={articleModalOpen}
@ -164,6 +310,88 @@ export const ArticleEditModal = observer(() => {
)} )}
/> />
</Box> </Box>
{/* 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>
</Edit> </Edit>
</Box> </Box>
</Modal> </Modal>

View File

@ -6,11 +6,11 @@ type ModelViewerProps = {
height?: string; height?: string;
}; };
export const ModelViewer = ({ fileUrl, height }: ModelViewerProps) => { export const ModelViewer = ({ fileUrl, height = "80vh" }: ModelViewerProps) => {
const { scene } = useGLTF(fileUrl); const { scene } = useGLTF(fileUrl);
return ( return (
<Canvas style={{ width: "100%", height: "400px" }}> <Canvas style={{ width: "100%", height: height }}>
<ambientLight /> <ambientLight />
<directionalLight /> <directionalLight />
<Stage environment="city" intensity={0.6}> <Stage environment="city" intensity={0.6}>

File diff suppressed because it is too large Load Diff