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,7 +120,8 @@ 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 import.meta.env.VITE_KRBL_API
@ -128,6 +131,7 @@ export const CreateSightArticle = ({
page_num: nextPageNum, 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 () => {
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); setArticleModalOpenAction(false);
reset(); reset();
window.location.reload(); 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}>

View File

@ -6,6 +6,7 @@ import {
Typography, Typography,
Tab, Tab,
Tabs, Tabs,
Button,
} from "@mui/material"; } from "@mui/material";
import { Edit, useAutocomplete } from "@refinedev/mui"; import { Edit, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form"; import { useForm } from "@refinedev/react-hook-form";
@ -23,6 +24,8 @@ import axios from "axios";
import { LanguageSwitch } from "../../components/LanguageSwitch/index"; import { LanguageSwitch } from "../../components/LanguageSwitch/index";
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"; import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
import { ModelViewer } from "../media/ModelViewer"; import { ModelViewer } from "../media/ModelViewer";
import { articleStore } from "../../store/ArticleStore";
import { ArticleEditModal } from "../../components/modals/ArticleEditModal/index";
function a11yProps(index: number) { function a11yProps(index: number) {
return { return {
@ -56,7 +59,8 @@ function CustomTabPanel(props: TabPanelProps) {
export const SightEdit = observer(() => { export const SightEdit = observer(() => {
const { id: sightId } = useParams<{ id: string }>(); const { id: sightId } = useParams<{ id: string }>();
const { language, setLanguageAction } = languageStore; const { language, setLanguageAction } = languageStore;
const [previewSelected, setPreviewSelected] = useState(true);
const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
const [sightData, setSightData] = useState({ const [sightData, setSightData] = useState({
ru: { ru: {
name: "", name: "",
@ -129,13 +133,16 @@ export const SightEdit = observer(() => {
value, value,
}, },
], ],
meta: {
headers: {
"Accept-Language": language,
},
},
}); });
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({ const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
resource: "article", resource: "article",
queryOptions: {
queryKey: ["article", language],
},
onSearch: (value) => [ onSearch: (value) => [
{ {
field: "heading", field: "heading",
@ -148,6 +155,12 @@ export const SightEdit = observer(() => {
value, value,
}, },
], ],
meta: {
headers: {
"Accept-Language": language,
},
},
}); });
useEffect(() => { useEffect(() => {
@ -189,7 +202,7 @@ export const SightEdit = observer(() => {
latitude: "", latitude: "",
longitude: "", longitude: "",
}); });
const [selectedArticleIndex, setSelectedArticleIndex] = useState(0); const [selectedArticleIndex, setSelectedArticleIndex] = useState(-1);
const [cityPreview, setCityPreview] = useState(""); const [cityPreview, setCityPreview] = useState("");
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null); const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>( const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(
@ -198,11 +211,64 @@ export const SightEdit = observer(() => {
const [watermarkRDPreview, setWatermarkRDPreview] = useState<string | null>( const [watermarkRDPreview, setWatermarkRDPreview] = useState<string | null>(
null null
); );
const [leftArticlePreview, setLeftArticlePreview] = useState("");
const [previewArticlePreview, setPreviewArticlePreview] = useState("");
const [linkedArticles, setLinkedArticles] = useState<ArticleItem[]>([]); const [linkedArticles, setLinkedArticles] = useState<ArticleItem[]>([]);
// Следим за изменениями во всех полях // Следим за изменениями во всех полях
const selectedArticle = linkedArticles[selectedArticleIndex]; const selectedArticle = linkedArticles[selectedArticleIndex];
const [previewMedia, setPreviewMedia] = useState<{
src: string;
media_type: number;
filename: string;
} | null>(null);
const [leftArticleMedia, setLeftArticleMedia] = useState<{
src: string;
media_type: number;
filename: string;
} | null>(null);
const previewMediaId = watch("preview_media");
const leftArticleId = watch("left_article");
useEffect(() => {
if (previewMediaId) {
const getMedia = async () => {
try {
const response = await axios.get(
`${import.meta.env.VITE_KRBL_API}/article/${previewMediaId}/media`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
"Accept-Language": language,
},
}
);
const media = response.data[0];
if (media) {
setPreviewMedia({
src: `${import.meta.env.VITE_KRBL_MEDIA}${
media.id
}/download?token=${localStorage.getItem(TOKEN_KEY)}`,
media_type: media.media_type,
filename: media.filename,
});
} else {
setPreviewMedia({
src: "",
media_type: 1,
filename: "",
}); // или другой дефолт
}
} catch (error) {
console.error("Error fetching media:", error);
setPreviewMedia({
src: "",
media_type: 1,
filename: "",
}); // или другой дефолт
}
};
getMedia();
}
}, [previewMediaId]);
const addressContent = watch("address"); const addressContent = watch("address");
const nameContent = watch("name"); const nameContent = watch("name");
@ -244,6 +310,7 @@ export const SightEdit = observer(() => {
{ {
headers: { headers: {
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`, Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
"Accept-Language": language,
}, },
} }
); );
@ -322,18 +389,36 @@ export const SightEdit = observer(() => {
}, [watermarkRDContent, mediaAutocompleteProps.options]); }, [watermarkRDContent, mediaAutocompleteProps.options]);
useEffect(() => { useEffect(() => {
const getMedia = async () => {
const selectedLeftArticle = articleAutocompleteProps.options.find( const selectedLeftArticle = articleAutocompleteProps.options.find(
(option) => option.id === leftArticleContent (option) => option.id === leftArticleContent
); );
setLeftArticlePreview(selectedLeftArticle?.heading || ""); if (!selectedLeftArticle) return;
}, [leftArticleContent, articleAutocompleteProps.options]); const response = await axios.get(
`${import.meta.env.VITE_KRBL_API}/article/${
useEffect(() => { selectedLeftArticle?.id
const selectedPreviewArticle = articleAutocompleteProps.options.find( }/media`,
(option) => option.id === previewArticleContent {
headers: {
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
"Accept-Language": language,
},
}
); );
setPreviewArticlePreview(selectedPreviewArticle?.heading || ""); const media = response.data[0];
}, [previewArticleContent, articleAutocompleteProps.options]); if (media) {
setLeftArticleMedia({
src: `${import.meta.env.VITE_KRBL_MEDIA}${
media.id
}/download?token=${localStorage.getItem(TOKEN_KEY)}`,
media_type: media.media_type,
filename: media.filename,
});
}
};
getMedia();
}, [leftArticleId, leftArticleContent]);
const handleLanguageChange = (lang: string) => { const handleLanguageChange = (lang: string) => {
setSightData((prevData) => ({ setSightData((prevData) => ({
@ -567,6 +652,7 @@ export const SightEdit = observer(() => {
)} )}
/> />
<Box sx={{ display: "none" }}>
<Controller <Controller
control={control} control={control}
name="thumbnail" name="thumbnail"
@ -611,7 +697,52 @@ export const SightEdit = observer(() => {
/> />
)} )}
/> />
</Box>
<Box sx={{ display: "none" }}>
<Controller
control={control}
name="preview_media"
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...mediaAutocompleteProps}
value={
mediaAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
}}
getOptionLabel={(item) => {
return item ? item.media_name : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.media_name
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Выберите водный знак (Левый верх)"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
)}
/>
</Box>
<Box sx={{ display: "none" }}> <Box sx={{ display: "none" }}>
<Controller <Controller
control={control} control={control}
@ -715,6 +846,7 @@ export const SightEdit = observer(() => {
} }
onChange={(_, value) => { onChange={(_, value) => {
field.onChange(value?.id || ""); field.onChange(value?.id || "");
setLeftArticleMedia(null);
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
return item ? item.heading : ""; return item ? item.heading : "";
@ -743,49 +875,6 @@ export const SightEdit = observer(() => {
/> />
)} )}
/> />
<Controller
control={control}
name="preview_article"
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...articleAutocompleteProps}
value={
articleAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
}}
getOptionLabel={(item) => {
return item ? item.heading : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.heading
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Медиа-предпросмотр"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
)}
/>
</Box> </Box>
</Box> </Box>
@ -797,20 +886,99 @@ export const SightEdit = observer(() => {
width: "30%", width: "30%",
top: "179px", top: "179px",
minHeight: "600px",
right: 50, right: 50,
zIndex: 1000, zIndex: 1000,
borderRadius: 2, borderRadius: 2,
border: "1px solid", border: "1px solid",
borderColor: "primary.main", borderColor: "primary.main",
bgcolor: (theme) => bgcolor: "#806c59",
theme.palette.mode === "dark" ? "background.paper" : "#fff",
}} }}
> >
<Typography variant="h6" gutterBottom color="primary"> <Box
Предпросмотр sx={{
</Typography> mb: 2,
margin: "0 auto 40px auto",
display: "flex",
flexDirection: "column",
maxHeight: "300px",
gap: 2,
}}
>
{leftArticleMedia &&
leftArticleMedia.src &&
leftArticleMedia.media_type === 1 && (
<img
src={leftArticleMedia.src}
alt={leftArticleMedia.filename}
style={{
maxWidth: "100%",
height: "300px",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{leftArticleMedia && leftArticleMedia.media_type === 2 && (
<video
src={leftArticleMedia.src}
style={{
maxWidth: "50%",
objectFit: "contain",
borderRadius: 30,
}}
controls
autoPlay
muted
/>
)}
{leftArticleMedia && leftArticleMedia.media_type === 3 && (
<img
src={leftArticleMedia.src}
alt={leftArticleMedia.filename}
style={{
maxWidth: "100%",
height: "40vh",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{leftArticleMedia && leftArticleMedia.media_type === 4 && (
<img
src={leftArticleMedia.src}
alt={leftArticleMedia.filename}
style={{
maxWidth: "100%",
height: "40vh",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{leftArticleMedia &&
leftArticleMedia.src &&
leftArticleMedia.media_type == 5 && (
<ReactPhotoSphereViewer
src={leftArticleMedia.src}
height={"300px"}
width={"350px"}
/>
)}
{leftArticleMedia &&
leftArticleMedia.src &&
leftArticleMedia.media_type === 6 && (
<ModelViewer
height={"400px"}
fileUrl={leftArticleMedia.src}
/>
)}
</Box>
{/* Название достопримечательности */} {/* Название достопримечательности */}
<Typography <Typography
variant="h4" variant="h4"
@ -855,34 +1023,34 @@ export const SightEdit = observer(() => {
{addressContent} {addressContent}
</Box> </Box>
</Typography> </Typography>
{/* Обложка */}
{thumbnailPreview && (
<Box sx={{ mb: 2 }}>
<Typography
variant="body1"
gutterBottom
sx={{ color: "text.secondary" }}
>
Логотип достопримечательности:
</Typography>
<Box
component="img"
src={thumbnailPreview}
alt="Логотип"
sx={{
maxWidth: "100%",
height: "40vh",
borderRadius: 2,
border: "1px solid",
borderColor: "primary.main",
}}
/>
</Box>
)}
</Paper> </Paper>
</Box> </Box>
</Edit> </Edit>
{!leftArticleId && (
<CreateSightArticle
parentId={sightId!}
parentResource="sight"
childResource="article"
title="левую статью"
/>
)}
{leftArticleId && (
<Button
variant="outlined"
size="large"
sx={{
marginTop: 5,
width: "100%",
}}
onClick={() => {
setArticleModalOpenAction(true);
setArticleIdAction(leftArticleId);
}}
color="secondary"
>
Редактировать левую статью
</Button>
)}
</CustomTabPanel> </CustomTabPanel>
<CustomTabPanel value={tabValue} index={1}> <CustomTabPanel value={tabValue} index={1}>
@ -909,18 +1077,52 @@ export const SightEdit = observer(() => {
sx={{ flex: 1, display: "flex", flexDirection: "column" }} sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off" autoComplete="off"
> >
<Controller
control={control}
name="preview_media"
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...articleAutocompleteProps}
value={
articleAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onClick={() => {}}
onChange={(_, value) => {
field.onChange(value?.id || "");
}}
getOptionLabel={(item) => {
return item ? item.heading : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.heading
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField <TextField
{...register("name", { {...params}
required: "Это поле является обязательным", onClick={() => {
})} setPreviewSelected(true);
error={!!(errors as any)?.name} setSelectedArticleIndex(-1);
helperText={(errors as any)?.name?.message} }}
label="Медиа-предпросмотр"
margin="normal" margin="normal"
fullWidth variant="outlined"
InputLabelProps={{ shrink: true }} error={!!errors.arms}
type="text" helperText={(errors as any)?.arms?.message}
label={"Название *"} required
name="name" />
)}
/>
)}
/> />
</Box> </Box>
<LanguageSwitch /> <LanguageSwitch />
@ -941,6 +1143,7 @@ export const SightEdit = observer(() => {
parentResource="sight" parentResource="sight"
childResource="article" childResource="article"
title="статью" title="статью"
left
/> />
</Box> </Box>
</Box> </Box>
@ -951,33 +1154,45 @@ export const SightEdit = observer(() => {
flexDirection: "column", flexDirection: "column",
position: "fixed", position: "fixed",
p: 2, p: 2,
height: "max-content",
width: "30%", width: "30%",
overflowY: "scroll",
height: "80vh",
top: "178px",
top: "178px",
minHeight: "600px",
right: 50, right: 50,
zIndex: 1000, zIndex: 1000,
borderRadius: 2, borderRadius: 2,
border: "1px solid", border: "1px solid",
borderColor: "primary.main",
bgcolor: (theme) => bgcolor: "#806c59",
theme.palette.mode === "dark" ? "background.paper" : "#fff", }}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
gap: 2,
}}
>
{!previewSelected && (
<Box
sx={{
mb: 2,
margin: "0 auto",
display: "flex",
flexDirection: "column",
maxHeight: "300px",
gap: 2,
}} }}
> >
<Typography variant="h6" gutterBottom color="primary">
Предпросмотр
</Typography>
<Box sx={{ mb: 2, margin: "0 auto" }}>
{mediaFile && mediaFile.src && mediaFile.media_type === 1 && ( {mediaFile && mediaFile.src && mediaFile.media_type === 1 && (
<img <img
src={mediaFile.src} src={mediaFile.src}
alt={mediaFile.filename} alt={mediaFile.filename}
style={{ style={{
maxWidth: "100%", maxWidth: "100%",
height: "40vh", height: "300px",
objectFit: "contain", objectFit: "contain",
borderRadius: 8, borderRadius: 8,
}} }}
@ -1026,26 +1241,117 @@ export const SightEdit = observer(() => {
{mediaFile && mediaFile.src && mediaFile.media_type == 5 && ( {mediaFile && mediaFile.src && mediaFile.media_type == 5 && (
<ReactPhotoSphereViewer <ReactPhotoSphereViewer
src={mediaFile.src} src={mediaFile.src}
width={"100%"}
height={"300px"} height={"300px"}
width={"350px"}
/> />
)} )}
{mediaFile && mediaFile.media_type === 6 && ( {mediaFile && mediaFile.media_type === 6 && (
<ModelViewer height={"30%"} fileUrl={mediaFile.src} /> <ModelViewer height={"400px"} fileUrl={mediaFile.src} />
)} )}
</Box> </Box>
{/* Водяные знаки */} )}
{
<Box <Box
sx={{ sx={{
mt: 2,
mb: 2, mb: 2,
flexGrow: 1, flexGrow: 1,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
justifyContent: "space-between", justifyContent: "space-between",
height: "250px",
overflowY: "scroll",
}}
>
{previewSelected &&
previewMedia &&
previewMedia.src &&
previewMedia.media_type === 1 && (
<img
src={previewMedia.src}
alt={previewMedia.filename}
style={{
maxWidth: "100%",
height: "250px",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{previewSelected &&
previewMedia &&
previewMedia.media_type === 2 && (
<video
src={previewMedia.src}
style={{
maxWidth: "50%",
objectFit: "contain",
borderRadius: 30,
}}
controls
autoPlay
muted
/>
)}
{previewSelected &&
previewMedia &&
previewMedia.media_type === 3 && (
<img
src={previewMedia.src}
alt={previewMedia.filename}
style={{
maxWidth: "100%",
height: "250px",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{previewSelected &&
previewMedia &&
previewMedia.media_type === 4 && (
<img
src={previewMedia.src}
alt={previewMedia.filename}
style={{
maxWidth: "100%",
height: "40vh",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{previewSelected &&
previewMedia &&
previewMedia.src &&
previewMedia.media_type == 5 && (
<ReactPhotoSphereViewer
src={previewMedia.src}
height={"300px"}
width={"350px"}
/>
)}
{previewSelected &&
previewMedia &&
previewMedia.media_type === 6 && (
<ModelViewer height={"400px"} fileUrl={previewMedia.src} />
)}
{!previewSelected && (
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
gap: 2,
}} }}
> >
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
{selectedArticle && ( {selectedArticle && (
<Typography <Typography
variant="h4" variant="h4"
@ -1065,38 +1371,41 @@ export const SightEdit = observer(() => {
{selectedArticle.body} {selectedArticle.body}
</Typography> </Typography>
)} )}
{selectedArticle && (
<Typography
variant="body1"
gutterBottom
sx={{ color: "text.primary" }}
>
{selectedArticle.body}
</Typography>
)}
{selectedArticle && (
<Typography
variant="body1"
gutterBottom
sx={{ color: "text.primary" }}
>
{selectedArticle.body}
</Typography>
)}
</Box> </Box>
)}
<Box sx={{ display: "flex", gap: 1, mt: 2 }}> <Box>
<Typography variant="h6" gutterBottom>
Привязанные статьи
</Typography>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
borderRadius: 2,
background:
"linear-gradient(180deg, hsla(0,0%,100%,.2), hsla(0,0%,100%,0)), hsla(29,15%,65%,.4)",
boxShadow: "inset 4px 4px 12px hsla(0,0%,100%,.12)",
gap: 1,
mt: 2,
}}
>
{linkedArticles.map((article, index) => ( {linkedArticles.map((article, index) => (
<Box <Box
key={article.id} key={article.id}
onClick={() => setSelectedArticleIndex(index)} onClick={() => {
setSelectedArticleIndex(index);
setPreviewSelected(false);
}}
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
bgcolor: bgcolor:
selectedArticleIndex === index selectedArticleIndex === index
? "primary.main" ? "primary.main"
: "transparent", : "transparent",
color: selectedArticleIndex === index ? "white" : "inherit", color:
selectedArticleIndex === index
? "white"
: "inherit",
p: 1, p: 1,
borderRadius: 1, borderRadius: 1,
}} }}
@ -1108,6 +1417,9 @@ export const SightEdit = observer(() => {
))} ))}
</Box> </Box>
</Box> </Box>
</Box>
}
</Box>
</Paper> </Paper>
</CustomTabPanel> </CustomTabPanel>
@ -1134,7 +1446,6 @@ export const SightEdit = observer(() => {
sx={{ flex: 1, display: "flex", flexDirection: "column" }} sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off" autoComplete="off"
> >
<LanguageSwitch />
<Controller <Controller
control={control} control={control}
name="thumbnail" name="thumbnail"
@ -1144,8 +1455,7 @@ export const SightEdit = observer(() => {
{...mediaAutocompleteProps} {...mediaAutocompleteProps}
value={ value={
mediaAutocompleteProps.options.find( mediaAutocompleteProps.options.find(
(option) => (option) => option.id === field.value
option.id === field.value && option.media_type === 3
) || null ) || null
} }
onChange={(_, value) => { onChange={(_, value) => {
@ -1158,16 +1468,18 @@ export const SightEdit = observer(() => {
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter((option) => return options.filter(
(option) =>
option.media_name option.media_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase()) &&
option.media_type === 3
); );
}} }}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
label="Выберите водный знак (Левый верх)" label="Выберите логотип достопримечательности"
margin="normal" margin="normal"
variant="outlined" variant="outlined"
error={!!errors.arms} error={!!errors.arms}
@ -1201,10 +1513,12 @@ export const SightEdit = observer(() => {
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter((option) => return options.filter(
(option) =>
option.media_name option.media_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase()) &&
option.media_type === 4
); );
}} }}
renderInput={(params) => ( renderInput={(params) => (
@ -1244,10 +1558,12 @@ export const SightEdit = observer(() => {
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter((option) => return options.filter(
(option) =>
option.media_name option.media_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase()) &&
option.media_type === 4
); );
}} }}
renderInput={(params) => ( renderInput={(params) => (
@ -1405,6 +1721,7 @@ export const SightEdit = observer(() => {
</Box> </Box>
</Edit> </Edit>
</CustomTabPanel> </CustomTabPanel>
<ArticleEditModal />
</Box> </Box>
); );
}); });