WhiteNightsAdminPanel/src/pages/sight/edit.tsx
itoshi 3268f98240
All checks were successful
release-tag / release-image (push) Successful in 2m13s
feat: Add more margin and width for image
2025-05-28 21:04:19 +03:00

1692 lines
62 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 {
Autocomplete,
Box,
TextField,
Paper,
Typography,
Tab,
Tabs,
Button,
Stack,
} from "@mui/material";
import { Edit, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Controller, FieldValues } from "react-hook-form";
import { Link, useParams } from "react-router";
import React, { useState, useEffect } from "react";
import { CreateSightArticle, LinkedItemsContents } from "@components";
import { ArticleItem, articleFields } from "./types";
import { axiosInstance, TOKEN_KEY } from "@providers";
import { observer } from "mobx-react-lite";
import {
Languages,
languageStore,
articleStore,
META_LANGUAGE,
EVERY_LANGUAGE,
} from "@stores";
import axios from "axios";
import { LanguageSelector, MediaData, MediaView } from "@ui";
import { ArticleEditModal } from "../../components/modals/ArticleEditModal/index";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function CustomTabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
export const SightEdit = observer(() => {
const { id: sightId } = useParams<{ id: string }>();
const { language, setLanguageAction } = languageStore;
const [previewSelected, setPreviewSelected] = useState(true);
const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
const [sightData, setSightData] = useState({
name: EVERY_LANGUAGE(""),
address: EVERY_LANGUAGE(""),
});
const [coordinates, setCoordinates] = useState("");
const {
saveButtonProps,
register,
refineCore: { onFinish },
control,
watch,
getValues,
setValue,
handleSubmit,
formState: { errors },
} = useForm({
refineCoreProps: META_LANGUAGE(language),
warnWhenUnsavedChanges: false,
});
const name = watch("name");
const getMedia = async (id?: string | number) => {
if (!id) return;
try {
const response = await axios.get(
`${import.meta.env.VITE_KRBL_API}/article/${id}/media`,
{
headers: {
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
"Accept-Language": language,
},
}
);
return response.data[0];
} catch {
return;
}
};
useEffect(() => {
setLanguageAction("ru");
}, []);
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city",
onSearch: (value) => [
{
field: "name",
operator: "contains",
value,
},
],
...META_LANGUAGE("ru"),
});
const [mediaFile, setMediaFile] = useState<MediaData>();
const [leftArticleData, setLeftArticleData] = useState<{
heading: string;
body: string;
media: MediaData;
}>();
const [tabValue, setTabValue] = useState(0);
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
resource: "media",
onSearch: (value) => [
{
field: "media_name",
operator: "contains",
value,
},
],
});
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
resource: "article",
onSearch: (value) => [
{
field: "heading",
operator: "contains",
value,
},
{
field: "media_type",
operator: "contains",
value,
},
],
});
useEffect(() => {
if (sightData.name[language]) setValue("name", sightData.name[language]);
if (sightData.address[language])
setValue("address", sightData.address[language]);
}, [language, sightData, setValue]);
// Состояния для предпросмотра
const [creatingArticleHeading, setCreatingArticleHeading] =
useState<string>("");
const [creatingArticleBody, setCreatingArticleBody] = useState<string>("");
const [coordinatesPreview, setCoordinatesPreview] = useState("");
const [selectedArticleIndex, setSelectedArticleIndex] = useState(-1);
const [previewMediaFile, setPreviewMediaFile] = useState<MediaData>();
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(
null
);
const [watermarkRDPreview, setWatermarkRDPreview] = useState<string | null>(
null
);
const [linkedArticles, setLinkedArticles] = useState<ArticleItem[]>([]);
// Следим за изменениями во всех полях
const selectedArticle = linkedArticles[selectedArticleIndex];
const previewMediaId = watch("preview_media");
const leftArticleId = watch("left_article");
useEffect(() => {
if (previewMediaId) {
const selectedMedia = mediaAutocompleteProps.options.find(
(option) => option.id === previewMediaId
);
console.log("Triggering", previewMediaId);
if (!selectedMedia) return;
setPreviewMediaFile(selectedMedia);
}
}, [previewMediaId, mediaAutocompleteProps.options]);
// useEffect(() => {
// const selectedWatermarkLU = mediaAutocompleteProps.options.find(
// (option) => option.id === watermarkLUContent
// );
// setWatermarkLUPreview(
// selectedWatermarkLU
// ? `${import.meta.env.VITE_KRBL_MEDIA}${
// selectedWatermarkLU.id
// }/download?token=${localStorage.getItem(TOKEN_KEY)}`
// : null
// );
// }, [watermarkLUContent, ]);
const addressContent = watch("address");
const latitudeContent = watch("latitude");
const longitudeContent = watch("longitude");
const thumbnailContent = watch("thumbnail");
const watermarkLUContent = watch("watermark_lu");
const watermarkRDContent = watch("watermark_rd");
useEffect(() => {
if (latitudeContent && longitudeContent) {
setCoordinates(`${latitudeContent} ${longitudeContent}`);
}
}, [latitudeContent, longitudeContent]);
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCoordinates(e.target.value);
if (e.target.value) {
const [lat, lon] = e.target.value.split(" ").map((s) => s.trim());
setCoordinatesPreview(`${lat ?? "0"}, ${lon ?? "0"}`);
setValue("latitude", lat ?? "");
setValue("longitude", lon ?? "");
} else {
setCoordinatesPreview("");
setValue("latitude", "");
setValue("longitude", "");
}
};
useEffect(() => {
if (linkedArticles[selectedArticleIndex]?.id) {
getMedia(linkedArticles[selectedArticleIndex].id).then((media) => {
setMediaFile(media);
});
}
}, [selectedArticleIndex, linkedArticles]);
useEffect(() => {
const selectedThumbnail = mediaAutocompleteProps.options.find(
(option) => option.id === thumbnailContent
);
setThumbnailPreview(
selectedThumbnail
? `${import.meta.env.VITE_KRBL_MEDIA}${
selectedThumbnail.id
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
: null
);
}, [thumbnailContent, mediaAutocompleteProps.options]);
useEffect(() => {
const selectedWatermarkLU = mediaAutocompleteProps.options.find(
(option) => option.id === watermarkLUContent
);
setWatermarkLUPreview(
selectedWatermarkLU
? `${import.meta.env.VITE_KRBL_MEDIA}${
selectedWatermarkLU.id
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
: null
);
}, [watermarkLUContent, mediaAutocompleteProps.options]);
useEffect(() => {
const selectedWatermarkRD = mediaAutocompleteProps.options.find(
(option) => option.id === watermarkRDContent
);
setWatermarkRDPreview(
selectedWatermarkRD
? `${import.meta.env.VITE_KRBL_MEDIA}${
selectedWatermarkRD.id
}/download?token=${localStorage.getItem(TOKEN_KEY)}`
: null
);
}, [watermarkRDContent, mediaAutocompleteProps.options]);
useEffect(() => {
const selectedLeftArticle = articleAutocompleteProps.options.find(
(option) => option.id === leftArticleId
);
if (!selectedLeftArticle?.id) return;
getMedia(selectedLeftArticle.id).then((media) => {
setLeftArticleData({
heading: selectedLeftArticle.heading,
body: selectedLeftArticle.body,
media,
});
});
}, [leftArticleId, articleAutocompleteProps.loading]);
function updateTranslations(update: boolean = true) {
const newSightData = {
...sightData,
name: {
...sightData.name,
[language]: watch("name") ?? "",
},
address: {
...sightData.address,
[language]: watch("address") ?? "",
},
};
if (update) setSightData(newSightData);
return newSightData;
}
const handleLanguageChange = (lang: Languages) => {
updateTranslations();
setLanguageAction(lang);
};
const handleFormSubmit = handleSubmit(async (values: FieldValues) => {
const newTranslations = updateTranslations(false);
console.log(newTranslations);
await onFinish({
...values,
translations: newTranslations,
});
});
useEffect(() => {
return () => {
setLanguageAction("ru");
};
}, [setLanguageAction]);
const [articleAdditionMode, setArticleAdditionMode] = useState<
"attaching" | "creating"
>("attaching");
const [selectedItemId, setSelectedItemId] = useState<string>();
const [updatedLinkedArticles, setUpdatedLinkedArticles] = useState<
ArticleItem[]
>([]);
const linkItem = () => {
if (!selectedItemId) return;
const requestData = {
article_id: selectedItemId,
page_num: linkedArticles.length + 1,
};
axiosInstance
.post(
`${import.meta.env.VITE_KRBL_API}/sight/${sightId}/article`,
requestData
)
.then(() => {
axiosInstance
.get(`${import.meta.env.VITE_KRBL_API}/sight/${sightId}/article`)
.then((response) => {
setUpdatedLinkedArticles(response?.data || []);
setSelectedItemId(undefined);
});
})
.catch((error) => {
console.error("Error linking item:", error);
});
};
return (
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={tabValue}
onChange={(_, newValue) => setTabValue(newValue)}
aria-label="basic tabs example"
>
<Tab label="Левый виджет" {...a11yProps(1)} />
<Tab label="Правый виджет" {...a11yProps(2)} />
<Tab label="Основная информация" {...a11yProps(3)} />
</Tabs>
</Box>
<CustomTabPanel value={tabValue} index={0}>
<Edit
saveButtonProps={{
...saveButtonProps,
onClick: handleFormSubmit,
}}
footerButtonProps={{
sx: {
bottom: 0,
left: 0,
},
}}
>
<Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}>
<Box
sx={{ display: "flex", gap: 2, position: "relative", flex: 1 }}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
flex: 1,
gap: 10,
justifyContent: "space-between",
}}
>
<LanguageSelector action={handleLanguageChange} />
{/* Форма редактирования */}
<Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("name", {
//required: "Это поле является обязательным",
})}
error={!!(errors as any)?.name}
helperText={(errors as any)?.name?.message}
margin="normal"
fullWidth
slotProps={{ inputLabel: { shrink: true } }}
type="text"
label={"Название"}
name="name"
/>
<TextField
{...register("address", {
//required: "Это поле является обязательным",
})}
error={!!(errors as any)?.address}
helperText={(errors as any)?.address?.message}
margin="normal"
fullWidth
slotProps={{ inputLabel: { shrink: true } }}
type="text"
label={"Адрес"}
name="address"
/>
<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}
onClick={() => {
//setPreviewSelected(true);
//setSelectedMediaIndex(-1);
}}
label="Медиа-предпросмотр"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
/>
)}
/>
)}
/>
</Box>
<input
type="hidden"
{...register("longitude", {
required: "Это поле является обязательным",
valueAsNumber: true,
})}
/>
<input
type="hidden"
{...register("latitude", {
required: "Это поле является обязательным",
valueAsNumber: true,
})}
/>
<Controller
control={control}
name="city_id"
rules={{ required: "Это поле является обязательным" }}
defaultValue={null}
render={() => <div />}
/>
<Box sx={{ display: "none" }}>
<Controller
control={control}
name="thumbnail"
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()) &&
option.media_type === 3
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Выберите логотип достопримечательности"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
)}
/>
</Box>
<Box sx={{ display: "none" }}>
<Controller
control={control}
name="watermark_lu"
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" }}>
<Controller
control={control}
name="watermark_rd"
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>
<Controller
control={control}
name="left_article"
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...articleAutocompleteProps}
value={
articleAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
setLeftArticleData(undefined);
}}
getOptionLabel={(item) => {
return item ? item.service_name : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.service_name
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Левая статья"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
/>
)}
/>
)}
/>
{leftArticleId ? (
<Button
variant="outlined"
size="large"
sx={{
width: "100%",
}}
onClick={() => {
setArticleModalOpenAction(true);
setArticleIdAction(leftArticleId);
}}
color="secondary"
>
Редактировать выбранную левую статью
</Button>
) : (
<>
<Typography
variant="h6"
gutterBottom
px={2}
py={0.5}
sx={{
color: "text.primary",
}}
>
Создать и прикрепить новую статью:
</Typography>
<CreateSightArticle
language={language}
parentId={sightId!}
parentResource="sight"
childResource="article"
title="статью"
left
setHeadingParent={(heading) => {
//console.log("Updating", heading)
setCreatingArticleHeading(heading);
}}
setBodyParent={(body) => {
setCreatingArticleBody(body);
}}
/>
</>
// <Link to="/article/create">
// <Button
// variant="outlined"
// size="large"
// sx={{
// width: "100%",
// }}
// color="secondary"
// >
// Создать новую статью
// </Button>
// </Link>
)}
</Box>
</Box>
</Box>
<Paper
sx={{
p: 2,
width: "30%",
top: "179px",
minHeight: "600px",
right: 50,
zIndex: 1000,
borderRadius: 2,
border: "1px solid",
borderColor: "primary.main",
bgcolor: "#806c59",
}}
>
{leftArticleData?.media && (
<MediaView media={leftArticleData.media} />
)}
{/* Заголовок статьи */}
<Typography
variant="h4"
sx={{
wordWrap: "break-word",
color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
mb: 3,
mt: 3,
}}
>
{name}
</Typography>
{/* Адрес */}
<Typography variant="body1" sx={{ mb: 2 }}>
<Box component="span" sx={{ color: "text.secondary" }}>
Адрес:{" "}
</Box>
<Box
component="span"
sx={{
color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
}}
>
{addressContent}
</Box>
</Typography>
<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 rehypePlugins={[rehypeRaw]}>
{leftArticleId ? leftArticleData?.body : creatingArticleBody}
</ReactMarkdown>
</Box>
</Paper>
</Box>
</Edit>
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={1}>
<Edit
saveButtonProps={{
...saveButtonProps,
onClick: handleFormSubmit,
}}
footerButtonProps={{
sx: { bottom: 0, left: 0 },
}}
>
<Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}>
<Box sx={{ flex: 1, gap: 2, position: "relative" }}>
<Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<Controller
control={control}
name="preview_media"
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...mediaAutocompleteProps}
value={
mediaAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
console.log(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}
onClick={() => {
setPreviewSelected(true);
//setSelectedMediaIndex(-1);
}}
label="Медиа-предпросмотр"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
/>
)}
/>
)}
/>
</Box>
<LanguageSelector action={handleLanguageChange} />
<Box sx={{ mt: 3 }}>
<Box sx={{ mb: 2 }}>
<Box sx={{ display: "flex", gap: 2, height: "min-content" }}>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor:
articleAdditionMode === "attaching"
? "primary.main"
: "transparent",
color:
articleAdditionMode === "attaching"
? "white"
: "inherit",
borderRadius: 1,
p: 1,
}}
onClick={() => setArticleAdditionMode("attaching")}
>
Добавить существующую статью
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor:
articleAdditionMode === "creating"
? "primary.main"
: "transparent",
color:
articleAdditionMode === "creating"
? "white"
: "inherit",
borderRadius: 1,
p: 1,
}}
onClick={() => setArticleAdditionMode("creating")}
>
Создать и привязать новую статью
</Box>
</Box>
</Box>
{articleAdditionMode === "attaching" && (
<Stack gap={2} mt={2}>
<Autocomplete
{...articleAutocompleteProps}
value={
articleAutocompleteProps.options.find(
(option) => option.id === selectedItemId
) || null
}
onChange={(_, value) => {
setSelectedItemId(value?.id || "");
setLeftArticleData(undefined);
}}
getOptionLabel={(item) => {
return item ? item.service_name : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.service_name
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Добавляемая статья"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
/>
)}
/>
<Box sx={{ mt: -2, display: "flex", gap: 2 }}>
<Button
variant="contained"
onClick={linkItem}
disabled={!selectedItemId}
sx={{ alignSelf: "flex-start" }}
>
Добавить
</Button>
<Button
variant="outlined"
onClick={() => setSelectedItemId(undefined)}
>
Очистить
</Button>
</Box>
</Stack>
)}
{articleAdditionMode === "creating" && (
<CreateSightArticle
language={language}
parentId={sightId!}
parentResource="sight"
childResource="article"
title="статью"
//left
setHeadingParent={(heading) => {
console.log("Updating", heading);
setCreatingArticleHeading(heading);
}}
setBodyParent={(body) => {
setCreatingArticleBody(body);
}}
/>
)}
<Typography
variant="subtitle1"
fontWeight="bold"
sx={{ mt: 4 }}
>
Привязанные статьи
</Typography>
<LinkedItemsContents<ArticleItem>
type="edit"
disableCreation
parentId={sightId!}
dragAllowed={true}
setItemsParent={setLinkedArticles}
parentResource="sight"
fields={articleFields}
childResource="article"
title="статьи"
updatedLinkedItems={updatedLinkedArticles}
/>
</Box>
</Box>
{/* Предпросмотр */}
<Paper
sx={{
display: "flex",
flexDirection: "column",
p: 0,
height: "max-content",
minWidth: "400px",
width: "30%",
top: "178px",
maxHeight: "600px",
overflowY: "auto",
right: 50,
zIndex: 1000,
borderRadius: 2,
border: "1px solid",
bgcolor: "#806c59",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
}}
>
<Box
sx={{
mb: 2,
//margin: "0 auto",
display: "flex",
flexDirection: "column",
maxHeight: "300px",
gap: 2,
}}
>
{previewSelected && previewMediaFile && (
<MediaView media={previewMediaFile} />
)}
{mediaFile && !previewSelected && (
<MediaView media={mediaFile} />
)}
</Box>
{
<Box
sx={{
//mt: "auto",
flexGrow: 1,
mb: 0,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
//height: "250px",
overflowY: "auto",
}}
>
{!previewSelected && articleAdditionMode !== "creating" && (
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
gap: 2,
}}
>
{selectedArticle && (
<Typography
variant="h4"
gutterBottom
px={2}
py={0.5}
sx={{
color: "text.primary",
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)",
}}
>
{selectedArticle.heading}
</Typography>
)}
{selectedArticle && (
<Box
sx={{
mt: -6,
p: 2,
"& 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 rehypePlugins={[rehypeRaw]}>
{selectedArticle.body}
</ReactMarkdown>
</Box>
)}
</Box>
)}
{articleAdditionMode === "creating" && (
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
gap: 2,
}}
>
<Typography
variant="h4"
gutterBottom
px={2}
py={0.5}
sx={{
color: "text.primary",
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)",
}}
>
{creatingArticleHeading}
</Typography>
<Box
sx={{
mt: -6,
p: 2,
"& 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 rehypePlugins={[rehypeRaw]}>
{creatingArticleBody}
</ReactMarkdown>
</Box>
</Box>
)}
<Box sx={{ mt: "auto" }}>
<Box
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
margin: "0 auto",
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)",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
borderRadius: 2,
}}
>
{linkedArticles.map((article, index) => (
<Box
key={article.id}
onClick={() => {
setSelectedArticleIndex(index);
setPreviewSelected(false);
}}
sx={{
cursor: "pointer",
bgcolor: "transparent",
color: "inherit",
textDecoration:
selectedArticleIndex === index
? "underline"
: "none",
p: 1,
borderRadius: 1,
}}
>
<Typography variant="body1">
{article.heading}
</Typography>
</Box>
))}
</Box>
</Box>
</Box>
</Box>
}
</Box>
</Paper>
</Box>
</Edit>
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={2}>
<Edit
saveButtonProps={{ ...saveButtonProps, onClick: handleFormSubmit }}
footerButtonProps={{ sx: { bottom: 0, left: 0 } }}
>
<Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}>
<Box
sx={{ display: "flex", flex: 1, gap: 2, position: "relative" }}
>
<Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<LanguageSelector action={handleLanguageChange} />
<Controller
control={control}
name="city_id"
//rules={{ required: "Это поле является обязательным" }}
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...cityAutocompleteProps}
value={
cityAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
}}
getOptionLabel={(item) => {
return item ? item.name : "";
}}
isOptionEqualToValue={(option, value) => {
console.log(cityAutocompleteProps.options);
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.name
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Выберите город"
margin="normal"
variant="outlined"
error={!!errors.city_id}
helperText={(errors as any)?.city_id?.message}
/>
)}
/>
)}
/>
<Controller
control={control}
name="thumbnail"
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()) &&
option.media_type === 3
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Выберите логотип достопримечательности"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
/>
)}
/>
)}
/>
<Controller
control={control}
name="watermark_lu"
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()) &&
option.media_type === 4
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Выберите водяной знак (Левый верх)"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
/>
)}
/>
)}
/>
<Controller
control={control}
name="watermark_rd"
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()) &&
option.media_type === 4
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Выберите водяной знак (Правый вверх)"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
/>
)}
/>
)}
/>
<TextField
value={coordinates}
onChange={handleCoordinatesChange}
error={!!(errors as any)?.latitude}
helperText={(errors as any)?.latitude?.message}
margin="normal"
fullWidth
slotProps={{ inputLabel: { shrink: true } }}
type="text"
label={"Координаты *"}
/>
</Box>
{/* Предпросмотр */}
<Paper
sx={{
p: 2,
minWidth: "fit-content",
width: "30%",
top: "178px",
right: 50,
zIndex: 1000,
borderRadius: 2,
border: "1px solid",
borderColor: "primary.main",
bgcolor: (theme) =>
theme.palette.mode === "dark" ? "background.paper" : "#fff",
}}
>
<Typography variant="h6" gutterBottom color="primary">
Предпросмотр
</Typography>
{thumbnailPreview && (
<Box>
<Typography
variant="body1"
sx={{ display: "flex", flexDirection: "column", mb: 2 }}
>
<Box component="span" sx={{ color: "text.secondary" }}>
Адрес:{" "}
</Box>
<Box
component="span"
sx={{
color: (theme) =>
theme.palette.mode === "dark"
? "grey.300"
: "grey.800",
}}
>
{`${addressContent}`}
</Box>
</Typography>
<Typography
variant="body2"
gutterBottom
sx={{ color: "text.secondary" }}
>
Логотип достопримечательности:
</Typography>
<Box
component="img"
src={thumbnailPreview}
alt="Логотип достопримечательности"
sx={{
width: "50%",
objectFit: "cover",
borderRadius: 1,
marginBottom: 2,
}}
/>
</Box>
)}
{/* Водяные знаки */}
<Box sx={{ mb: 2 }}>
<Typography
variant="body1"
gutterBottom
sx={{ color: "text.secondary" }}
>
Водяные знаки:
</Typography>
<Box sx={{ display: "flex", gap: 2 }}>
{watermarkLUPreview && (
<Box>
<Typography
variant="body2"
gutterBottom
sx={{ color: "text.secondary" }}
>
Левый верхний:
</Typography>
<Box
component="img"
src={watermarkLUPreview}
alt="Водяной знак (ЛВ)"
sx={{
width: 100,
height: 100,
objectFit: "cover",
borderRadius: 1,
border: "1px solid",
borderColor: "primary.main",
}}
/>
</Box>
)}
{watermarkRDPreview && (
<Box>
<Typography
variant="body2"
gutterBottom
sx={{ color: "text.secondary" }}
>
Правый верхний:
</Typography>
<Box
component="img"
src={watermarkRDPreview}
alt="Водяной знак (ПН)"
sx={{
width: 100,
height: 100,
objectFit: "cover",
borderRadius: 1,
border: "1px solid",
borderColor: "primary.main",
}}
/>
</Box>
)}
</Box>
{/* Координаты */}
<Typography
variant="body1"
sx={{ display: "flex", flexDirection: "column", mb: 2 }}
>
<Box component="span" sx={{ color: "text.secondary" }}>
Координаты:{" "}
</Box>
<Box
component="span"
sx={{
color: (theme) =>
theme.palette.mode === "dark"
? "grey.300"
: "grey.800",
}}
>
{coordinatesPreview}
</Box>
</Typography>
</Box>
</Paper>
</Box>
</Box>
</Edit>
</CustomTabPanel>
<ArticleEditModal />
</Box>
);
});