439 lines
14 KiB
TypeScript
439 lines
14 KiB
TypeScript
import {
|
||
Button,
|
||
TextField,
|
||
Box,
|
||
Autocomplete,
|
||
MenuItem,
|
||
Menu as MuiMenu,
|
||
} from "@mui/material";
|
||
import {
|
||
BackButton,
|
||
TabPanel,
|
||
languageStore,
|
||
Language,
|
||
cityStore,
|
||
editSightStore,
|
||
SelectMediaDialog,
|
||
PreviewMediaDialog,
|
||
SightLanguageInfo,
|
||
SightCommonInfo,
|
||
UploadMediaDialog,
|
||
MEDIA_TYPE_VALUES,
|
||
} from "@shared";
|
||
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
||
import { Save } from "lucide-react";
|
||
|
||
import { observer } from "mobx-react-lite";
|
||
import { useEffect, useState } from "react";
|
||
|
||
import { toast } from "react-toastify";
|
||
|
||
// Мокап для всплывающей подсказки
|
||
|
||
export const InformationTab = observer(
|
||
({ value, index }: { value: number; index: number }) => {
|
||
const { ruCities } = cityStore;
|
||
|
||
const [mediaId, setMediaId] = useState<string>("");
|
||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||
const { language } = languageStore;
|
||
|
||
const { sight, updateSightInfo, updateSight } = editSightStore;
|
||
|
||
const [, setCity] = useState<number>(sight.common.city_id ?? 0);
|
||
const [coordinates, setCoordinates] = useState<string>(`0, 0`);
|
||
|
||
// Menu state for each media button
|
||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||
const [activeMenuType, setActiveMenuType] = useState<
|
||
"thumbnail" | "watermark_lu" | "watermark_rd" | null
|
||
>(null);
|
||
const [isAddMediaOpen, setIsAddMediaOpen] = useState(false);
|
||
const [hardcodeType, setHardcodeType] = useState<
|
||
"thumbnail" | "watermark_lu" | "watermark_rd" | null
|
||
>(null);
|
||
useEffect(() => {
|
||
// Показывать только при инициализации (не менять при ошибках пользователя)
|
||
if (sight.common.latitude !== 0 || sight.common.longitude !== 0) {
|
||
setCoordinates(`${sight.common.latitude}, ${sight.common.longitude}`);
|
||
}
|
||
// если координаты обнулились — оставить поле как есть
|
||
}, [sight.common.latitude, sight.common.longitude]);
|
||
|
||
const handleMenuClose = () => {
|
||
setMenuAnchorEl(null);
|
||
setActiveMenuType(null);
|
||
};
|
||
|
||
const handleCreateNew = () => {
|
||
handleMenuClose();
|
||
};
|
||
|
||
const handleAddMedia = () => {
|
||
setIsAddMediaOpen(true);
|
||
handleMenuClose();
|
||
};
|
||
|
||
const handleMediaSelect = (media: {
|
||
id: string;
|
||
filename: string;
|
||
media_name?: string;
|
||
media_type: number;
|
||
}) => {
|
||
if (!activeMenuType) return;
|
||
handleChange(
|
||
language as Language,
|
||
{
|
||
[activeMenuType ?? "thumbnail"]: media.id,
|
||
},
|
||
true
|
||
);
|
||
setActiveMenuType(null);
|
||
setIsUploadMediaOpen(false);
|
||
};
|
||
|
||
const handleChange = (
|
||
language: Language,
|
||
content: Partial<SightLanguageInfo | SightCommonInfo>,
|
||
common: boolean = false
|
||
) => {
|
||
updateSightInfo(language, content, common);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<TabPanel value={value} index={index}>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
gap: 3,
|
||
position: "relative",
|
||
paddingBottom: "70px" /* Space for save button */,
|
||
}}
|
||
>
|
||
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||
<BackButton />
|
||
<h1 className="text-3xl break-words">{sight[language].name}</h1>
|
||
</div>
|
||
<LanguageSwitcher />
|
||
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
|
||
gap: 4, // Added gap between the two main columns
|
||
width: "100%",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
{/* Left column with main fields */}
|
||
<Box
|
||
sx={{
|
||
flexGrow: 1,
|
||
display: "flex",
|
||
width: "80%",
|
||
flexDirection: "column",
|
||
gap: 2.5,
|
||
}}
|
||
>
|
||
<TextField
|
||
label={`Название (${language.toUpperCase()})`}
|
||
value={sight[language].name}
|
||
onChange={(e) => {
|
||
handleChange(language as Language, {
|
||
name: e.target.value,
|
||
});
|
||
}}
|
||
fullWidth
|
||
variant="outlined"
|
||
/>
|
||
|
||
<TextField
|
||
label="Адрес"
|
||
value={sight[language].address}
|
||
onChange={(e) => {
|
||
handleChange(language as Language, {
|
||
address: e.target.value,
|
||
});
|
||
}}
|
||
fullWidth
|
||
variant="outlined"
|
||
/>
|
||
|
||
<Autocomplete
|
||
options={ruCities?.data ?? []}
|
||
value={
|
||
ruCities?.data?.find(
|
||
(city) => city.id === sight.common.city_id
|
||
) ?? null
|
||
}
|
||
getOptionLabel={(option) => option.name}
|
||
onChange={(_, value) => {
|
||
setCity(value?.id ?? 0);
|
||
handleChange(
|
||
language as Language,
|
||
{
|
||
city_id: value?.id ?? 0,
|
||
},
|
||
true
|
||
);
|
||
}}
|
||
renderInput={(params) => (
|
||
<TextField {...params} label="Город" />
|
||
)}
|
||
/>
|
||
|
||
<TextField
|
||
label="Координаты"
|
||
value={coordinates}
|
||
onChange={(e) => {
|
||
const newValue = e.target.value;
|
||
setCoordinates(newValue); // сохраняем ввод пользователя как есть
|
||
|
||
// Обрабатываем значение для сохранения
|
||
const input = newValue.replace(/,/g, " ").trim();
|
||
const [latStr, lonStr] = input.split(/\s+/);
|
||
|
||
const lat = parseFloat(latStr);
|
||
const lon = parseFloat(lonStr);
|
||
|
||
// Проверка, что обе координаты валидные числа
|
||
const isValidLat = !isNaN(lat);
|
||
const isValidLon = !isNaN(lon);
|
||
|
||
if (isValidLat && isValidLon) {
|
||
handleChange(
|
||
language as Language,
|
||
{
|
||
latitude: lat,
|
||
longitude: lon,
|
||
},
|
||
true
|
||
);
|
||
} else {
|
||
handleChange(
|
||
language as Language,
|
||
{
|
||
latitude: 0,
|
||
longitude: 0,
|
||
},
|
||
true
|
||
);
|
||
}
|
||
}}
|
||
fullWidth
|
||
variant="outlined"
|
||
placeholder="Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)"
|
||
/>
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
|
||
gap: 4,
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
justifyContent: "space-around",
|
||
width: "80%",
|
||
gap: 2,
|
||
flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
|
||
}}
|
||
>
|
||
<ImageUploadCard
|
||
title="Логотип"
|
||
imageKey="thumbnail"
|
||
imageUrl={sight.common.thumbnail}
|
||
onImageClick={() => {
|
||
setIsPreviewMediaOpen(true);
|
||
setMediaId(sight.common.thumbnail ?? "");
|
||
}}
|
||
onDeleteImageClick={() => {
|
||
handleChange(
|
||
language as Language,
|
||
{
|
||
thumbnail: null,
|
||
},
|
||
true
|
||
);
|
||
setActiveMenuType(null);
|
||
}}
|
||
onSelectFileClick={() => {
|
||
setActiveMenuType("thumbnail");
|
||
setIsAddMediaOpen(true);
|
||
}}
|
||
setUploadMediaOpen={() => {
|
||
setIsUploadMediaOpen(true);
|
||
setActiveMenuType("thumbnail");
|
||
}}
|
||
setHardcodeType={(type) => {
|
||
setHardcodeType(
|
||
type as "thumbnail" | "watermark_lu" | "watermark_rd"
|
||
);
|
||
}}
|
||
/>
|
||
<ImageUploadCard
|
||
title="Водяной знак (левый верхний)"
|
||
imageKey="watermark_lu"
|
||
imageUrl={sight.common.watermark_lu}
|
||
onImageClick={() => {
|
||
setIsPreviewMediaOpen(true);
|
||
setMediaId(sight.common.watermark_lu ?? "");
|
||
}}
|
||
onDeleteImageClick={() => {
|
||
handleChange(
|
||
language as Language,
|
||
{
|
||
watermark_lu: null,
|
||
},
|
||
true
|
||
);
|
||
setActiveMenuType(null);
|
||
}}
|
||
onSelectFileClick={() => {
|
||
setActiveMenuType("watermark_lu");
|
||
setIsAddMediaOpen(true);
|
||
}}
|
||
setUploadMediaOpen={() => {
|
||
setIsUploadMediaOpen(true);
|
||
setActiveMenuType("watermark_lu");
|
||
}}
|
||
setHardcodeType={(type) => {
|
||
setHardcodeType(
|
||
type as "thumbnail" | "watermark_lu" | "watermark_rd"
|
||
);
|
||
}}
|
||
/>
|
||
<ImageUploadCard
|
||
title="Водяной знак (правый нижний)"
|
||
imageKey="watermark_rd"
|
||
imageUrl={sight.common.watermark_rd}
|
||
onImageClick={() => {
|
||
setIsPreviewMediaOpen(true);
|
||
setMediaId(sight.common.watermark_rd ?? "");
|
||
}}
|
||
onDeleteImageClick={() => {
|
||
handleChange(
|
||
language as Language,
|
||
{
|
||
watermark_rd: null,
|
||
},
|
||
true
|
||
);
|
||
setActiveMenuType(null);
|
||
}}
|
||
onSelectFileClick={() => {
|
||
setActiveMenuType("watermark_rd");
|
||
setIsAddMediaOpen(true);
|
||
}}
|
||
setUploadMediaOpen={() => {
|
||
setIsUploadMediaOpen(true);
|
||
setActiveMenuType("watermark_rd");
|
||
}}
|
||
setHardcodeType={(type) => {
|
||
setHardcodeType(
|
||
type as "thumbnail" | "watermark_lu" | "watermark_rd"
|
||
);
|
||
}}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* LanguageSwitcher positioned at the top right */}
|
||
|
||
<LanguageSwitcher />
|
||
|
||
{/* Save Button fixed at the bottom right */}
|
||
<Box
|
||
sx={{
|
||
position: "absolute",
|
||
bottom: 0,
|
||
right: 0,
|
||
padding: 2,
|
||
backgroundColor: "background.paper", // To ensure it stands out over content
|
||
width: "100%", // Take full width to cover content below it
|
||
display: "flex",
|
||
justifyContent: "flex-end", // Align to the right
|
||
}}
|
||
>
|
||
<Button
|
||
variant="contained"
|
||
color="success"
|
||
startIcon={<Save color="white" size={18} />}
|
||
onClick={async () => {
|
||
await updateSight();
|
||
toast.success("Достопримечательность сохранена");
|
||
}}
|
||
>
|
||
Сохранить
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
</TabPanel>
|
||
|
||
{/* Media Menu */}
|
||
<MuiMenu
|
||
anchorEl={menuAnchorEl}
|
||
open={Boolean(menuAnchorEl)}
|
||
onClose={handleMenuClose}
|
||
anchorOrigin={{
|
||
vertical: "top",
|
||
horizontal: "right",
|
||
}}
|
||
transformOrigin={{
|
||
vertical: "bottom",
|
||
horizontal: "right",
|
||
}}
|
||
>
|
||
<MenuItem onClick={handleCreateNew}>Создать новую</MenuItem>
|
||
<MenuItem onClick={handleAddMedia}>Выбрать существующую</MenuItem>
|
||
</MuiMenu>
|
||
|
||
<SelectMediaDialog
|
||
open={isAddMediaOpen}
|
||
onClose={() => {
|
||
setIsAddMediaOpen(false);
|
||
setActiveMenuType(null);
|
||
}}
|
||
onSelectMedia={handleMediaSelect}
|
||
mediaType={
|
||
activeMenuType
|
||
? MEDIA_TYPE_VALUES[
|
||
activeMenuType as keyof typeof MEDIA_TYPE_VALUES
|
||
]
|
||
: undefined
|
||
}
|
||
/>
|
||
|
||
<UploadMediaDialog
|
||
open={isUploadMediaOpen}
|
||
onClose={() => setIsUploadMediaOpen(false)}
|
||
afterUpload={(media) => {
|
||
handleChange(
|
||
language as Language,
|
||
{
|
||
[activeMenuType ?? "thumbnail"]: media.id,
|
||
},
|
||
true
|
||
);
|
||
setActiveMenuType(null);
|
||
setIsUploadMediaOpen(false);
|
||
}}
|
||
hardcodeType={hardcodeType}
|
||
/>
|
||
<PreviewMediaDialog
|
||
open={isPreviewMediaOpen}
|
||
onClose={() => setIsPreviewMediaOpen(false)}
|
||
mediaId={mediaId}
|
||
/>
|
||
</>
|
||
);
|
||
}
|
||
);
|