Files
WhiteNightsAdminPanel/src/widgets/SightTabs/InformationTab/index.tsx

559 lines
17 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 {
Button,
TextField,
Box,
Autocomplete,
MenuItem,
Menu as MuiMenu,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material";
import {
BackButton,
TabPanel,
languageStore,
Language,
cityStore,
editSightStore,
SelectMediaDialog,
PreviewMediaDialog,
SightLanguageInfo,
SightCommonInfo,
UploadMediaDialog,
MEDIA_TYPE_VALUES,
} from "@shared";
import {
ImageUploadCard,
LanguageSwitcher,
VideoPreviewCard,
MediaViewer,
} from "@widgets";
import { Save } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { SaveWithoutCityAgree } from "@widgets";
import { LinkedStations } from "@pages";
export const InformationTab = observer(
({ value, index }: { value: number; index: number }) => {
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`);
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
>(null);
const [isAddMediaOpen, setIsAddMediaOpen] = useState(false);
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const [hardcodeType, setHardcodeType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
>(null);
const { cities } = cityStore;
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
useEffect(() => {}, [hardcodeType]);
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;
},
type: "thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview"
) => {
handleChange(
language as Language,
{
[type]: media.id,
},
true
);
};
const handleVideoPreviewClick = () => {
if (sight.common.video_preview && sight.common.video_preview !== "") {
setIsVideoPreviewOpen(true);
}
};
const handleChange = (
language: Language,
content: Partial<SightLanguageInfo | SightCommonInfo>,
common: boolean = false
) => {
updateSightInfo(language, content, common);
};
const executeSave = async () => {
await updateSight();
toast.success("Достопримечательность сохранена");
};
const handleSave = async () => {
const isCityMissing = !sight.common.city_id;
const isNameMissing = !sight.ru.name || !sight.en.name || !sight.zh.name;
if (isCityMissing || isNameMissing) {
setIsSaveWarningOpen(true);
return;
}
await executeSave();
};
const handleConfirmSave = async () => {
setIsSaveWarningOpen(false);
await executeSave();
};
const handleCancelSave = () => {
setIsSaveWarningOpen(false);
};
return (
<>
<TabPanel value={value} index={index}>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 3,
position: "relative",
paddingBottom: "70px",
}}
>
<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,
width: "100%",
flexDirection: "column",
}}
>
<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={cities["ru"]?.data ?? []}
value={
cities["ru"]?.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={{ width: "80%" }}>
{sight.common.id !== 0 && (
<LinkedStations
parentId={sight.common.id}
fields={[{ label: "Название", data: "name" }]}
type="edit"
/>
)}
</Box>
<Box
sx={{
display: "flex",
gap: 4,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-around",
width: "80%",
gap: 2,
flexDirection: { xs: "column", sm: "row" },
}}
>
<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("thumbnail");
}}
setHardcodeType={() => {
setHardcodeType("thumbnail");
}}
/>
<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("watermark_lu");
}}
setHardcodeType={() => {
setHardcodeType("watermark_lu");
}}
/>
<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("watermark_rd");
}}
setHardcodeType={() => {
setHardcodeType("watermark_rd");
}}
/>
<VideoPreviewCard
title="Видеозаставка"
videoId={sight.common.video_preview}
onVideoClick={handleVideoPreviewClick}
onDeleteVideoClick={() => {
handleChange(
language as Language,
{
video_preview: null,
},
true
);
}}
onSelectVideoClick={(file) => {
if (file) {
editSightStore.setFileToUpload(file);
setActiveMenuType("video_preview");
setIsUploadMediaOpen(true);
} else {
setActiveMenuType("video_preview");
setIsAddMediaOpen(true);
}
}}
/>
</Box>
</Box>
</Box>
<LanguageSwitcher />
<Box
sx={{
position: "absolute",
bottom: 0,
right: 0,
padding: 2,
backgroundColor: "background.paper",
width: "100%",
display: "flex",
justifyContent: "flex-end",
}}
>
<Button
variant="contained"
color="success"
startIcon={<Save color="white" size={18} />}
onClick={handleSave}
>
Сохранить
</Button>
</Box>
</Box>
</TabPanel>
<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={(media) => {
if (activeMenuType) {
handleMediaSelect(media, activeMenuType);
}
}}
mediaType={
activeMenuType
? MEDIA_TYPE_VALUES[
activeMenuType as keyof typeof MEDIA_TYPE_VALUES
]
: undefined
}
/>
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
contextObjectName={sight[language].name}
contextType="sight"
afterUpload={(media) => {
if (activeMenuType === "video_preview") {
handleChange(
language as Language,
{
video_preview: media.id,
},
true
);
} else {
handleChange(
language as Language,
{
[activeMenuType ?? "thumbnail"]: media.id,
},
true
);
}
setActiveMenuType(null);
setIsUploadMediaOpen(false);
}}
hardcodeType={activeMenuType}
initialFile={editSightStore.fileToUpload || undefined}
/>
<PreviewMediaDialog
open={isPreviewMediaOpen}
onClose={() => setIsPreviewMediaOpen(false)}
mediaId={mediaId}
/>
{sight.common.video_preview && sight.common.video_preview !== "" && (
<Dialog
open={isVideoPreviewOpen}
onClose={() => setIsVideoPreviewOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>Предпросмотр видео</DialogTitle>
<DialogContent>
<Box className="flex justify-center items-center p-4">
<MediaViewer
media={{
id: sight.common.video_preview,
media_type: 2,
filename: "video_preview",
}}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsVideoPreviewOpen(false)}>
Закрыть
</Button>
</DialogActions>
</Dialog>
)}
{isSaveWarningOpen && (
<SaveWithoutCityAgree
blocker={{
proceed: handleConfirmSave,
reset: handleCancelSave,
}}
/>
)}
</>
);
}
);