WhiteNightsAdminPanel/src/pages/sight/edit.tsx

915 lines
32 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,
} from "@mui/material";
import { Edit, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import { useParams, Link } from "react-router";
import React, { useState, useEffect } from "react";
import { LinkedItems } from "../../components/LinkedItems";
import { CreateSightArticle } from "../../components/CreateSightArticle";
import { ArticleItem, articleFields } from "./types";
import { TOKEN_KEY } from "../../authProvider";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
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 {
saveButtonProps,
register,
control,
watch,
getValues,
setValue,
formState: { errors },
} = useForm({
refineCoreProps: {
meta: {
headers: {
"Accept-Language": language,
},
},
},
});
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city",
onSearch: (value) => [
{
field: "name",
operator: "contains",
value,
},
],
});
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",
queryOptions: {
queryKey: ["article", language],
},
onSearch: (value) => [
{
field: "heading",
operator: "contains",
value,
},
],
});
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 [namePreview, setNamePreview] = useState("");
const [coordinatesPreview, setCoordinatesPreview] = useState({
latitude: "",
longitude: "",
});
const [cityPreview, setCityPreview] = useState("");
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(
null
);
const [watermarkRDPreview, setWatermarkRDPreview] = useState<string | null>(
null
);
const [leftArticlePreview, setLeftArticlePreview] = useState("");
const [previewArticlePreview, setPreviewArticlePreview] = useState("");
// Следим за изменениями во всех полях
const coordinatesContent = watch("coordinates");
const addressContent = watch("address");
const nameContent = watch("name");
const latitudeContent = watch("latitude");
const longitudeContent = watch("longitude");
const cityContent = watch("city_id");
const thumbnailContent = watch("thumbnail");
const watermarkLUContent = watch("watermark_lu");
const watermarkRDContent = watch("watermark_rd");
const leftArticleContent = watch("left_article");
const previewArticleContent = watch("preview_article");
// Обновляем состояния при изменении полей
useEffect(() => {
setNamePreview(nameContent || "");
}, [nameContent]);
useEffect(() => {
setCoordinatesPreview({
latitude: latitudeContent || "",
longitude: longitudeContent || "",
});
}, [latitudeContent, longitudeContent]);
useEffect(() => {
const selectedCity = cityAutocompleteProps.options.find(
(option) => option.id === cityContent
);
setCityPreview(selectedCity?.name || "");
}, [cityContent, cityAutocompleteProps.options]);
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 === leftArticleContent
);
setLeftArticlePreview(selectedLeftArticle?.heading || "");
}, [leftArticleContent, articleAutocompleteProps.options]);
useEffect(() => {
const selectedPreviewArticle = articleAutocompleteProps.options.find(
(option) => option.id === previewArticleContent
);
setPreviewArticlePreview(selectedPreviewArticle?.heading || "");
}, [previewArticleContent, articleAutocompleteProps.options]);
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}>
<Box sx={{ display: "flex", gap: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
flex: 1,
gap: 10,
justifyContent: "space-between",
}}
>
{/* Language Selection */}
<Box
sx={{
display: "flex",
gap: 2,
}}
>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "ru" ? "primary.main" : "transparent",
color: language === "ru" ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
onClick={() => setLanguageAction("ru")}
>
RU
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "en" ? "primary.main" : "transparent",
color: language === "en" ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
onClick={() => setLanguageAction("en")}
>
EN
</Box>
<Box
sx={{
cursor: "pointer",
flex: 1,
display: "flex",
justifyContent: "center",
bgcolor: language === "zh" ? "primary.main" : "transparent",
color: language === "zh" ? "white" : "inherit",
p: 1,
borderRadius: 1,
}}
onClick={() => setLanguageAction("zh")}
>
ZH
</Box>
</Box>
{/* Форма редактирования */}
<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
InputLabelProps={{ shrink: true }}
type="text"
label={"Название *"}
name="name"
/>
<TextField
value={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}
onChange={handleCoordinatesChange}
error={!!(errors as any)?.latitude}
helperText={(errors as any)?.latitude?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={"Координаты *"}
/>
<input
type="hidden"
{...register("longitude", {
value: coordinatesPreview.longitude,
required: "Это поле является обязательным",
valueAsNumber: true,
})}
/>
<input
type="hidden"
{...register("latitude", {
value: coordinatesPreview.latitude,
required: "Это поле является обязательным",
valueAsNumber: true,
})}
/>
{/*
<TextField
{...register("longitude", {
required: "Это поле является обязательным",
valueAsNumber: true,
})}
error={!!(errors as any)?.longitude}
helperText={(errors as any)?.longitude?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Долгота *"}
name="longitude"
/> */}
{/* <TextField
{...register("coordinates", {
required: "Это поле является обязательным",
valueAsNumber: true,
})}
error={!!(errors as any)?.coordinates}
helperText={(errors as any)?.coordinates?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="number"
label={"Координаты *"}
name="coordinates"
/> */}
<TextField
{...register("address", {
required: "Это поле является обязательным",
})}
error={!!(errors as any)?.address}
helperText={(errors as any)?.address?.message}
margin="normal"
fullWidth
InputLabelProps={{ 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) => {
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}
required
/>
)}
/>
)}
/>
<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())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Выберите обложку"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
)}
/>
<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
/>
)}
/>
)}
/>
<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
/>
)}
/>
)}
/>
<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 || "");
}}
getOptionLabel={(item) => {
return item ? item.heading : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.heading
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Левая статья"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
)}
/>
<Controller
control={control}
name="preview_article"
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...articleAutocompleteProps}
value={
articleAutocompleteProps.options.find(
(option) => option.id === field.value
) || null
}
onChange={(_, value) => {
field.onChange(value?.id || "");
}}
getOptionLabel={(item) => {
return item ? item.heading : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.heading
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Cтатья-предпросмотр"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
)}
/>
</Box>
</Box>
{/* Блок предпросмотра */}
<Paper
sx={{
flex: 1,
p: 2,
position: "sticky",
top: 16,
borderRadius: 2,
border: "1px solid",
borderColor: "primary.main",
bgcolor: (theme) =>
theme.palette.mode === "dark" ? "background.paper" : "#fff",
}}
>
<Typography variant="h6" gutterBottom color="primary">
Предпросмотр
</Typography>
{/* Название достопримечательности */}
<Typography
variant="h4"
gutterBottom
sx={{
color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
mb: 3,
}}
>
{namePreview}
</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",
}}
>
{cityPreview}
</Box>
</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: "text.secondary" }}>
Координаты:{" "}
</Box>
<Box
component="span"
sx={{
color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
}}
>
{`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}
</Box>
</Typography>
{/* Обложка */}
{thumbnailPreview && (
<Box sx={{ mb: 2 }}>
<Typography
variant="body1"
gutterBottom
sx={{ color: "text.secondary" }}
>
Обложка:
</Typography>
<Box
component="img"
src={thumbnailPreview}
alt="Обложка"
sx={{
maxWidth: "100%",
height: "40vh",
borderRadius: 2,
border: "1px solid",
borderColor: "primary.main",
}}
/>
</Box>
)}
{/* Водяные знаки */}
<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>
</Box>
{/* Связанные статьи */}
<Box>
{/* <Typography variant="body1" gutterBottom sx={{color: 'text.secondary'}}>
Связанные статьи:
</Typography> */}
{leftArticlePreview && (
<Typography variant="body1" gutterBottom>
<Box component="span" sx={{ color: "text.secondary" }}>
Левая статья:{" "}
</Box>
<Box
component={Link}
to={`/article/show/${watch("left_article")}`}
sx={{
color: (theme) =>
theme.palette.mode === "dark"
? "grey.300"
: "grey.800",
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
}}
>
{leftArticlePreview}
</Box>
</Typography>
)}
{previewArticlePreview && (
<Typography variant="body1" gutterBottom>
<Box component="span" sx={{ color: "text.secondary" }}>
Статья-предпросмотр:{" "}
</Box>
<Box
component={Link}
to={`/article/show/${watch("preview_article")}`}
sx={{
color: (theme) =>
theme.palette.mode === "dark"
? "grey.300"
: "grey.800",
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
}}
>
{previewArticlePreview}
</Box>
</Typography>
)}
</Box>
</Paper>
</Box>
<Box sx={{ mt: 3 }}>
<LinkedItems<ArticleItem>
type="edit"
parentId={sightId!}
parentResource="sight"
childResource="article"
fields={articleFields}
title="статьи"
/>
<CreateSightArticle
parentId={sightId!}
parentResource="sight"
childResource="article"
title="статью"
/>
</Box>
</Edit>
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={1}>
1
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={2}>
2
</CustomTabPanel>
</Box>
);
});