1542 lines
54 KiB
TypeScript
1542 lines
54 KiB
TypeScript
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>
|
||
);
|
||
});
|