Merge branch 'update' into preview
This commit is contained in:
commit
fb16891de3
@ -33,6 +33,7 @@ type Props = {
|
||||
parentResource: string;
|
||||
childResource: string;
|
||||
title: string;
|
||||
left?: boolean;
|
||||
};
|
||||
|
||||
export const CreateSightArticle = ({
|
||||
@ -40,6 +41,7 @@ export const CreateSightArticle = ({
|
||||
parentResource,
|
||||
childResource,
|
||||
title,
|
||||
left,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
|
||||
@ -118,16 +120,18 @@ export const CreateSightArticle = ({
|
||||
const existingItems = existingItemsResponse.data || [];
|
||||
const nextPageNum = existingItems.length + 1;
|
||||
|
||||
// Привязываем статью к достопримечательности
|
||||
await axiosInstance.post(
|
||||
`${
|
||||
import.meta.env.VITE_KRBL_API
|
||||
}/${parentResource}/${parentId}/${childResource}/`,
|
||||
{
|
||||
[`${childResource}_id`]: itemId,
|
||||
page_num: nextPageNum,
|
||||
}
|
||||
);
|
||||
if (!left) {
|
||||
// Привязываем статью к достопримечательности если она не левая
|
||||
await axiosInstance.post(
|
||||
`${
|
||||
import.meta.env.VITE_KRBL_API
|
||||
}/${parentResource}/${parentId}/${childResource}/`,
|
||||
{
|
||||
[`${childResource}_id`]: itemId,
|
||||
page_num: nextPageNum,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Загружаем все медиа файлы и получаем их ID
|
||||
const mediaIds = await Promise.all(
|
||||
@ -174,6 +178,7 @@ export const CreateSightArticle = ({
|
||||
marginTop: 2,
|
||||
background: theme.palette.background.paper,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
zIndex: 2000,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight="bold">
|
||||
@ -192,6 +197,10 @@ export const CreateSightArticle = ({
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
type="text"
|
||||
sx={{
|
||||
zIndex: 2000,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}}
|
||||
label="Заголовок *"
|
||||
/>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Close } from "@mui/icons-material";
|
||||
import { languageStore } from "../store/LanguageStore";
|
||||
import {
|
||||
Stack,
|
||||
Typography,
|
||||
@ -88,6 +88,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||
type,
|
||||
onSave,
|
||||
}: LinkedItemsProps<T>) => {
|
||||
const { language } = languageStore;
|
||||
const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
|
||||
const { setStationModalOpenAction, setStationIdAction } = stationStore;
|
||||
const [position, setPosition] = useState<number>(1);
|
||||
@ -152,7 +153,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||
setLinkedItems([]);
|
||||
});
|
||||
}
|
||||
}, [parentId, parentResource, childResource]);
|
||||
}, [parentId, parentResource, childResource, language]);
|
||||
|
||||
useEffect(() => {
|
||||
if (type === "edit") {
|
||||
@ -354,7 +355,10 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => deleteItem(item.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteItem(item.id);
|
||||
}}
|
||||
>
|
||||
Отвязать
|
||||
</Button>
|
||||
@ -430,7 +434,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Номер страницы"
|
||||
label="Позиция добавляемой статьи"
|
||||
name="page_num"
|
||||
value={pageNum}
|
||||
onChange={(e) => {
|
||||
|
@ -4,16 +4,30 @@ import { observer } from "mobx-react-lite";
|
||||
import { useForm } from "@refinedev/react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
import "easymde/dist/easymde.min.css";
|
||||
import { memo, useMemo, useEffect } from "react";
|
||||
import { memo, useMemo, useEffect, useCallback } from "react";
|
||||
import { MarkdownEditor } from "../../MarkdownEditor";
|
||||
import { Edit } from "@refinedev/mui";
|
||||
import { languageStore } from "../../../store/LanguageStore";
|
||||
import { LanguageSwitch } from "../../LanguageSwitch/index";
|
||||
import { useNavigate } from "react-router";
|
||||
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);
|
||||
|
||||
type MediaFile = {
|
||||
file: File;
|
||||
preview: string;
|
||||
uploading: boolean;
|
||||
mediaId?: number;
|
||||
};
|
||||
|
||||
const style = {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
@ -45,12 +59,58 @@ export const ArticleEditModal = observer(() => {
|
||||
articleStore;
|
||||
const { language } = languageStore;
|
||||
|
||||
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
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 {
|
||||
register,
|
||||
control,
|
||||
@ -66,10 +126,37 @@ export const ArticleEditModal = observer(() => {
|
||||
action: "edit",
|
||||
redirect: false,
|
||||
|
||||
onMutationSuccess: () => {
|
||||
setArticleModalOpenAction(false);
|
||||
reset();
|
||||
window.location.reload();
|
||||
onMutationSuccess: async () => {
|
||||
try {
|
||||
// Upload new media files
|
||||
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: {
|
||||
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 (
|
||||
<Modal
|
||||
open={articleModalOpen}
|
||||
@ -164,6 +310,88 @@ export const ArticleEditModal = observer(() => {
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
@ -6,11 +6,11 @@ type ModelViewerProps = {
|
||||
height?: string;
|
||||
};
|
||||
|
||||
export const ModelViewer = ({ fileUrl, height }: ModelViewerProps) => {
|
||||
export const ModelViewer = ({ fileUrl, height = "80vh" }: ModelViewerProps) => {
|
||||
const { scene } = useGLTF(fileUrl);
|
||||
|
||||
return (
|
||||
<Canvas style={{ width: "100%", height: "400px" }}>
|
||||
<Canvas style={{ width: "100%", height: height }}>
|
||||
<ambientLight />
|
||||
<directionalLight />
|
||||
<Stage environment="city" intensity={0.6}>
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user