feat: Add edit/create/list sight page

This commit is contained in:
2025-05-29 16:25:18 +03:00
parent 17de7e495f
commit e2ca6b4132
25 changed files with 1519 additions and 240 deletions

View File

@ -1,48 +1,484 @@
import { TextField } from "@mui/material";
import { BackButton, TabPanel } from "@shared";
import {
Button,
TextField,
Box,
Typography,
IconButton,
Paper,
Tooltip,
} from "@mui/material";
import { BackButton, Sight, sightsStore, TabPanel, Language } from "@shared";
import { LanguageSwitcher } from "@widgets";
import { ImagePlus, Info } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useState, useEffect } from "react";
import { useLocation } from "react-router-dom";
export const InformationTab = ({
value,
index,
}: {
value: number;
index: number;
}) => {
return (
<TabPanel value={value} index={index}>
<div className="flex-1 flex flex-col relative">
<div className="flex-1 flex flex-col gap-10">
<BackButton />
<div className="flex flex-col gap-5 w-1/2">
<TextField label="Название" />
<TextField label="Адрес" />
<TextField label="Город" />
<TextField label="Координаты" />
<div className="flex justify-around w-full mt-20">
<div className="flex flex-col gap-2 ">
<p>Логотип</p>
<button>Выбрать</button>
</div>
<div className="flex flex-col gap-2">
<p>Водяной знак (л.в)</p>
<button>Выбрать</button>
</div>
<div className="flex flex-col gap-2">
<p>Водяной знак (п.в)</p>
<button>Выбрать</button>
</div>
</div>
</div>
</div>
<button className="bg-green-400 w-min ml-auto text-white py-2 rounded-2xl px-4">
Сохранить
</button>
<div className="absolute top-1/2 -translate-y-1/2 right-0">
<LanguageSwitcher />
</div>
</div>
</TabPanel>
);
// Мокап данных для отображения, потом это будет приходить из store/props
// Keeping this mock for demonstration, but in a real app,
// this would come from the MobX store's 'sight' object.
const mockSightData = {
name: "Эрмитаж",
address: "Дворцовая площадь, 2",
city: "Санкт-Петербург", // или city_id, если будет Select
coordinates: "59.9398, 30.3146",
logo: null, // null или URL/ID медиа
watermark_lu: null,
watermark_rd: null,
};
// Мокап для всплывающей подсказки
const watermarkTooltipText = "При наведении открывается просмотр в поп-апе";
const logoTooltipText = "При наведении открывается просмотр логотипа в поп-апе";
export const InformationTab = observer(
({ value, index }: { value: number; index: number }) => {
const {
sight,
cachedMultilingualContent,
updateCachedLanguageContent,
clearCachedMultilingualContent,
// Assuming you'll have an action to update the main sight object
updateSight,
} = sightsStore;
// Initialize local states with data from the MobX store's 'sight'
const [address, setAddress] = useState(sight?.address ?? "");
const [city, setCity] = useState(sight?.city ?? "");
const [coordinates, setCoordinates] = useState(
sight?.latitude && sight?.longitude
? `${sight.latitude}, ${sight.longitude}`
: ""
);
const [currentLanguage, setCurrentLanguage] = useState<Language>("ru");
const pathname = useLocation().pathname;
// Effect to initialize local states when `sight` data becomes available or changes
useEffect(() => {
if (sight) {
setAddress(sight.address ?? "");
setCity(sight.city ?? "");
setCoordinates(
sight.latitude && sight.longitude
? `${sight.latitude}, ${sight.longitude}`
: ""
);
// Initialize cached content if not already set
if (!cachedMultilingualContent) {
sightsStore.setCachedMultilingualContent({
ru: { name: sight.name, description: "", address: sight.address },
en: { name: "", description: "", address: "" },
zh: { name: "", description: "", address: "" },
});
}
}
}, [sight, cachedMultilingualContent]); // Add cachedMultilingualContent to dependencies
// Effect to clear cached content when the route changes
useEffect(() => {
clearCachedMultilingualContent();
}, [pathname, clearCachedMultilingualContent]);
const handleLanguageChange = (lang: Language) => {
setCurrentLanguage(lang);
};
const handleSelectMedia = (
type: "logo" | "watermark_lu" | "watermark_rd"
) => {
// Here will be logic for opening modal window for media selection
console.log("Select media for:", type);
// In a real application, you might open a dialog here
// and update the sight object with the selected media URL/ID.
};
const handleSave = () => {
// Parse coordinates back to latitude and longitude
let latitude: number | undefined;
let longitude: number | undefined;
const coordsArray = coordinates
.split(",")
.map((coord) => parseFloat(coord.trim()));
if (
coordsArray.length === 2 &&
!isNaN(coordsArray[0]) &&
!isNaN(coordsArray[1])
) {
latitude = coordsArray[0];
longitude = coordsArray[1];
}
// Prepare the updated sight data
const updatedSightData = {
...sight, // Keep existing sight data
address: address,
city: city,
latitude: latitude,
longitude: longitude,
// Assuming logo and watermark updates would happen via handleSelectMedia
// and then be reflected in the sight object in the store.
};
// Here we would save both the sight data and the multilingual content
console.log("Saving general information and multilingual content...", {
updatedSightData,
multilingualContent: cachedMultilingualContent,
});
// Call an action from your store to save the data
// For example:
// sightsStore.saveSight({ ...updatedSightData, multilingualContent: cachedMultilingualContent });
// You might have a specific action in your store for saving all this data.
};
return (
<TabPanel value={value} index={index}>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 3,
position: "relative",
paddingBottom: "70px" /* Space for save button */,
}}
>
<BackButton />
<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={`Название (${currentLanguage.toUpperCase()})`}
value={cachedMultilingualContent?.[currentLanguage]?.name ?? ""}
onChange={(e) => {
updateCachedLanguageContent(currentLanguage, {
name: e.target.value,
});
}}
fullWidth
variant="outlined"
/>
<TextField
label={`Описание (${currentLanguage.toUpperCase()})`}
value={
cachedMultilingualContent?.[currentLanguage]?.description ??
""
}
onChange={(e) => {
updateCachedLanguageContent(currentLanguage, {
description: e.target.value,
});
}}
fullWidth
variant="outlined"
multiline
rows={4}
/>
<TextField
label="Адрес"
value={address}
onChange={(e) => {
setAddress(e.target.value);
}}
fullWidth
variant="outlined"
/>
<TextField
label="Город"
value={city}
onChange={(e) => {
setCity(e.target.value);
}}
fullWidth
variant="outlined"
/>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<TextField
label="Координаты"
value={coordinates}
onChange={(e) => {
setCoordinates(e.target.value);
}}
fullWidth
variant="outlined"
helperText="Формат: широта, долгота (например, 59.9398, 30.3146)"
/>
</Box>
</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
}}
>
<Paper
elevation={2}
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="subtitle2"
gutterBottom
sx={{ mb: 0, mr: 0.5 }}
>
Логотип
</Typography>
<Tooltip title={watermarkTooltipText}>
<Info
size={16}
color="gray"
style={{ cursor: "pointer" }}
/>
</Tooltip>
</Box>
<Box
sx={{
width: 80,
height: 80,
backgroundColor: "grey.200",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 1,
mb: 1,
cursor: mockSightData.watermark_lu
? "pointer"
: "default", // Only clickable if there's an image
"&:hover": {
backgroundColor: mockSightData.watermark_lu
? "grey.300"
: "grey.200",
},
}}
onClick={() =>
mockSightData.watermark_lu &&
handleSelectMedia("watermark_lu")
}
>
{mockSightData.watermark_lu ? (
<img
src={mockSightData.watermark_lu}
alt="Знак л.в"
style={{ maxWidth: "100%", maxHeight: "100%" }}
/>
) : (
<ImagePlus size={24} color="grey" />
)}
</Box>
<Button
variant="outlined"
size="small"
onClick={() => handleSelectMedia("watermark_lu")}
>
Выбрать
</Button>
</Paper>
<Paper
elevation={2}
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="subtitle2"
gutterBottom
sx={{ mb: 0, mr: 0.5 }}
>
Водяной знак (л.в)
</Typography>
<Tooltip title={watermarkTooltipText}>
<Info
size={16}
color="gray"
style={{ cursor: "pointer" }}
/>
</Tooltip>
</Box>
<Box
sx={{
width: 80,
height: 80,
backgroundColor: "grey.200",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 1,
mb: 1,
cursor: mockSightData.watermark_lu
? "pointer"
: "default", // Only clickable if there's an image
"&:hover": {
backgroundColor: mockSightData.watermark_lu
? "grey.300"
: "grey.200",
},
}}
onClick={() =>
mockSightData.watermark_lu &&
handleSelectMedia("watermark_lu")
}
>
{mockSightData.watermark_lu ? (
<img
src={mockSightData.watermark_lu}
alt="Знак л.в"
style={{ maxWidth: "100%", maxHeight: "100%" }}
/>
) : (
<ImagePlus size={24} color="grey" />
)}
</Box>
<Button
variant="outlined"
size="small"
onClick={() => handleSelectMedia("watermark_lu")}
>
Выбрать
</Button>
</Paper>
<Paper
elevation={2}
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 1,
flex: 1,
minWidth: 150, // Ensure a minimum width
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="subtitle2"
gutterBottom
sx={{ mb: 0, mr: 0.5 }}
>
Водяной знак (п.в)
</Typography>
<Tooltip title={watermarkTooltipText}>
<Info
size={16}
color="gray"
style={{ cursor: "pointer" }}
/>
</Tooltip>
</Box>
<Box
sx={{
width: 80,
height: 80,
backgroundColor: "grey.200",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 1,
mb: 1,
cursor: mockSightData.watermark_rd
? "pointer"
: "default", // Only clickable if there's an image
"&:hover": {
backgroundColor: mockSightData.watermark_rd
? "grey.300"
: "grey.200",
},
}}
onClick={() =>
mockSightData.watermark_rd &&
handleSelectMedia("watermark_rd")
}
>
{mockSightData.watermark_rd ? (
<img
src={mockSightData.watermark_rd}
alt="Знак п.в"
style={{ maxWidth: "100%", maxHeight: "100%" }}
/>
) : (
<ImagePlus size={24} color="grey" />
)}
</Box>
<Button
variant="outlined"
size="small"
onClick={() => handleSelectMedia("watermark_rd")}
>
Выбрать
</Button>
</Paper>
</Box>
</Box>
</Box>
{/* LanguageSwitcher positioned at the top right */}
<Box sx={{ position: "absolute", top: 0, right: 0, zIndex: 1 }}>
<LanguageSwitcher onLanguageChange={handleLanguageChange} />
</Box>
{/* 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" onClick={handleSave}>
Сохранить
</Button>
</Box>
</Box>
</TabPanel>
);
}
);