Compare commits

..

1 Commits

Author SHA1 Message Date
cf2a116ecb Latest version (#12)
All checks were successful
release-tag / release-image (push) Successful in 2m17s
Co-authored-by: itoshi <kkzemeow@gmail.com>
Co-authored-by: Spynder <19329095+Spynder@users.noreply.github.com>
Reviewed-on: #12
Co-authored-by: Alexander Lazarenko <kerblif@unprism.ru>
Co-committed-by: Alexander Lazarenko <kerblif@unprism.ru>
2025-05-29 10:12:00 +00:00
20 changed files with 468 additions and 212 deletions

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" type="image/png" href="/favicon-ship.png" /> <link rel="icon" type="image/png" href="/favicon_ship.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

BIN
public/favicon_ship.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -15,9 +15,9 @@ export function MediaView({ media }: Readonly<{ media?: MediaData }>) {
<Box <Box
sx={{ sx={{
maxHeight: "300px", maxHeight: "300px",
width: "100%", width: "80%",
height: "100%", height: "100%",
maxWidth: "300px", maxWidth: "600px",
display: "flex", display: "flex",
flexGrow: 1, flexGrow: 1,
justifyContent: "center", justifyContent: "center",

View File

@ -1,12 +1,11 @@
import { Ship } from "lucide-react"; import { Logo } from "@/icons/Logo";
import { ProjectIcon } from "./Icons";
export default function SidebarTitle({ collapsed }: { collapsed: boolean }) { export default function SidebarTitle({ collapsed }: { collapsed: boolean }) {
return ( return (
<div <div
style={{ display: "flex", alignItems: "center", whiteSpace: "nowrap" }} style={{ display: "flex", alignItems: "center", whiteSpace: "nowrap" }}
> >
<Ship size={40} style={{ color: "#7f6b58" }} /> <Logo width={40} height={40} />
{!collapsed && ( {!collapsed && (
<span style={{ marginLeft: 8, fontWeight: "bold" }}>Белые ночи</span> <span style={{ marginLeft: 8, fontWeight: "bold" }}>Белые ночи</span>

3
src/icons/124.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.5 KiB

22
src/icons/Logo.tsx Normal file

File diff suppressed because one or more lines are too long

View File

@ -3,19 +3,63 @@ import { Create, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form"; import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { languageStore, META_LANGUAGE } from "../../store/LanguageStore"; import { META_LANGUAGE } from "../../store/LanguageStore";
import { LanguageSwitch } from "@/components/LanguageSwitch";
import { useEffect, useState } from "react";
import { EVERY_LANGUAGE, Languages, languageStore } from "@stores";
export const CarrierCreate = observer(() => { export const CarrierCreate = observer(() => {
const { language } = languageStore; const { language, setLanguageAction } = languageStore;
const { const {
saveButtonProps, saveButtonProps,
refineCore: { formLoading }, refineCore: { formLoading },
register, register,
control, control,
setValue,
watch,
formState: { errors }, formState: { errors },
} = useForm({ } = useForm({
refineCoreProps: META_LANGUAGE(language) refineCoreProps: META_LANGUAGE(language),
}); });
const [carrierData, setCarrierData] = useState({
full_name: EVERY_LANGUAGE(""),
short_name: EVERY_LANGUAGE(""),
slogan: EVERY_LANGUAGE(""),
});
useEffect(() => {
setValue("full_name", carrierData.full_name[language]);
setValue("short_name", carrierData.short_name[language]);
setValue("slogan", carrierData.slogan[language]);
}, [carrierData, language, setValue]);
function updateTranslations(update: boolean = true) {
const newCarrierData = {
...carrierData,
full_name: {
...carrierData.full_name,
[language]: watch("full_name") ?? "",
},
short_name: {
...carrierData.short_name,
[language]: watch("short_name") ?? "",
},
slogan: {
...carrierData.slogan,
[language]: watch("slogan") ?? "",
},
};
if (update) setCarrierData(newCarrierData);
return newCarrierData;
}
const handleLanguageChange = (lang: Languages) => {
updateTranslations();
setLanguageAction(lang);
};
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city", resource: "city",
onSearch: (value) => [ onSearch: (value) => [
@ -45,6 +89,7 @@ export const CarrierCreate = observer(() => {
sx={{ display: "flex", flexDirection: "column" }} sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off" autoComplete="off"
> >
<LanguageSwitch action={handleLanguageChange} />
<Controller <Controller
control={control} control={control}
name="city_id" name="city_id"
@ -95,7 +140,7 @@ export const CarrierCreate = observer(() => {
helperText={(errors as any)?.full_name?.message} helperText={(errors as any)?.full_name?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Полное имя *"} label={"Полное имя *"}
name="full_name" name="full_name"
@ -109,16 +154,13 @@ export const CarrierCreate = observer(() => {
helperText={(errors as any)?.short_name?.message} helperText={(errors as any)?.short_name?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Короткое имя"} label={"Короткое имя"}
name="short_name" name="short_name"
/> />
<Box component="form" <Box component="form" sx={{ display: "flex" }} autoComplete="off">
sx={{ display: "flex" }}
autoComplete="off"
>
<TextField <TextField
{...register("main_color", { {...register("main_color", {
// required: 'Это поле является обязательным', // required: 'Это поле является обязательным',
@ -127,7 +169,7 @@ export const CarrierCreate = observer(() => {
helperText={(errors as any)?.main_color?.message} helperText={(errors as any)?.main_color?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="color" type="color"
label={"Основной цвет"} label={"Основной цвет"}
name="main_color" name="main_color"
@ -149,7 +191,7 @@ export const CarrierCreate = observer(() => {
helperText={(errors as any)?.left_color?.message} helperText={(errors as any)?.left_color?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="color" type="color"
label={"Цвет левого виджета"} label={"Цвет левого виджета"}
name="left_color" name="left_color"
@ -172,7 +214,7 @@ export const CarrierCreate = observer(() => {
helperText={(errors as any)?.right_color?.message} helperText={(errors as any)?.right_color?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="color" type="color"
label={"Цвет правого виджета"} label={"Цвет правого виджета"}
name="right_color" name="right_color"
@ -195,7 +237,7 @@ export const CarrierCreate = observer(() => {
helperText={(errors as any)?.slogan?.message} helperText={(errors as any)?.slogan?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Слоган"} label={"Слоган"}
name="slogan" name="slogan"

View File

@ -1,69 +1,224 @@
import {Autocomplete, Box, TextField} from '@mui/material' import { Autocomplete, Box, TextField } from "@mui/material";
import {Create, useAutocomplete} from '@refinedev/mui' import { Create, useAutocomplete } from "@refinedev/mui";
import {useForm} from '@refinedev/react-hook-form' import { useForm } from "@refinedev/react-hook-form";
import {Controller} from 'react-hook-form' import { Controller } from "react-hook-form";
import { useEffect, useState } from "react";
import { EVERY_LANGUAGE, Languages, languageStore } from "@stores";
import { LanguageSwitch } from "@/components/LanguageSwitch";
import { axiosInstanceForGet } from "@/providers";
import { useNavigate } from "react-router";
import { useNotification } from "@refinedev/core";
export const CityCreate = () => { export const CityCreate = () => {
const { language, setLanguageAction } = languageStore;
const navigate = useNavigate();
const notification = useNotification();
// State to manage city name translations across all supported languages.
// Initializes with empty strings for each language.
const [allLanguageNames, setAllLanguageNames] = useState<
Record<Languages, string>
>(EVERY_LANGUAGE(""));
const { const {
saveButtonProps, saveButtonProps,
refineCore: {formLoading}, refineCore: { formLoading },
register, register,
control, control,
formState: {errors}, setValue,
} = useForm({}) watch,
handleSubmit,
formState: { errors },
} = useForm<{
name: string;
country_code: string;
arms: string;
}>({});
const {autocompleteProps: countryAutocompleteProps} = useAutocomplete({ // Keeps the 'name' input field synchronized with the currently active language's translation.
resource: 'country', // Updates whenever the active language or the `allLanguageNames` state changes.
}) useEffect(() => {
setValue("name", allLanguageNames[language]);
}, [language, allLanguageNames, setValue]);
const {autocompleteProps: mediaAutocompleteProps} = useAutocomplete({ // Captures the current value of the 'name' TextField and updates the `allLanguageNames` state.
resource: 'media', // This is vital for preserving user input when switching languages or before form submission.
const updateCurrentLanguageName = () => {
const currentNameValue = watch("name");
setAllLanguageNames((prev) => ({
...prev,
[language]: currentNameValue || "",
}));
};
// Handles language changes. It first saves the current input, then updates the active language.
const handleLanguageChange = (lang: Languages) => {
updateCurrentLanguageName();
setLanguageAction(lang);
};
// Autocomplete hooks for selecting a country and city arms (media).
const { autocompleteProps: countryAutocompleteProps } = useAutocomplete({
resource: "country",
});
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
resource: "media",
onSearch: (value) => [ onSearch: (value) => [
{ {
field: 'media_name', field: "media_name",
operator: 'contains', operator: "contains",
value, value,
}, },
], ],
}) });
// --- Form Submission Logic ---
// Handles the form submission. It saves the current language's input,
// validates the Russian name, and then sends requests to create/update city data
// across different languages.
const onFinish = async (data: {
name: string;
country_code: string;
arms: string;
}) => {
updateCurrentLanguageName();
const finalNames = {
...allLanguageNames,
[language]: data.name,
};
try {
if (!finalNames.ru) {
console.error("Russian name is required for initial city creation.");
if (notification && typeof notification.open === "function") {
notification.open({
message: "Ошибка",
description: "Русское название города обязательно для создания.",
type: "error",
});
}
return;
}
console.log("Submitting with names:", finalNames);
// Create the city with the Russian name first.
const ruResponse = await axiosInstanceForGet("ru").post("/city", {
name: finalNames.ru,
country_code: data.country_code,
arms: data.arms,
});
const id = ruResponse.data.id;
// Update the city with English and Chinese names if available.
if (finalNames.en) {
await axiosInstanceForGet("en").patch(`/city/${id}`, {
name: finalNames.en,
country_code: data.country_code,
arms: data.arms,
});
}
if (finalNames.zh) {
await axiosInstanceForGet("zh").patch(`/city/${id}`, {
name: finalNames.zh,
country_code: data.country_code,
arms: data.arms,
});
}
console.log("City created/updated successfully!");
if (notification && typeof notification.open === "function") {
notification.open({
message: "Город успешно создан",
type: "success",
});
}
navigate("/city", { replace: true });
} catch (error) {
console.error("Error creating/updating city:", error);
}
};
return ( return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}> <Create
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off"> isLoading={formLoading}
saveButtonProps={{
...saveButtonProps,
disabled: saveButtonProps.disabled,
onClick: handleSubmit(onFinish as any),
}}
>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<LanguageSwitch action={handleLanguageChange} />
<Controller <Controller
control={control} control={control}
name="country_code" name="country_code"
rules={{required: 'Это поле является обязательным'}} rules={{ required: "Это поле является обязательным" }}
defaultValue={null} defaultValue={null}
render={({field}) => ( render={({ field }) => (
<Autocomplete <Autocomplete
{...countryAutocompleteProps} {...countryAutocompleteProps}
value={countryAutocompleteProps.options.find((option) => option.code === field.value) || null} value={
onChange={(_, value) => { countryAutocompleteProps.options.find(
field.onChange(value?.code || '') (option: { code: string; name: string; id: string }) =>
option.code === field.value
) || null
}
onChange={(
_,
value: { code: string; name: string; id: string } | null
) => {
field.onChange(value?.code || "");
}} }}
getOptionLabel={(item) => { getOptionLabel={(item: {
return item ? item.name : '' code: string;
name: string;
id: string;
}) => {
return item ? item.name : "";
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(
return option.id === value?.id option: { code: string; name: string; id: string },
value: { code: string; name: string; id: string }
) => {
return option.id === value?.id;
}} }}
renderInput={(params) => <TextField {...params} label="Выберите страну" margin="normal" variant="outlined" error={!!errors.country_code} helperText={(errors as any)?.country_code?.message} required />} renderInput={(params) => (
<TextField
{...params}
label="Выберите страну"
margin="normal"
variant="outlined"
error={!!errors.country_code}
required
/>
)}
/> />
)} )}
/> />
<TextField <TextField
{...register('name', { {...register("name", {
required: 'Это поле является обязательным', required: "Это поле является обязательным",
onBlur: updateCurrentLanguageName,
})} })}
error={!!(errors as any)?.name} error={!!errors.name}
helperText={(errors as any)?.name?.message}
margin="normal" margin="normal"
fullWidth fullWidth
InputLabelProps={{shrink: true}} InputLabelProps={{ shrink: true }}
type="text" type="text"
label={'Название *'} label={"Название *"}
name="name" name="name"
/> />
@ -71,27 +226,53 @@ export const CityCreate = () => {
control={control} control={control}
name="arms" name="arms"
defaultValue={null} defaultValue={null}
render={({field}) => ( render={({ field }) => (
<Autocomplete <Autocomplete
{...mediaAutocompleteProps} {...mediaAutocompleteProps}
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null} value={
onChange={(_, value) => { mediaAutocompleteProps.options.find(
field.onChange(value?.id || '') (option: { id: string; media_name: string }) =>
option.id === field.value
) || null
}
onChange={(
_,
value: { id: string; media_name: string } | null
) => {
field.onChange(value?.id || "");
}} }}
getOptionLabel={(item) => { getOptionLabel={(item: { id: string; media_name: string }) => {
return item ? item.media_name : '' return item ? item.media_name : "";
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(
return option.id === value?.id option: { id: string; media_name: string },
value: { id: string; media_name: string }
) => {
return option.id === value?.id;
}} }}
filterOptions={(options, {inputValue}) => { filterOptions={(
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase())) options: { id: string; media_name: string }[],
{ 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} />} renderInput={(params) => (
<TextField
{...params}
label="Выберите герб"
margin="normal"
variant="outlined"
error={!!errors.arms}
/>
)}
/> />
)} )}
/> />
</Box> </Box>
</Create> </Create>
) );
} };

View File

@ -1,13 +1,20 @@
import {AuthPage, ThemedTitleV2} from '@refinedev/mui' import { AuthPage, ThemedTitleV2 } from "@refinedev/mui";
import {ProjectIcon} from '../../components/ui/Icons'
import { Logo } from "@/icons/Logo";
export const Login = () => { export const Login = () => {
return ( return (
<AuthPage <AuthPage
type="login" type="login"
title={<ThemedTitleV2 collapsed={false} text="Белые Ночи" icon={<ProjectIcon style={{color: '#7f6b58'}} />} />} title={
<ThemedTitleV2
collapsed={false}
text="Белые Ночи"
icon={<Logo width={24} height={24} />}
/>
}
forgotPasswordLink={false} forgotPasswordLink={false}
registerLink={false} // only admin can add users registerLink={false} // only admin can add users
/> />
) );
} };

View File

@ -100,7 +100,6 @@ export const MediaList = observer(() => {
columns={columns} columns={columns}
localeText={localeText} localeText={localeText}
getRowId={(row: any) => row.id} getRowId={(row: any) => row.id}
languageEnabled
/> />
</List> </List>
); );

View File

@ -114,10 +114,10 @@ export const StationLabel = observer(
rotation={-rotation} rotation={-rotation}
> >
<pixiText <pixiText
anchor={{ x: 0.5, y: 0.5 }} anchor={{ x: 1, y: 0.5 }}
text={station.name} text={station.name}
position={{ position={{
x: position.x / scale, x: position.x / scale + 24,
y: position.y / scale, y: position.y / scale,
}} }}
style={{ style={{
@ -129,10 +129,10 @@ export const StationLabel = observer(
{ruLabel && ( {ruLabel && (
<pixiText <pixiText
anchor={{ x: 0.5, y: -1 }} anchor={{ x: 1, y: -1 }}
text={ruLabel} text={ruLabel}
position={{ position={{
x: position.x / scale, x: position.x / scale + 24,
y: position.y / scale, y: position.y / scale,
}} }}
style={{ style={{

View File

@ -153,7 +153,11 @@ export const RouteMap = observer(() => {
<Station <Station
station={obj} station={obj}
key={obj.id} key={obj.id}
ruLabel={language === "ru" ? null : stationData.ru[index].name} ruLabel={
language === "ru"
? stationData.en[index].name
: stationData.ru[index].name
}
/> />
))} ))}

View File

@ -22,6 +22,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
export const SightCreate = observer(() => { export const SightCreate = observer(() => {
const { language, setLanguageAction } = languageStore; const { language, setLanguageAction } = languageStore;
const [coordinates, setCoordinates] = useState("");
const [sightData, setSightData] = useState({ const [sightData, setSightData] = useState({
name: EVERY_LANGUAGE(""), name: EVERY_LANGUAGE(""),
address: EVERY_LANGUAGE(""), address: EVERY_LANGUAGE(""),
@ -79,10 +80,7 @@ export const SightCreate = observer(() => {
}); });
const [namePreview, setNamePreview] = useState(""); const [namePreview, setNamePreview] = useState("");
const [coordinatesPreview, setCoordinatesPreview] = useState({ const [coordinatesPreview, setCoordinatesPreview] = useState("");
latitude: "",
longitude: "",
});
const [creatingArticleHeading, setCreatingArticleHeading] = const [creatingArticleHeading, setCreatingArticleHeading] =
useState<string>(""); useState<string>("");
@ -102,13 +100,19 @@ export const SightCreate = observer(() => {
const [previewArticlePreview, setPreviewArticlePreview] = useState(""); const [previewArticlePreview, setPreviewArticlePreview] = useState("");
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const [lat, lon] = e.target.value.split(",").map((s) => s.trim()); setCoordinates(e.target.value);
setCoordinatesPreview({ if (e.target.value) {
latitude: lat, const [lat, lon] = e.target.value.split(" ").map((s) => s.trim());
longitude: lon, setCoordinatesPreview(
}); `${lat ? Number(lat) : 0}, ${lon ? Number(lon) : 0}`
setValue("latitude", lat); );
setValue("longitude", lon); setValue("latitude", lat ? Number(lat) : 0);
setValue("longitude", lon ? Number(lon) : 0);
} else {
setCoordinatesPreview("");
setValue("latitude", "");
setValue("longitude", "");
}
}; };
// Автокомплиты // Автокомплиты
@ -170,13 +174,6 @@ export const SightCreate = observer(() => {
setNamePreview(nameContent ?? ""); setNamePreview(nameContent ?? "");
}, [nameContent]); }, [nameContent]);
useEffect(() => {
setCoordinatesPreview({
latitude: latitudeContent || "",
longitude: longitudeContent || "",
});
}, [latitudeContent, longitudeContent]);
useEffect(() => { useEffect(() => {
const selectedCity = cityAutocompleteProps.options.find( const selectedCity = cityAutocompleteProps.options.find(
(option) => option.id === cityContent (option) => option.id === cityContent
@ -276,7 +273,7 @@ export const SightCreate = observer(() => {
/> />
<TextField <TextField
value={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} value={coordinates}
onChange={handleCoordinatesChange} onChange={handleCoordinatesChange}
error={!!(errors as any)?.latitude} error={!!(errors as any)?.latitude}
helperText={(errors as any)?.latitude?.message} helperText={(errors as any)?.latitude?.message}
@ -286,10 +283,11 @@ export const SightCreate = observer(() => {
type="text" type="text"
label={"Координаты *"} label={"Координаты *"}
/> />
<input <input
type="hidden" type="hidden"
{...register("longitude", { {...register("longitude", {
value: coordinatesPreview.longitude, value: coordinates.split(" ")[1],
required: "Это поле является обязательным", required: "Это поле является обязательным",
valueAsNumber: true, valueAsNumber: true,
})} })}
@ -297,7 +295,7 @@ export const SightCreate = observer(() => {
<input <input
type="hidden" type="hidden"
{...register("latitude", { {...register("latitude", {
value: coordinatesPreview.latitude, value: coordinates.split(" ")[0],
required: "Это поле является обязательным", required: "Это поле является обязательным",
valueAsNumber: true, valueAsNumber: true,
})} })}
@ -501,13 +499,13 @@ export const SightCreate = observer(() => {
onChange={(_, value) => { onChange={(_, value) => {
field.onChange(value?.id ?? ""); field.onChange(value?.id ?? "");
}} }}
getOptionLabel={(item) => (item ? item.heading : "")} getOptionLabel={(item) => (item ? item.service_name : "")}
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) =>
option.id === value?.id option.id === value?.id
} }
filterOptions={(options, { inputValue }) => filterOptions={(options, { inputValue }) =>
options.filter((option) => options.filter((option) =>
option.heading option.service_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase())
) )
@ -668,21 +666,22 @@ export const SightCreate = observer(() => {
</Typography> </Typography>
{/* Координаты */} {/* Координаты */}
<Typography variant="body1" sx={{ mb: 2 }}> {coordinatesPreview && (
<Box component="span" sx={{ color: "text.secondary" }}> <Typography variant="body1" sx={{ mb: 2 }}>
Координаты:{" "} <Box component="span" sx={{ color: "text.secondary" }}>
</Box> Координаты:{" "}
<Box </Box>
component="span" <Box
sx={{ component="span"
color: (theme) => sx={{
theme.palette.mode === "dark" ? "grey.300" : "grey.800", color: (theme) =>
}} theme.palette.mode === "dark" ? "grey.300" : "grey.800",
> }}
{`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} >
</Box> {coordinatesPreview}
</Typography> </Box>
</Typography>
)}
{/* Обложка */} {/* Обложка */}
{thumbnailPreview && ( {thumbnailPreview && (
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>

View File

@ -70,6 +70,7 @@ export const SightEdit = observer(() => {
name: EVERY_LANGUAGE(""), name: EVERY_LANGUAGE(""),
address: EVERY_LANGUAGE(""), address: EVERY_LANGUAGE(""),
}); });
const [coordinates, setCoordinates] = useState("");
const { const {
saveButtonProps, saveButtonProps,
@ -163,37 +164,13 @@ export const SightEdit = observer(() => {
setValue("address", sightData.address[language]); setValue("address", sightData.address[language]);
}, [language, sightData, setValue]); }, [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] = const [creatingArticleHeading, setCreatingArticleHeading] =
useState<string>(""); useState<string>("");
const [creatingArticleBody, setCreatingArticleBody] = useState<string>(""); const [creatingArticleBody, setCreatingArticleBody] = useState<string>("");
const [coordinatesPreview, setCoordinatesPreview] = useState({ const [coordinatesPreview, setCoordinatesPreview] = useState("");
latitude: "",
longitude: "",
});
const [selectedArticleIndex, setSelectedArticleIndex] = useState(-1); const [selectedArticleIndex, setSelectedArticleIndex] = useState(-1);
const [previewMediaFile, setPreviewMediaFile] = useState<MediaData>(); const [previewMediaFile, setPreviewMediaFile] = useState<MediaData>();
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null); const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
@ -243,12 +220,25 @@ export const SightEdit = observer(() => {
const watermarkRDContent = watch("watermark_rd"); const watermarkRDContent = watch("watermark_rd");
useEffect(() => { useEffect(() => {
setCoordinatesPreview({ if (latitudeContent && longitudeContent) {
latitude: latitudeContent ?? "", setCoordinates(`${latitudeContent} ${longitudeContent}`);
longitude: longitudeContent ?? "", }
});
}, [latitudeContent, longitudeContent]); }, [latitudeContent, longitudeContent]);
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCoordinates(e.target.value);
if (e.target.value) {
const [lat, lon] = e.target.value.split(" ").map((s) => s.trim());
setCoordinatesPreview(`${lat ?? "0"}, ${lon ?? "0"}`);
setValue("latitude", lat ?? "");
setValue("longitude", lon ?? "");
} else {
setCoordinatesPreview("");
setValue("latitude", "");
setValue("longitude", "");
}
};
useEffect(() => { useEffect(() => {
if (linkedArticles[selectedArticleIndex]?.id) { if (linkedArticles[selectedArticleIndex]?.id) {
getMedia(linkedArticles[selectedArticleIndex].id).then((media) => { getMedia(linkedArticles[selectedArticleIndex].id).then((media) => {
@ -505,7 +495,6 @@ export const SightEdit = observer(() => {
<input <input
type="hidden" type="hidden"
{...register("longitude", { {...register("longitude", {
value: coordinatesPreview.longitude,
required: "Это поле является обязательным", required: "Это поле является обязательным",
valueAsNumber: true, valueAsNumber: true,
})} })}
@ -513,7 +502,6 @@ export const SightEdit = observer(() => {
<input <input
type="hidden" type="hidden"
{...register("latitude", { {...register("latitude", {
value: coordinatesPreview.latitude,
required: "Это поле является обязательным", required: "Это поле является обязательным",
valueAsNumber: true, valueAsNumber: true,
})} })}
@ -680,14 +668,14 @@ export const SightEdit = observer(() => {
setLeftArticleData(undefined); setLeftArticleData(undefined);
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
return item ? item.heading : ""; return item ? item.service_name : "";
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(option, value) => {
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter((option) => return options.filter((option) =>
option.heading option.service_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase())
); );
@ -794,6 +782,7 @@ export const SightEdit = observer(() => {
color: (theme) => color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800", theme.palette.mode === "dark" ? "grey.300" : "grey.800",
mb: 3, mb: 3,
mt: 3,
}} }}
> >
{name} {name}
@ -990,14 +979,14 @@ export const SightEdit = observer(() => {
setLeftArticleData(undefined); setLeftArticleData(undefined);
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
return item ? item.heading : ""; return item ? item.service_name : "";
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(option, value) => {
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter((option) => return options.filter((option) =>
option.heading option.service_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase())
); );
@ -1535,7 +1524,7 @@ export const SightEdit = observer(() => {
/> />
<TextField <TextField
value={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} value={coordinates}
onChange={handleCoordinatesChange} onChange={handleCoordinatesChange}
error={!!(errors as any)?.latitude} error={!!(errors as any)?.latitude}
helperText={(errors as any)?.latitude?.message} helperText={(errors as any)?.latitude?.message}
@ -1687,7 +1676,7 @@ export const SightEdit = observer(() => {
: "grey.800", : "grey.800",
}} }}
> >
{`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} {coordinatesPreview}
</Box> </Box>
</Typography> </Typography>
</Box> </Box>

View File

@ -56,6 +56,7 @@ export const SightList = observer(() => {
display: "flex", display: "flex",
align: "left", align: "left",
headerAlign: "left", headerAlign: "left",
flex: 1,
}, },
{ {
field: "latitude", field: "latitude",

View File

@ -41,40 +41,19 @@ export const StationCreate = () => {
}, },
}); });
const [coordinatesPreview, setCoordinatesPreview] = useState({ const [coordinates, setCoordinates] = useState("");
latitude: "",
longitude: "",
});
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const [lat, lon] = e.target.value.split(",").map((s) => s.trim()); setCoordinates(e.target.value);
setCoordinatesPreview({ const [lat, lon] = e.target.value
latitude: lat, .replace(/,/g, "") // Remove all commas from the string
longitude: lon, .split(" ")
}); .map((s) => s.trim());
console.log(lat, lon);
setValue("latitude", lat); setValue("latitude", lat);
setValue("longitude", lon); setValue("longitude", lon);
}; };
const latitudeContent = watch("latitude");
const longitudeContent = watch("longitude");
useEffect(() => {
setCoordinatesPreview({
latitude: latitudeContent || "",
longitude: longitudeContent || "",
});
}, [latitudeContent, longitudeContent]);
useEffect(() => {
const latitude = getValues("latitude");
const longitude = getValues("longitude");
if (latitude && longitude) {
setCoordinatesPreview({
latitude: latitude,
longitude: longitude,
});
}
}, [getValues]);
const directions = [ const directions = [
{ {
label: "Прямой", label: "Прямой",
@ -188,7 +167,7 @@ export const StationCreate = () => {
)} )}
/> />
<TextField <TextField
value={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} value={coordinates}
onChange={handleCoordinatesChange} onChange={handleCoordinatesChange}
error={!!(errors as any)?.latitude} error={!!(errors as any)?.latitude}
helperText={(errors as any)?.latitude?.message} helperText={(errors as any)?.latitude?.message}
@ -198,10 +177,11 @@ export const StationCreate = () => {
type="text" type="text"
label={"Координаты *"} label={"Координаты *"}
/> />
<input <input
type="hidden" type="hidden"
{...register("latitude", { {...register("latitude", {
value: coordinatesPreview.latitude, value: coordinates.split(",")[0],
setValueAs: (value) => { setValueAs: (value) => {
if (value === "") { if (value === "") {
return 0; return 0;
@ -213,7 +193,7 @@ export const StationCreate = () => {
<input <input
type="hidden" type="hidden"
{...register("longitude", { {...register("longitude", {
value: coordinatesPreview.longitude, value: coordinates.split(",")[1],
setValueAs: (value) => { setValueAs: (value) => {
if (value === "") { if (value === "") {
return 0; return 0;

View File

@ -40,24 +40,24 @@ export const StationEdit = observer(() => {
system_name: "", system_name: "",
description: "", description: "",
address: "", address: "",
latitude: "", latitude: 0,
longitude: "", longitude: 0,
}, },
en: { en: {
name: "", name: "",
system_name: "", system_name: "",
description: "", description: "",
address: "", address: "",
latitude: "", latitude: 0,
longitude: "", longitude: 0,
}, },
zh: { zh: {
name: "", name: "",
system_name: "", system_name: "",
description: "", description: "",
address: "", address: "",
latitude: "", latitude: 0,
longitude: "", longitude: 0,
}, },
}); });
@ -69,8 +69,8 @@ export const StationEdit = observer(() => {
system_name: watch("system_name") ?? "", system_name: watch("system_name") ?? "",
description: watch("description") ?? "", description: watch("description") ?? "",
address: watch("address") ?? "", address: watch("address") ?? "",
latitude: watch("latitude") ?? "", latitude: Number(watch("latitude")) || 0,
longitude: watch("longitude") ?? "", longitude: Number(watch("longitude")) || 0,
}, },
})); }));
}; };
@ -120,7 +120,7 @@ export const StationEdit = observer(() => {
if (stationData[language as keyof typeof stationData]?.name) { if (stationData[language as keyof typeof stationData]?.name) {
setValue("name", stationData[language as keyof typeof stationData]?.name); setValue("name", stationData[language as keyof typeof stationData]?.name);
} }
if (stationData[language as keyof typeof stationData]?.address) { if (stationData[language as keyof typeof stationData]?.system_name) {
setValue( setValue(
"system_name", "system_name",
stationData[language as keyof typeof stationData]?.system_name || "" stationData[language as keyof typeof stationData]?.system_name || ""
@ -132,16 +132,20 @@ export const StationEdit = observer(() => {
stationData[language as keyof typeof stationData]?.description || "" stationData[language as keyof typeof stationData]?.description || ""
); );
} }
if (stationData[language as keyof typeof stationData]?.latitude) { if (
stationData[language as keyof typeof stationData]?.latitude !== undefined
) {
setValue( setValue(
"latitude", "latitude",
stationData[language as keyof typeof stationData]?.latitude || "" stationData[language as keyof typeof stationData]?.latitude || 0
); );
} }
if (stationData[language as keyof typeof stationData]?.longitude) { if (
stationData[language as keyof typeof stationData]?.longitude !== undefined
) {
setValue( setValue(
"longitude", "longitude",
stationData[language as keyof typeof stationData]?.longitude || "" stationData[language as keyof typeof stationData]?.longitude || 0
); );
} }
}, [language, stationData, setValue]); }, [language, stationData, setValue]);
@ -150,28 +154,36 @@ export const StationEdit = observer(() => {
setLanguageAction("ru"); setLanguageAction("ru");
}, []); }, []);
const { id: stationId } = useParams<{ id: string }>(); const [coordinates, setCoordinates] = useState("");
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { id: stationId } = useParams<{ id: string }>();
const [lat, lon] = e.target.value.split(",").map((s) => s.trim());
setCoordinatesPreview({
latitude: lat,
longitude: lon,
});
setValue("latitude", lat);
setValue("longitude", lon);
};
const latitudeContent = watch("latitude"); const latitudeContent = watch("latitude");
const longitudeContent = watch("longitude"); const longitudeContent = watch("longitude");
useEffect(() => { useEffect(() => {
setCoordinatesPreview({ if (latitudeContent && longitudeContent) {
latitude: latitudeContent || "", setCoordinates(`${latitudeContent} ${longitudeContent}`);
longitude: longitudeContent || "", }
});
}, [latitudeContent, longitudeContent]); }, [latitudeContent, longitudeContent]);
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCoordinates(e.target.value);
if (e.target.value) {
const [lat, lon] = e.target.value
.replace(/,/g, "") // Remove all commas from the string
.split(" ")
.map((s) => s.trim());
setCoordinates(`${lat ?? 0} ${lon ?? 0}`);
setValue("latitude", lat ?? 0);
setValue("longitude", lon ?? 0);
} else {
setCoordinates("");
setValue("latitude", "");
setValue("longitude", "");
}
};
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({ const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
resource: "city", resource: "city",
onSearch: (value) => [ onSearch: (value) => [
@ -282,7 +294,7 @@ export const StationEdit = observer(() => {
/> />
<TextField <TextField
value={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} value={coordinates}
onChange={handleCoordinatesChange} onChange={handleCoordinatesChange}
error={!!(errors as any)?.latitude} error={!!(errors as any)?.latitude}
helperText={(errors as any)?.latitude?.message} helperText={(errors as any)?.latitude?.message}
@ -295,12 +307,28 @@ export const StationEdit = observer(() => {
<input <input
type="hidden" type="hidden"
{...register("latitude", { {...register("latitude", {
value: coordinatesPreview.latitude, valueAsNumber: true,
value: Number(coordinates.split(" ")[0]),
setValueAs: (value) => {
if (value === "") {
return 0;
}
return Number(value);
},
})} })}
/> />
<input <input
type="hidden" type="hidden"
{...register("longitude", { value: coordinatesPreview.longitude })} {...register("longitude", {
valueAsNumber: true,
value: Number(coordinates.split(" ")[1]),
setValueAs: (value) => {
if (value === "") {
return 0;
}
return Number(value);
},
})}
/> />
<Controller <Controller

View File

@ -104,6 +104,7 @@ export const VehicleList = observer(() => {
type: "string", type: "string",
minWidth: 150, minWidth: 150,
display: "flex", display: "flex",
flex: 1,
align: "left", align: "left",
headerAlign: "left", headerAlign: "left",
renderCell: (params) => { renderCell: (params) => {
@ -116,6 +117,7 @@ export const VehicleList = observer(() => {
headerName: "Город", headerName: "Город",
type: "string", type: "string",
minWidth: 150, minWidth: 150,
flex: 1,
display: "flex", display: "flex",
align: "left", align: "left",
headerAlign: "left", headerAlign: "left",