WhiteNightsAdminPanel/src/pages/sight/edit.tsx
2025-05-20 00:09:57 +03:00

1542 lines
54 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";
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 {
saveButtonProps,
register,
refineCore: {onFinish},
control,
watch,
getValues,
setValue,
handleSubmit,
formState: { errors },
} = useForm({
refineCoreProps: META_LANGUAGE(language),
warnWhenUnsavedChanges: false
});
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]);
useEffect(() => {
const latitude = getValues("latitude");
const longitude = getValues("longitude");
if (latitude && longitude) {
setCoordinatesPreview({
latitude: latitude,
longitude: longitude,
});
}
}, [getValues]);
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const [lat, lon] = e.target.value.split(",").map((s) => s.trim());
setCoordinatesPreview({
latitude: lat,
longitude: lon,
});
setValue("latitude", lat);
setValue("longitude", lon);
};
// Состояния для предпросмотра
const [creatingArticleHeading, setCreatingArticleHeading] = useState<string>("");
const [creatingArticleBody, setCreatingArticleBody] = useState<string>("");
const [coordinatesPreview, setCoordinatesPreview] = useState({
latitude: "",
longitude: "",
});
const [selectedArticleIndex, setSelectedArticleIndex] = useState(-1);
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;
setMediaFile(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(() => {
setCoordinatesPreview({
latitude: latitudeContent ?? "",
longitude: longitudeContent ?? "",
});
}, [latitudeContent, longitudeContent]);
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"
/>
<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", {
value: coordinatesPreview.longitude,
required: "Это поле является обязательным",
valueAsNumber: true,
})}
/>
<input
type="hidden"
{...register("latitude", {
value: coordinatesPreview.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.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}
/>
)}
/>
)}
/>
{leftArticleId ? (
<Button
variant="outlined"
size="large"
sx={{
width: "100%",
}}
onClick={() => {
setArticleModalOpenAction(true);
setArticleIdAction(leftArticleId);
}}
color="secondary"
>
Редактировать выбранную левую статью
</Button>
) : (
<>
<Typography
variant="h6"
gutterBottom
px={2}
py={.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",
}}
>
<Box
sx={{
mb: 2,
margin: "0 auto 40px auto",
display: "flex",
flexDirection: "column",
maxHeight: "300px",
gap: 2,
}}
>
{leftArticleData?.media && (
<MediaView media={leftArticleData.media} />
)}
</Box>
{/* Заголовок статьи */}
<Typography
variant="h4"
sx={{
wordWrap: "break-word",
color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
mb: 3,
}}
>
{leftArticleId ? leftArticleData?.heading : creatingArticleHeading}
</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>
{/* Текст статьи */}
<Typography variant="body1" sx={{ mb: 2 }}>
<Box
component="span"
sx={{
color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
}}
>
{leftArticleId ? leftArticleData?.body : creatingArticleBody}
</Box>
</Typography>
</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()) &&
[1,2,5,6].includes(option.media_type)
);
}}
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.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}
/>
)}
/>
<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",
minHeight: "600px",
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,
}}
>
{mediaFile && (
<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={.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 && (
<Typography
variant="body1"
gutterBottom
px={2}
sx={{ color: "text.primary" }}
>
{selectedArticle.body}
</Typography>
)}
</Box>
)}
{articleAdditionMode === "creating" && (
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
gap: 2,
}}
>
<Typography
variant="h4"
gutterBottom
px={2}
py={.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>
<Typography
variant="body1"
gutterBottom
px={2}
sx={{ color: "text.primary" }}
>
{creatingArticleBody}
</Typography>
</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} />
<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"
/>
<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={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}
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.latitude}, ${coordinatesPreview.longitude}`}
</Box>
</Typography>
</Box>
</Paper>
</Box>
</Box>
</Edit>
</CustomTabPanel>
<ArticleEditModal />
</Box>
);
});