Compare commits
1 Commits
9bf294e124
...
master
Author | SHA1 | Date | |
---|---|---|---|
cf2a116ecb |
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<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
|
||||||
@ -19,9 +19,7 @@
|
|||||||
name="twitter:image"
|
name="twitter:image"
|
||||||
content="https://refine.dev/img/refine_social.png"
|
content="https://refine.dev/img/refine_social.png"
|
||||||
/>
|
/>
|
||||||
<title>
|
<title>Белые ночи</title>
|
||||||
Белые ночи
|
|
||||||
</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.2 KiB |
BIN
public/favicon_ship.png
Normal file
BIN
public/favicon_ship.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
@ -70,6 +70,7 @@ type LinkedItemsProps<T> = {
|
|||||||
disableCreation?: boolean;
|
disableCreation?: boolean;
|
||||||
updatedLinkedItems?: T[];
|
updatedLinkedItems?: T[];
|
||||||
refresh?: number;
|
refresh?: number;
|
||||||
|
cityId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reorder = (list: any[], startIndex: number, endIndex: number) => {
|
const reorder = (list: any[], startIndex: number, endIndex: number) => {
|
||||||
@ -131,6 +132,7 @@ export const LinkedItemsContents = <
|
|||||||
disableCreation = false,
|
disableCreation = false,
|
||||||
updatedLinkedItems,
|
updatedLinkedItems,
|
||||||
refresh,
|
refresh,
|
||||||
|
cityId,
|
||||||
}: LinkedItemsProps<T>) => {
|
}: LinkedItemsProps<T>) => {
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
|
const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
|
||||||
@ -216,7 +218,7 @@ export const LinkedItemsContents = <
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === "edit") {
|
if (type === "edit") {
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.get(`${import.meta.env.VITE_KRBL_API}/${childResource}/`)
|
.get(`${import.meta.env.VITE_KRBL_API}/${childResource}/`, {})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setItems(response?.data || []);
|
setItems(response?.data || []);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -445,7 +447,7 @@ export const LinkedItemsContents = <
|
|||||||
availableItems?.find((item) => item.id === selectedItemId) || null
|
availableItems?.find((item) => item.id === selectedItemId) || null
|
||||||
}
|
}
|
||||||
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
|
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
|
||||||
options={availableItems}
|
options={availableItems.filter((item) => item.city_id == cityId)}
|
||||||
getOptionLabel={(item) => String(item[fields[0].data])}
|
getOptionLabel={(item) => String(item[fields[0].data])}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField {...params} label={`Выберите ${title}`} fullWidth />
|
<TextField {...params} label={`Выберите ${title}`} fullWidth />
|
||||||
@ -456,6 +458,7 @@ export const LinkedItemsContents = <
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.filter((word) => word.length > 0);
|
.filter((word) => word.length > 0);
|
||||||
|
|
||||||
return options.filter((option) => {
|
return options.filter((option) => {
|
||||||
const optionWords = String(option[fields[0].data])
|
const optionWords = String(option[fields[0].data])
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
@ -143,8 +143,17 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = observer(
|
|||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={2}
|
spacing={2}
|
||||||
|
color="white"
|
||||||
|
sx={{
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FormControl variant="standard" sx={{ width: "min-content" }}>
|
<FormControl
|
||||||
|
variant="standard"
|
||||||
|
sx={{ width: "min-content", color: "white" }}
|
||||||
|
>
|
||||||
{city_id && cities && (
|
{city_id && cities && (
|
||||||
<Select
|
<Select
|
||||||
defaultValue={city_id}
|
defaultValue={city_id}
|
||||||
|
@ -121,7 +121,7 @@ export const StationEditModal = observer(() => {
|
|||||||
>
|
>
|
||||||
<Box sx={style}>
|
<Box sx={style}>
|
||||||
<Edit
|
<Edit
|
||||||
title={<Typography variant="h5">Редактирование станции</Typography>}
|
title={<Typography variant="h5">Редактирование остановки</Typography>}
|
||||||
saveButtonProps={saveButtonProps}
|
saveButtonProps={saveButtonProps}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
@ -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",
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import {ProjectIcon} from './Icons'
|
import { Logo } from "@/icons/Logo";
|
||||||
|
|
||||||
export default function SidebarTitle({collapsed}: {collapsed: boolean}) {
|
export default function SidebarTitle({ collapsed }: { collapsed: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div style={{display: 'flex', alignItems: 'center', whiteSpace: 'nowrap'}}>
|
<div
|
||||||
<ProjectIcon style={{color: '#7f6b58'}} />
|
style={{ display: "flex", alignItems: "center", whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
<Logo width={40} height={40} />
|
||||||
|
|
||||||
{!collapsed && <span style={{marginLeft: 8, fontWeight: 'bold'}}>Белые ночи</span>}
|
{!collapsed && (
|
||||||
|
<span style={{ marginLeft: 8, fontWeight: "bold" }}>Белые ночи</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 35px;
|
width: 32px;
|
||||||
height: 35px;
|
height: 32px;
|
||||||
color: #544044;
|
color: rgba(79, 138, 95, 1);
|
||||||
border-radius: 10%;
|
border-radius: 10%;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
.backup-button:hover {
|
.backup-button:hover {
|
||||||
background-color: rgba(84, 64, 68, 0.5);
|
background-color: rgba(79, 138, 95, 0.05);
|
||||||
}
|
}
|
||||||
|
3
src/icons/124.svg
Normal file
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
22
src/icons/Logo.tsx
Normal file
File diff suppressed because one or more lines are too long
@ -94,9 +94,9 @@
|
|||||||
},
|
},
|
||||||
"station": {
|
"station": {
|
||||||
"titles": {
|
"titles": {
|
||||||
"create": "Создать станцию",
|
"create": "Создать остановку",
|
||||||
"edit": "Редактировать станцию",
|
"edit": "Редактировать остановку",
|
||||||
"show": "Показать станцию"
|
"show": "Показать остановку"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"snapshots": {
|
"snapshots": {
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -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
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -1,179 +1,230 @@
|
|||||||
import { FederatedMouseEvent, FederatedWheelEvent } from "pixi.js";
|
import { FederatedMouseEvent, FederatedWheelEvent } from "pixi.js";
|
||||||
import { Component, ReactNode, useEffect, useState } from "react";
|
import { Component, ReactNode, useEffect, useState, useRef } from "react";
|
||||||
import { useTransform } from "./TransformContext";
|
import { useTransform } from "./TransformContext";
|
||||||
import { useMapData } from "./MapDataContext";
|
import { useMapData } from "./MapDataContext";
|
||||||
import { SCALE_FACTOR } from "./Constants";
|
import { SCALE_FACTOR } from "./Constants";
|
||||||
import { useApplication } from "@pixi/react";
|
import { useApplication } from "@pixi/react";
|
||||||
|
|
||||||
class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
|
class ErrorBoundary extends Component<
|
||||||
state = { hasError: false };
|
{ children: ReactNode },
|
||||||
|
{ hasError: boolean }
|
||||||
static getDerivedStateFromError() {
|
> {
|
||||||
return { hasError: true };
|
state = { hasError: false };
|
||||||
}
|
|
||||||
|
static getDerivedStateFromError() {
|
||||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
return { hasError: true };
|
||||||
console.error("Error caught:", error, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.state.hasError ? <p>Whoopsie Daisy!</p> : this.props.children;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||||
|
console.error("Error caught:", error, info);
|
||||||
|
}
|
||||||
|
|
||||||
export function InfiniteCanvas({children} : Readonly<{children?: ReactNode}>) {
|
render() {
|
||||||
const { position, setPosition, scale, setScale, rotation, setRotation, setScreenCenter, screenCenter } = useTransform();
|
return this.state.hasError ? <p>Whoopsie Daisy!</p> : this.props.children;
|
||||||
const { routeData, originalRouteData } = useMapData();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const applicationRef = useApplication();
|
export function InfiniteCanvas({
|
||||||
|
children,
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
}: Readonly<{ children?: ReactNode }>) {
|
||||||
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
const {
|
||||||
const [startRotation, setStartRotation] = useState(0);
|
position,
|
||||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
setPosition,
|
||||||
|
scale,
|
||||||
|
setScale,
|
||||||
|
rotation,
|
||||||
|
setRotation,
|
||||||
|
setScreenCenter,
|
||||||
|
screenCenter,
|
||||||
|
} = useTransform();
|
||||||
|
const { routeData, originalRouteData } = useMapData();
|
||||||
|
|
||||||
useEffect(() => {
|
const applicationRef = useApplication();
|
||||||
const canvas = applicationRef?.app.canvas;
|
|
||||||
if (!canvas) return;
|
|
||||||
const canvasRect = canvas.getBoundingClientRect();
|
|
||||||
const canvasLeft = canvasRect?.left ?? 0;
|
|
||||||
const canvasTop = canvasRect?.top ?? 0;
|
|
||||||
const centerX = window.innerWidth / 2 - canvasLeft;
|
|
||||||
const centerY = window.innerHeight / 2 - canvasTop;
|
|
||||||
setScreenCenter({x: centerX, y: centerY});
|
|
||||||
}, [applicationRef?.app.canvas, window.innerWidth, window.innerHeight]);
|
|
||||||
|
|
||||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
setIsDragging(true);
|
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
||||||
setStartPosition({
|
const [startRotation, setStartRotation] = useState(0);
|
||||||
x: position.x,
|
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||||
y: position.y
|
|
||||||
});
|
|
||||||
setStartMousePosition({
|
|
||||||
x: e.globalX,
|
|
||||||
y: e.globalY
|
|
||||||
});
|
|
||||||
setStartRotation(rotation);
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута
|
||||||
|
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
// Реф для отслеживания последнего значения originalRouteData?.rotate
|
||||||
setRotation((originalRouteData?.rotate ?? 0) * Math.PI / 180);
|
const lastOriginalRotation = useRef<number | undefined>(undefined);
|
||||||
}, [originalRouteData?.rotate]);
|
|
||||||
// Get canvas element and its dimensions/position
|
|
||||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
|
||||||
if (!isDragging) return;
|
|
||||||
|
|
||||||
if (e.shiftKey) {
|
useEffect(() => {
|
||||||
const center = screenCenter ?? {x: 0, y: 0};
|
const canvas = applicationRef?.app.canvas;
|
||||||
const startAngle = Math.atan2(startMousePosition.y - center.y, startMousePosition.x - center.x);
|
if (!canvas) return;
|
||||||
const currentAngle = Math.atan2(e.globalY - center.y, e.globalX - center.x);
|
|
||||||
|
|
||||||
// Calculate rotation difference in radians
|
const canvasRect = canvas.getBoundingClientRect();
|
||||||
const rotationDiff = currentAngle - startAngle;
|
const canvasLeft = canvasRect.left;
|
||||||
|
const canvasTop = canvasRect.top;
|
||||||
|
const centerX = window.innerWidth / 2 - canvasLeft;
|
||||||
|
const centerY = window.innerHeight / 2 - canvasTop;
|
||||||
|
setScreenCenter({ x: centerX, y: centerY });
|
||||||
|
}, [applicationRef?.app.canvas, setScreenCenter]);
|
||||||
|
|
||||||
// Update rotation
|
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||||
setRotation(startRotation + rotationDiff);
|
setIsDragging(true);
|
||||||
|
setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя
|
||||||
|
setStartPosition({
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
});
|
||||||
|
setStartMousePosition({
|
||||||
|
x: e.globalX,
|
||||||
|
y: e.globalY,
|
||||||
|
});
|
||||||
|
setStartRotation(rotation);
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
const cosDelta = Math.cos(rotationDiff);
|
// Устанавливаем rotation только при изменении originalRouteData и отсутствии взаимодействия пользователя
|
||||||
const sinDelta = Math.sin(rotationDiff);
|
useEffect(() => {
|
||||||
|
const newRotation = originalRouteData?.rotate ?? 0;
|
||||||
setPosition({
|
|
||||||
x: center.x * (1 - cosDelta) + startPosition.x * cosDelta + (center.y - startPosition.y) * sinDelta,
|
|
||||||
y: center.y * (1 - cosDelta) + startPosition.y * cosDelta + (startPosition.x - center.x) * sinDelta
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
setRotation(startRotation);
|
|
||||||
setPosition({
|
|
||||||
x: startPosition.x - startMousePosition.x + e.globalX,
|
|
||||||
y: startPosition.y - startMousePosition.y + e.globalY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle mouse up
|
// Обновляем rotation только если:
|
||||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
// 1. Пользователь не взаимодействует с канвасом
|
||||||
setIsDragging(false);
|
// 2. Значение действительно изменилось
|
||||||
e.stopPropagation();
|
if (!isUserInteracting && lastOriginalRotation.current !== newRotation) {
|
||||||
};
|
setRotation((newRotation * Math.PI) / 180);
|
||||||
// Handle mouse wheel for zooming
|
lastOriginalRotation.current = newRotation;
|
||||||
const handleWheel = (e: FederatedWheelEvent) => {
|
}
|
||||||
e.stopPropagation();
|
}, [originalRouteData?.rotate, isUserInteracting, setRotation]);
|
||||||
|
|
||||||
// Get mouse position relative to canvas
|
|
||||||
const mouseX = e.globalX - position.x;
|
|
||||||
const mouseY = e.globalY - position.y;
|
|
||||||
|
|
||||||
// Calculate new scale
|
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||||
const scaleMin = (routeData?.scale_min ?? 10)/SCALE_FACTOR;
|
if (!isDragging) return;
|
||||||
const scaleMax = (routeData?.scale_max ?? 20)/SCALE_FACTOR;
|
|
||||||
|
|
||||||
let zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in
|
if (e.shiftKey) {
|
||||||
//const newScale = scale * zoomFactor;
|
const center = screenCenter ?? { x: 0, y: 0 };
|
||||||
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
const startAngle = Math.atan2(
|
||||||
zoomFactor = newScale / scale;
|
startMousePosition.y - center.y,
|
||||||
|
startMousePosition.x - center.x
|
||||||
|
);
|
||||||
|
const currentAngle = Math.atan2(
|
||||||
|
e.globalY - center.y,
|
||||||
|
e.globalX - center.x
|
||||||
|
);
|
||||||
|
|
||||||
if (scale === newScale) {
|
// Calculate rotation difference in radians
|
||||||
return;
|
const rotationDiff = currentAngle - startAngle;
|
||||||
}
|
|
||||||
|
|
||||||
// Update position to zoom towards mouse cursor
|
// Update rotation
|
||||||
setPosition({
|
setRotation(startRotation + rotationDiff);
|
||||||
x: position.x + mouseX * (1 - zoomFactor),
|
|
||||||
y: position.y + mouseY * (1 - zoomFactor)
|
|
||||||
});
|
|
||||||
|
|
||||||
setScale(newScale);
|
const cosDelta = Math.cos(rotationDiff);
|
||||||
};
|
const sinDelta = Math.sin(rotationDiff);
|
||||||
|
|
||||||
useEffect(() => {
|
setPosition({
|
||||||
applicationRef?.app.render();
|
x:
|
||||||
console.log(position, scale, rotation);
|
center.x * (1 - cosDelta) +
|
||||||
}, [position, scale, rotation]);
|
startPosition.x * cosDelta +
|
||||||
|
(center.y - startPosition.y) * sinDelta,
|
||||||
|
y:
|
||||||
|
center.y * (1 - cosDelta) +
|
||||||
|
startPosition.y * cosDelta +
|
||||||
|
(startPosition.x - center.x) * sinDelta,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setRotation(startRotation);
|
||||||
|
setPosition({
|
||||||
|
x: startPosition.x - startMousePosition.x + e.globalX,
|
||||||
|
y: startPosition.y - startMousePosition.y + e.globalY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||||
return (
|
setIsDragging(false);
|
||||||
<ErrorBoundary>
|
// Сбрасываем флаг взаимодействия через небольшую задержку
|
||||||
{applicationRef?.app && (
|
// чтобы избежать немедленного срабатывания useEffect
|
||||||
<pixiGraphics
|
setTimeout(() => {
|
||||||
draw={(g) => {
|
setIsUserInteracting(false);
|
||||||
const canvas = applicationRef.app.canvas;
|
}, 100);
|
||||||
g.clear();
|
e.stopPropagation();
|
||||||
g.rect(0, 0, canvas?.width ?? 0, canvas?.height ?? 0);
|
};
|
||||||
g.fill("#111");
|
|
||||||
}}
|
|
||||||
eventMode={'static'}
|
|
||||||
interactive
|
|
||||||
onPointerDown={handlePointerDown}
|
|
||||||
onGlobalPointerMove={handlePointerMove}
|
|
||||||
onPointerUp={handlePointerUp}
|
|
||||||
onPointerUpOutside={handlePointerUp}
|
|
||||||
onWheel={handleWheel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<pixiContainer
|
|
||||||
x={position.x}
|
|
||||||
y={position.y}
|
|
||||||
scale={scale}
|
|
||||||
rotation={rotation}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</pixiContainer>
|
|
||||||
{/* Show center of the screen.
|
|
||||||
<pixiGraphics
|
|
||||||
eventMode="none"
|
|
||||||
|
|
||||||
draw={(g) => {
|
const handleWheel = (e: FederatedWheelEvent) => {
|
||||||
g.clear();
|
e.stopPropagation();
|
||||||
const center = screenCenter ?? {x: 0, y: 0};
|
setIsUserInteracting(true); // Устанавливаем флаг при зуме
|
||||||
g.circle(center.x, center.y, 1);
|
|
||||||
g.fill("#fff");
|
// Get mouse position relative to canvas
|
||||||
}}
|
const mouseX = e.globalX - position.x;
|
||||||
/> */}
|
const mouseY = e.globalY - position.y;
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
// Calculate new scale
|
||||||
}
|
const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR;
|
||||||
|
const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR;
|
||||||
|
|
||||||
|
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in
|
||||||
|
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
||||||
|
const actualZoomFactor = newScale / scale;
|
||||||
|
|
||||||
|
if (scale === newScale) {
|
||||||
|
// Сбрасываем флаг, если зум не изменился
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUserInteracting(false);
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position to zoom towards mouse cursor
|
||||||
|
setPosition({
|
||||||
|
x: position.x + mouseX * (1 - actualZoomFactor),
|
||||||
|
y: position.y + mouseY * (1 - actualZoomFactor),
|
||||||
|
});
|
||||||
|
|
||||||
|
setScale(newScale);
|
||||||
|
|
||||||
|
// Сбрасываем флаг взаимодействия через задержку
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsUserInteracting(false);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applicationRef?.app.render();
|
||||||
|
console.log(position, scale, rotation);
|
||||||
|
}, [position, scale, rotation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
{applicationRef?.app && (
|
||||||
|
<pixiGraphics
|
||||||
|
draw={(g) => {
|
||||||
|
const canvas = applicationRef.app.canvas;
|
||||||
|
g.clear();
|
||||||
|
g.rect(0, 0, canvas?.width ?? 0, canvas?.height ?? 0);
|
||||||
|
g.fill("#111");
|
||||||
|
}}
|
||||||
|
eventMode={"static"}
|
||||||
|
interactive
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onGlobalPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerUpOutside={handlePointerUp}
|
||||||
|
onWheel={handleWheel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<pixiContainer
|
||||||
|
x={position.x}
|
||||||
|
y={position.y}
|
||||||
|
scale={scale}
|
||||||
|
rotation={rotation}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</pixiContainer>
|
||||||
|
{/* Show center of the screen.
|
||||||
|
<pixiGraphics
|
||||||
|
eventMode="none"
|
||||||
|
draw={(g) => {
|
||||||
|
g.clear();
|
||||||
|
const center = screenCenter ?? {x: 0, y: 0};
|
||||||
|
g.circle(center.x, center.y, 1);
|
||||||
|
g.fill("#fff");
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,33 +1,89 @@
|
|||||||
import { Stack, Typography, Button } from "@mui/material";
|
import { Stack, Typography, Button } from "@mui/material";
|
||||||
|
|
||||||
|
import { useNavigate, useNavigationType } from "react-router";
|
||||||
|
|
||||||
export function LeftSidebar() {
|
export function LeftSidebar() {
|
||||||
return (
|
const navigate = useNavigate();
|
||||||
<Stack direction="column" width="300px" p={2} bgcolor="primary.main">
|
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
|
||||||
<Stack direction="column" alignItems="center" justifyContent="center" my={10}>
|
|
||||||
<img src={"/Emblem.svg"} alt="logo" width={100} height={100} />
|
|
||||||
<Typography sx={{ mb: 2 }} textAlign="center">
|
|
||||||
При поддержке Правительства
|
|
||||||
Санкт-Петербурга
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
<Stack direction="column" alignItems="center" justifyContent="center" my={10} spacing={2}>
|
if (navigationType === "PUSH") {
|
||||||
<Button variant="outlined" color="warning" fullWidth>
|
navigate(-1);
|
||||||
Достопримечательности
|
} else {
|
||||||
</Button>
|
navigate("/route");
|
||||||
<Button variant="outlined" color="warning" fullWidth>
|
}
|
||||||
Остановки
|
};
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack direction="column" alignItems="center" justifyContent="center" my={10}>
|
return (
|
||||||
<img src={"/GET.png"} alt="logo" width="80%" style={{margin: "0 auto"}}/>
|
<Stack direction="column" width="300px" p={2} bgcolor="primary.main">
|
||||||
</Stack>
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
<Typography variant="h6" textAlign="center" mt="auto">#ВсемПоПути</Typography>
|
type="button"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
color: "#fff",
|
||||||
|
backgroundColor: "#222",
|
||||||
|
borderRadius: 10,
|
||||||
|
width: "100%",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>Назад</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Stack
|
||||||
</Stack>
|
direction="column"
|
||||||
);
|
alignItems="center"
|
||||||
}
|
justifyContent="center"
|
||||||
|
my={10}
|
||||||
|
>
|
||||||
|
<img src={"/Emblem.svg"} alt="logo" width={100} height={100} />
|
||||||
|
<Typography sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||||
|
При поддержке Правительства Санкт-Петербурга
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
my={10}
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<Button variant="outlined" color="warning" fullWidth>
|
||||||
|
Достопримечательности
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" color="warning" fullWidth>
|
||||||
|
Остановки
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
my={10}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={"/GET.png"}
|
||||||
|
alt="logo"
|
||||||
|
width="80%"
|
||||||
|
style={{ margin: "0 auto" }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
textAlign="center"
|
||||||
|
mt="auto"
|
||||||
|
sx={{ color: "#fff" }}
|
||||||
|
>
|
||||||
|
#ВсемПоПути
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCustom, useApiUrl } from "@refinedev/core";
|
import { useApiUrl } from "@refinedev/core";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
@ -16,13 +16,16 @@ import {
|
|||||||
StationPatchData,
|
StationPatchData,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { axiosInstance } from "../../providers/data";
|
import { axiosInstance } from "../../providers/data";
|
||||||
|
import { languageStore, META_LANGUAGE } from "@/store/LanguageStore";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { axiosInstanceForGet } from "@/providers/data";
|
||||||
|
|
||||||
const MapDataContext = createContext<{
|
const MapDataContext = createContext<{
|
||||||
originalRouteData?: RouteData;
|
originalRouteData?: RouteData;
|
||||||
originalStationData?: StationData[];
|
originalStationData?: StationData[];
|
||||||
originalSightData?: SightData[];
|
originalSightData?: SightData[];
|
||||||
routeData?: RouteData;
|
routeData?: RouteData;
|
||||||
stationData?: StationData[];
|
stationData?: StationDataWithLanguage;
|
||||||
sightData?: SightData[];
|
sightData?: SightData[];
|
||||||
|
|
||||||
isRouteLoading: boolean;
|
isRouteLoading: boolean;
|
||||||
@ -57,210 +60,247 @@ const MapDataContext = createContext<{
|
|||||||
saveChanges: () => {},
|
saveChanges: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function MapDataProvider({
|
type StationDataWithLanguage = {
|
||||||
children,
|
[key: string]: StationData[];
|
||||||
}: Readonly<{ children: ReactNode }>) {
|
};
|
||||||
const { id: routeId } = useParams<{ id: string }>();
|
export const MapDataProvider = observer(
|
||||||
const apiUrl = useApiUrl();
|
({ children }: Readonly<{ children: ReactNode }>) => {
|
||||||
|
const { id: routeId } = useParams<{ id: string }>();
|
||||||
|
const apiUrl = useApiUrl();
|
||||||
|
|
||||||
const [originalRouteData, setOriginalRouteData] = useState<RouteData>();
|
const [originalRouteData, setOriginalRouteData] = useState<RouteData>();
|
||||||
const [originalStationData, setOriginalStationData] =
|
const [originalStationData, setOriginalStationData] =
|
||||||
useState<StationData[]>();
|
useState<StationData[]>();
|
||||||
const [originalSightData, setOriginalSightData] = useState<SightData[]>();
|
const [originalSightData, setOriginalSightData] = useState<SightData[]>();
|
||||||
|
|
||||||
const [routeData, setRouteData] = useState<RouteData>();
|
const [routeData, setRouteData] = useState<RouteData>();
|
||||||
const [stationData, setStationData] = useState<StationData[]>();
|
const [stationData, setStationData] = useState<StationDataWithLanguage>({
|
||||||
const [sightData, setSightData] = useState<SightData[]>();
|
RU: [],
|
||||||
|
EN: [],
|
||||||
const [routeChanges, setRouteChanges] = useState<RouteData>({} as RouteData);
|
ZH: [],
|
||||||
const [stationChanges, setStationChanges] = useState<StationPatchData[]>([]);
|
|
||||||
const [sightChanges, setSightChanges] = useState<SightPatchData[]>([]);
|
|
||||||
|
|
||||||
const { data: routeQuery, isLoading: isRouteLoading } = useCustom({
|
|
||||||
url: `${apiUrl}/route/${routeId}`,
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
const { data: stationQuery, isLoading: isStationLoading } = useCustom({
|
|
||||||
url: `${apiUrl}/route/${routeId}/station`,
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
const { data: sightQuery, isLoading: isSightLoading } = useCustom({
|
|
||||||
url: `${apiUrl}/route/${routeId}/sight`,
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
// if not undefined, set original data
|
|
||||||
if (routeQuery?.data) setOriginalRouteData(routeQuery.data as RouteData);
|
|
||||||
if (stationQuery?.data)
|
|
||||||
setOriginalStationData(stationQuery.data as StationData[]);
|
|
||||||
if (sightQuery?.data) setOriginalSightData(sightQuery.data as SightData[]);
|
|
||||||
console.log("queries", routeQuery, stationQuery, sightQuery);
|
|
||||||
}, [routeQuery, stationQuery, sightQuery]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// combine changes with original data
|
|
||||||
if (originalRouteData)
|
|
||||||
setRouteData({ ...originalRouteData, ...routeChanges });
|
|
||||||
if (originalStationData) setStationData(originalStationData);
|
|
||||||
if (originalSightData) setSightData(originalSightData);
|
|
||||||
}, [
|
|
||||||
originalRouteData,
|
|
||||||
originalStationData,
|
|
||||||
originalSightData,
|
|
||||||
routeChanges,
|
|
||||||
stationChanges,
|
|
||||||
sightChanges,
|
|
||||||
]);
|
|
||||||
|
|
||||||
function setScaleRange(min: number, max: number) {
|
|
||||||
setRouteChanges((prev) => {
|
|
||||||
return { ...prev, scale_min: min, scale_max: max };
|
|
||||||
});
|
});
|
||||||
}
|
const [sightData, setSightData] = useState<SightData[]>();
|
||||||
|
|
||||||
function setMapRotation(rotation: number) {
|
const [routeChanges, setRouteChanges] = useState<Partial<RouteData>>({});
|
||||||
setRouteChanges((prev) => {
|
const [stationChanges, setStationChanges] = useState<StationPatchData[]>(
|
||||||
return { ...prev, rotate: rotation };
|
[]
|
||||||
});
|
);
|
||||||
}
|
const [sightChanges, setSightChanges] = useState<SightPatchData[]>([]);
|
||||||
|
const { language } = languageStore;
|
||||||
|
|
||||||
function setMapCenter(x: number, y: number) {
|
const [isRouteLoading, setIsRouteLoading] = useState(true);
|
||||||
setRouteChanges((prev) => {
|
const [isStationLoading, setIsStationLoading] = useState(true);
|
||||||
return { ...prev, center_latitude: x, center_longitude: y };
|
const [isSightLoading, setIsSightLoading] = useState(true);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveChanges() {
|
useEffect(() => {
|
||||||
await axiosInstance.patch(`/route/${routeId}`, routeData);
|
const fetchData = async () => {
|
||||||
await saveStationChanges();
|
try {
|
||||||
await saveSightChanges();
|
setIsRouteLoading(true);
|
||||||
}
|
setIsStationLoading(true);
|
||||||
|
setIsSightLoading(true);
|
||||||
|
|
||||||
async function saveStationChanges() {
|
const [
|
||||||
for (const station of stationChanges) {
|
routeResponse,
|
||||||
const response = await axiosInstance.patch(
|
ruStationResponse,
|
||||||
`/route/${routeId}/station`,
|
enStationResponse,
|
||||||
station
|
zhStationResponse,
|
||||||
);
|
sightResponse,
|
||||||
}
|
] = await Promise.all([
|
||||||
}
|
axiosInstanceForGet(language).get(`/route/${routeId}`),
|
||||||
|
axiosInstanceForGet("ru").get(`/route/${routeId}/station`),
|
||||||
|
axiosInstanceForGet("en").get(`/route/${routeId}/station`),
|
||||||
|
axiosInstanceForGet("zh").get(`/route/${routeId}/station`),
|
||||||
|
axiosInstanceForGet(language).get(`/route/${routeId}/sight`),
|
||||||
|
]);
|
||||||
|
|
||||||
async function saveSightChanges() {
|
setOriginalRouteData(routeResponse.data as RouteData);
|
||||||
console.log("sightChanges", sightChanges);
|
setOriginalStationData(ruStationResponse.data as StationData[]);
|
||||||
for (const sight of sightChanges) {
|
setStationData({
|
||||||
const response = await axiosInstance.patch(
|
ru: ruStationResponse.data as StationData[],
|
||||||
`/route/${routeId}/sight`,
|
en: enStationResponse.data as StationData[],
|
||||||
sight
|
zh: zhStationResponse.data as StationData[],
|
||||||
);
|
});
|
||||||
}
|
setOriginalSightData(sightResponse.data as SightData[]);
|
||||||
}
|
|
||||||
|
|
||||||
function setStationOffset(stationId: number, x: number, y: number) {
|
setIsRouteLoading(false);
|
||||||
setStationChanges((prev) => {
|
setIsStationLoading(false);
|
||||||
let found = prev.find((station) => station.station_id === stationId);
|
setIsSightLoading(false);
|
||||||
if (found) {
|
} catch (error) {
|
||||||
found.offset_x = x;
|
console.error("Error fetching data:", error);
|
||||||
found.offset_y = y;
|
setIsRouteLoading(false);
|
||||||
|
setIsStationLoading(false);
|
||||||
return prev.map((station) => {
|
setIsSightLoading(false);
|
||||||
if (station.station_id === stationId) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
return station;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const foundStation = stationData?.find(
|
|
||||||
(station) => station.id === stationId
|
|
||||||
);
|
|
||||||
if (foundStation) {
|
|
||||||
return [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
station_id: stationId,
|
|
||||||
offset_x: x,
|
|
||||||
offset_y: y,
|
|
||||||
transfers: foundStation.transfers,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
return prev;
|
};
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSightCoordinates(
|
fetchData();
|
||||||
sightId: number,
|
}, [routeId]);
|
||||||
latitude: number,
|
|
||||||
longitude: number
|
|
||||||
) {
|
|
||||||
setSightChanges((prev) => {
|
|
||||||
let found = prev.find((sight) => sight.sight_id === sightId);
|
|
||||||
if (found) {
|
|
||||||
found.latitude = latitude;
|
|
||||||
found.longitude = longitude;
|
|
||||||
|
|
||||||
return prev.map((sight) => {
|
useEffect(() => {
|
||||||
if (sight.sight_id === sightId) {
|
// combine changes with original data
|
||||||
return found;
|
if (originalRouteData)
|
||||||
}
|
setRouteData({ ...originalRouteData, ...routeChanges });
|
||||||
return sight;
|
if (originalSightData) setSightData(originalSightData);
|
||||||
});
|
}, [
|
||||||
} else {
|
|
||||||
const foundSight = sightData?.find((sight) => sight.id === sightId);
|
|
||||||
if (foundSight) {
|
|
||||||
return [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
sight_id: sightId,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("sightChanges", sightChanges);
|
|
||||||
}, [sightChanges]);
|
|
||||||
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({
|
|
||||||
originalRouteData: originalRouteData,
|
|
||||||
originalStationData: originalStationData,
|
|
||||||
originalSightData: originalSightData,
|
|
||||||
routeData: routeData,
|
|
||||||
stationData: stationData,
|
|
||||||
sightData: sightData,
|
|
||||||
isRouteLoading,
|
|
||||||
isStationLoading,
|
|
||||||
isSightLoading,
|
|
||||||
setScaleRange,
|
|
||||||
setMapRotation,
|
|
||||||
setMapCenter,
|
|
||||||
saveChanges,
|
|
||||||
setStationOffset,
|
|
||||||
setSightCoordinates,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
originalRouteData,
|
originalRouteData,
|
||||||
originalStationData,
|
|
||||||
originalSightData,
|
originalSightData,
|
||||||
routeData,
|
routeChanges,
|
||||||
stationData,
|
stationChanges,
|
||||||
sightData,
|
sightChanges,
|
||||||
isRouteLoading,
|
]);
|
||||||
isStationLoading,
|
|
||||||
isSightLoading,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
function setScaleRange(min: number, max: number) {
|
||||||
<MapDataContext.Provider value={value}>{children}</MapDataContext.Provider>
|
setRouteChanges((prev) => {
|
||||||
);
|
return { ...prev, scale_min: min, scale_max: max };
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMapRotation(rotation: number) {
|
||||||
|
setRouteChanges((prev) => {
|
||||||
|
return { ...prev, rotate: rotation };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMapCenter(x: number, y: number) {
|
||||||
|
setRouteChanges((prev) => {
|
||||||
|
return { ...prev, center_latitude: x, center_longitude: y };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
await axiosInstance.patch(`/route/${routeId}`, routeData);
|
||||||
|
await saveStationChanges();
|
||||||
|
await saveSightChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStationChanges() {
|
||||||
|
for (const station of stationChanges) {
|
||||||
|
const response = await axiosInstance.patch(
|
||||||
|
`/route/${routeId}/station`,
|
||||||
|
station
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSightChanges() {
|
||||||
|
console.log("sightChanges", sightChanges);
|
||||||
|
for (const sight of sightChanges) {
|
||||||
|
const response = await axiosInstance.patch(
|
||||||
|
`/route/${routeId}/sight`,
|
||||||
|
sight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStationOffset(stationId: number, x: number, y: number) {
|
||||||
|
setStationChanges((prev) => {
|
||||||
|
let found = prev.find((station) => station.station_id === stationId);
|
||||||
|
if (found) {
|
||||||
|
found.offset_x = x;
|
||||||
|
found.offset_y = y;
|
||||||
|
|
||||||
|
return prev.map((station) => {
|
||||||
|
if (station.station_id === stationId) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return station;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const foundStation = stationData.ru?.find(
|
||||||
|
(station) => station.id === stationId
|
||||||
|
);
|
||||||
|
if (foundStation) {
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
station_id: stationId,
|
||||||
|
offset_x: x,
|
||||||
|
offset_y: y,
|
||||||
|
transfers: foundStation.transfers,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSightCoordinates(
|
||||||
|
sightId: number,
|
||||||
|
latitude: number,
|
||||||
|
longitude: number
|
||||||
|
) {
|
||||||
|
setSightChanges((prev) => {
|
||||||
|
let found = prev.find((sight) => sight.sight_id === sightId);
|
||||||
|
if (found) {
|
||||||
|
found.latitude = latitude;
|
||||||
|
found.longitude = longitude;
|
||||||
|
|
||||||
|
return prev.map((sight) => {
|
||||||
|
if (sight.sight_id === sightId) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return sight;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const foundSight = sightData?.find((sight) => sight.id === sightId);
|
||||||
|
if (foundSight) {
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
sight_id: sightId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("sightChanges", sightChanges);
|
||||||
|
}, [sightChanges]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
originalRouteData,
|
||||||
|
originalStationData,
|
||||||
|
originalSightData,
|
||||||
|
routeData,
|
||||||
|
stationData,
|
||||||
|
sightData,
|
||||||
|
isRouteLoading,
|
||||||
|
isStationLoading,
|
||||||
|
isSightLoading,
|
||||||
|
setScaleRange,
|
||||||
|
setMapRotation,
|
||||||
|
setMapCenter,
|
||||||
|
saveChanges,
|
||||||
|
setStationOffset,
|
||||||
|
setSightCoordinates,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
originalRouteData,
|
||||||
|
originalStationData,
|
||||||
|
originalSightData,
|
||||||
|
routeData,
|
||||||
|
stationData,
|
||||||
|
sightData,
|
||||||
|
isRouteLoading,
|
||||||
|
isStationLoading,
|
||||||
|
isSightLoading,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MapDataContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</MapDataContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const useMapData = () => {
|
export const useMapData = () => {
|
||||||
const context = useContext(MapDataContext);
|
const context = useContext(MapDataContext);
|
||||||
|
@ -5,187 +5,228 @@ import { useTransform } from "./TransformContext";
|
|||||||
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||||||
|
|
||||||
export function RightSidebar() {
|
export function RightSidebar() {
|
||||||
const { routeData, setScaleRange, saveChanges, originalRouteData, setMapRotation, setMapCenter } = useMapData();
|
const {
|
||||||
const { rotation, position, screenToLocal, screenCenter, rotateToAngle, setTransform } = useTransform();
|
routeData,
|
||||||
const [minScale, setMinScale] = useState<number>(1);
|
setScaleRange,
|
||||||
const [maxScale, setMaxScale] = useState<number>(10);
|
saveChanges,
|
||||||
const [localCenter, setLocalCenter] = useState<{x: number, y: number}>({x: 0, y: 0});
|
originalRouteData,
|
||||||
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
setMapRotation,
|
||||||
|
setMapCenter,
|
||||||
|
} = useMapData();
|
||||||
|
const {
|
||||||
|
rotation,
|
||||||
|
position,
|
||||||
|
screenToLocal,
|
||||||
|
screenCenter,
|
||||||
|
rotateToAngle,
|
||||||
|
setTransform,
|
||||||
|
} = useTransform();
|
||||||
|
const [minScale, setMinScale] = useState<number>(1);
|
||||||
|
const [maxScale, setMaxScale] = useState<number>(10);
|
||||||
|
const [localCenter, setLocalCenter] = useState<{ x: number; y: number }>({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(originalRouteData) {
|
if (originalRouteData) {
|
||||||
setMinScale(originalRouteData.scale_min ?? 1);
|
setMinScale(originalRouteData.scale_min ?? 1);
|
||||||
setMaxScale(originalRouteData.scale_max ?? 10);
|
setMaxScale(originalRouteData.scale_max ?? 10);
|
||||||
setRotationDegrees(originalRouteData.rotate ?? 0);
|
setRotationDegrees(originalRouteData.rotate ?? 0);
|
||||||
setLocalCenter({x: originalRouteData.center_latitude ?? 0, y: originalRouteData.center_longitude ?? 0});
|
setLocalCenter({
|
||||||
}
|
x: originalRouteData.center_latitude ?? 0,
|
||||||
}, [originalRouteData]);
|
y: originalRouteData.center_longitude ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [originalRouteData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(minScale && maxScale) {
|
if (minScale && maxScale) {
|
||||||
setScaleRange(minScale, maxScale);
|
setScaleRange(minScale, maxScale);
|
||||||
}
|
}
|
||||||
}, [minScale, maxScale]);
|
}, [minScale, maxScale]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRotationDegrees(
|
||||||
|
((Math.round((rotation * 180) / Math.PI) % 360) + 360) % 360
|
||||||
|
);
|
||||||
|
}, [rotation]);
|
||||||
|
useEffect(() => {
|
||||||
|
setMapRotation(rotationDegrees);
|
||||||
|
}, [rotationDegrees]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRotationDegrees((Math.round(rotation * 180 / Math.PI) % 360 + 360) % 360);
|
const center = screenCenter ?? { x: 0, y: 0 };
|
||||||
}, [rotation]);
|
const localCenter = screenToLocal(center.x, center.y);
|
||||||
useEffect(() => {
|
const coordinates = localToCoordinates(localCenter.x, localCenter.y);
|
||||||
setMapRotation(rotationDegrees);
|
setLocalCenter({ x: coordinates.latitude, y: coordinates.longitude });
|
||||||
}, [rotationDegrees]);
|
}, [position]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const center = screenCenter ?? {x: 0, y: 0};
|
setMapCenter(localCenter.x, localCenter.y);
|
||||||
const localCenter = screenToLocal(center.x, center.y);
|
}, [localCenter]);
|
||||||
const coordinates = localToCoordinates(localCenter.x, localCenter.y);
|
|
||||||
setLocalCenter({x: coordinates.latitude, y: coordinates.longitude});
|
|
||||||
}, [position]);
|
|
||||||
|
|
||||||
|
function setRotationFromDegrees(degrees: number) {
|
||||||
|
rotateToAngle((degrees * Math.PI) / 180);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
function pan({ x, y }: { x: number; y: number }) {
|
||||||
setMapCenter(localCenter.x, localCenter.y);
|
const coordinates = coordinatesToLocal(x, y);
|
||||||
}, [localCenter]);
|
setTransform(coordinates.x, coordinates.y);
|
||||||
|
}
|
||||||
|
|
||||||
function setRotationFromDegrees(degrees: number) {
|
if (!routeData) {
|
||||||
rotateToAngle(degrees * Math.PI / 180);
|
console.error("routeData is null");
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function pan({x, y}: {x: number, y: number}) {
|
return (
|
||||||
const coordinates = coordinatesToLocal(x,y);
|
<Stack
|
||||||
setTransform(coordinates.x, coordinates.y);
|
position="absolute"
|
||||||
}
|
right={8}
|
||||||
|
top={8}
|
||||||
|
bottom={8}
|
||||||
|
p={2}
|
||||||
|
gap={1}
|
||||||
|
minWidth="400px"
|
||||||
|
bgcolor="primary.main"
|
||||||
|
border="1px solid #e0e0e0"
|
||||||
|
borderRadius={2}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||||
|
Детали о достопримечательностях
|
||||||
|
</Typography>
|
||||||
|
|
||||||
if(!routeData) {
|
<Stack spacing={2} direction="row" alignItems="center">
|
||||||
console.error("routeData is null");
|
<TextField
|
||||||
return null;
|
type="number"
|
||||||
}
|
label="Минимальный масштаб"
|
||||||
|
variant="filled"
|
||||||
|
value={minScale}
|
||||||
|
onChange={(e) => setMinScale(Number(e.target.value))}
|
||||||
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
min: 0.1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Максимальный масштаб"
|
||||||
|
variant="filled"
|
||||||
|
value={maxScale}
|
||||||
|
onChange={(e) => setMaxScale(Number(e.target.value))}
|
||||||
|
style={{ backgroundColor: "#222", borderRadius: 4, color: "#fff" }}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
min: 0.1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
return (
|
<TextField
|
||||||
<Stack
|
type="number"
|
||||||
position="absolute" right={8} top={8} bottom={8} p={2}
|
label="Поворот (в градусах)"
|
||||||
gap={1}
|
variant="filled"
|
||||||
minWidth="400px" bgcolor="primary.main"
|
value={rotationDegrees}
|
||||||
border="1px solid #e0e0e0" borderRadius={2}
|
onChange={(e) => {
|
||||||
>
|
const value = Number(e.target.value);
|
||||||
<Typography variant="h6" sx={{ mb: 2 }} textAlign="center">
|
if (!isNaN(value)) {
|
||||||
Детали о достопримечательностях
|
setRotationFromDegrees(value);
|
||||||
</Typography>
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
min: 0,
|
||||||
|
max: 360,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Stack spacing={2} direction="row" alignItems="center">
|
<Stack direction="row" spacing={2}>
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
label="Минимальный масштаб"
|
label="Центр карты, широта"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
value={minScale}
|
value={Math.round(localCenter.x * 100000) / 100000}
|
||||||
onChange={(e) => setMinScale(Number(e.target.value))}
|
onChange={(e) => {
|
||||||
style={{backgroundColor: "#222", borderRadius: 4}}
|
setLocalCenter((prev) => ({ ...prev, x: Number(e.target.value) }));
|
||||||
sx={{
|
pan({ x: Number(e.target.value), y: localCenter.y });
|
||||||
'& .MuiInputLabel-root.Mui-focused': {
|
}}
|
||||||
color: "#fff"
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
}
|
sx={{
|
||||||
}}
|
"& .MuiInputLabel-root": {
|
||||||
slotProps={{
|
color: "#fff",
|
||||||
input: {
|
},
|
||||||
min: 0.1
|
"& .MuiInputBase-input": {
|
||||||
}
|
color: "#fff",
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
<TextField
|
/>
|
||||||
type="number"
|
<TextField
|
||||||
label="Максимальный масштаб"
|
type="number"
|
||||||
variant="filled"
|
label="Центр карты, высота"
|
||||||
value={maxScale}
|
variant="filled"
|
||||||
onChange={(e) => setMaxScale(Number(e.target.value))}
|
value={Math.round(localCenter.y * 100000) / 100000}
|
||||||
style={{backgroundColor: "#222", borderRadius: 4}}
|
onChange={(e) => {
|
||||||
sx={{
|
setLocalCenter((prev) => ({ ...prev, y: Number(e.target.value) }));
|
||||||
'& .MuiInputLabel-root.Mui-focused': {
|
pan({ x: localCenter.x, y: Number(e.target.value) });
|
||||||
color: "#fff"
|
}}
|
||||||
}
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
}}
|
sx={{
|
||||||
slotProps={{
|
"& .MuiInputLabel-root": {
|
||||||
input: {
|
color: "#fff",
|
||||||
min: 0.1
|
},
|
||||||
}
|
"& .MuiInputBase-input": {
|
||||||
}}
|
color: "#fff",
|
||||||
/>
|
},
|
||||||
</Stack>
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<TextField
|
<Button
|
||||||
type="number"
|
variant="contained"
|
||||||
label="Поворот (в градусах)"
|
color="secondary"
|
||||||
variant="filled"
|
sx={{ mt: 2 }}
|
||||||
value={rotationDegrees}
|
onClick={() => {
|
||||||
onChange={(e) => {
|
saveChanges();
|
||||||
const value = Number(e.target.value);
|
}}
|
||||||
if (!isNaN(value)) {
|
>
|
||||||
setRotationFromDegrees(value);
|
Сохранить изменения
|
||||||
}
|
</Button>
|
||||||
}}
|
</Stack>
|
||||||
onKeyDown={(e) => {
|
);
|
||||||
if (e.key === 'Enter') {
|
}
|
||||||
e.currentTarget.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{backgroundColor: "#222", borderRadius: 4}}
|
|
||||||
sx={{
|
|
||||||
'& .MuiInputLabel-root.Mui-focused': {
|
|
||||||
color: "#fff"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
slotProps={{
|
|
||||||
input: {
|
|
||||||
min: 0,
|
|
||||||
max: 360
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack direction="row" spacing={2}>
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
label="Центр карты, широта"
|
|
||||||
variant="filled"
|
|
||||||
value={Math.round(localCenter.x*100000)/100000}
|
|
||||||
onChange={(e) => {
|
|
||||||
setLocalCenter(prev => ({...prev, x: Number(e.target.value)}))
|
|
||||||
pan({x: Number(e.target.value), y: localCenter.y});
|
|
||||||
}}
|
|
||||||
style={{backgroundColor: "#222", borderRadius: 4}}
|
|
||||||
sx={{
|
|
||||||
'& .MuiInputLabel-root.Mui-focused': {
|
|
||||||
color: "#fff"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
label="Центр карты, высота"
|
|
||||||
variant="filled"
|
|
||||||
value={Math.round(localCenter.y*100000)/100000}
|
|
||||||
onChange={(e) => {
|
|
||||||
setLocalCenter(prev => ({...prev, y: Number(e.target.value)}))
|
|
||||||
pan({x: localCenter.x, y: Number(e.target.value)});
|
|
||||||
}}
|
|
||||||
style={{backgroundColor: "#222", borderRadius: 4}}
|
|
||||||
sx={{
|
|
||||||
'& .MuiInputLabel-root.Mui-focused': {
|
|
||||||
color: "#fff"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
sx={{ mt: 2 }}
|
|
||||||
onClick={() => {
|
|
||||||
saveChanges();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Сохранить изменения
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -1,109 +1,148 @@
|
|||||||
import { FederatedMouseEvent, Graphics } from "pixi.js";
|
import { FederatedMouseEvent, Graphics } from "pixi.js";
|
||||||
import { BACKGROUND_COLOR, PATH_COLOR, STATION_RADIUS, STATION_OUTLINE_WIDTH, UP_SCALE } from "./Constants";
|
import {
|
||||||
|
BACKGROUND_COLOR,
|
||||||
|
PATH_COLOR,
|
||||||
|
STATION_RADIUS,
|
||||||
|
STATION_OUTLINE_WIDTH,
|
||||||
|
UP_SCALE,
|
||||||
|
} from "./Constants";
|
||||||
import { useTransform } from "./TransformContext";
|
import { useTransform } from "./TransformContext";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { StationData } from "./types";
|
import { StationData } from "./types";
|
||||||
import { useMapData } from "./MapDataContext";
|
import { useMapData } from "./MapDataContext";
|
||||||
import { coordinatesToLocal } from "./utils";
|
import { coordinatesToLocal } from "./utils";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { languageStore } from "@stores";
|
||||||
|
|
||||||
interface StationProps {
|
interface StationProps {
|
||||||
station: StationData;
|
station: StationData;
|
||||||
|
ruLabel: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Station({
|
export const Station = observer(
|
||||||
station
|
({ station, ruLabel }: Readonly<StationProps>) => {
|
||||||
}: Readonly<StationProps>) {
|
const draw = useCallback((g: Graphics) => {
|
||||||
const draw = useCallback((g: Graphics) => {
|
g.clear();
|
||||||
g.clear();
|
const coordinates = coordinatesToLocal(
|
||||||
const coordinates = coordinatesToLocal(station.latitude, station.longitude);
|
station.latitude,
|
||||||
g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, STATION_RADIUS);
|
station.longitude
|
||||||
g.fill({color: PATH_COLOR});
|
);
|
||||||
g.stroke({color: BACKGROUND_COLOR, width: STATION_OUTLINE_WIDTH});
|
g.circle(
|
||||||
}, []);
|
coordinates.x * UP_SCALE,
|
||||||
|
coordinates.y * UP_SCALE,
|
||||||
|
STATION_RADIUS
|
||||||
|
);
|
||||||
|
g.fill({ color: PATH_COLOR });
|
||||||
|
g.stroke({ color: BACKGROUND_COLOR, width: STATION_OUTLINE_WIDTH });
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<pixiContainer>
|
<pixiContainer>
|
||||||
<pixiGraphics draw={draw}/>
|
<pixiGraphics draw={draw} />
|
||||||
<StationLabel station={station}/>
|
<StationLabel station={station} ruLabel={ruLabel} />
|
||||||
</pixiContainer>
|
</pixiContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export function StationLabel({
|
export const StationLabel = observer(
|
||||||
station
|
({ station, ruLabel }: Readonly<StationProps>) => {
|
||||||
}: Readonly<StationProps>) {
|
const { language } = languageStore;
|
||||||
const { rotation, scale } = useTransform();
|
const { rotation, scale } = useTransform();
|
||||||
const { setStationOffset } = useMapData();
|
const { setStationOffset } = useMapData();
|
||||||
|
|
||||||
const [position, setPosition] = useState({ x: station.offset_x, y: station.offset_y });
|
const [position, setPosition] = useState({
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
x: station.offset_x,
|
||||||
|
y: station.offset_y,
|
||||||
|
});
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||||
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
|
const [startMousePosition, setStartMousePosition] = useState({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
if(!station) {
|
if (!station) {
|
||||||
console.error("station is null");
|
console.error("station is null");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
setStartPosition({
|
setStartPosition({
|
||||||
x: position.x,
|
x: position.x,
|
||||||
y: position.y
|
y: position.y,
|
||||||
});
|
});
|
||||||
setStartMousePosition({
|
setStartMousePosition({
|
||||||
x: e.globalX,
|
x: e.globalX,
|
||||||
y: e.globalY
|
y: e.globalY,
|
||||||
});
|
});
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
const dx = (e.globalX - startMousePosition.x);
|
const dx = e.globalX - startMousePosition.x;
|
||||||
const dy = (e.globalY - startMousePosition.y);
|
const dy = e.globalY - startMousePosition.y;
|
||||||
const newPosition = {
|
const newPosition = {
|
||||||
x: startPosition.x + dx,
|
x: startPosition.x + dx,
|
||||||
y: startPosition.y + dy
|
y: startPosition.y + dy,
|
||||||
};
|
};
|
||||||
setPosition(newPosition);
|
setPosition(newPosition);
|
||||||
setStationOffset(station.id, newPosition.x, newPosition.y);
|
setStationOffset(station.id, newPosition.x, newPosition.y);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
const coordinates = coordinatesToLocal(station.latitude, station.longitude);
|
const coordinates = coordinatesToLocal(station.latitude, station.longitude);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<pixiContainer
|
<pixiContainer
|
||||||
eventMode='static'
|
eventMode="static"
|
||||||
interactive
|
interactive
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onGlobalPointerMove={handlePointerMove}
|
onGlobalPointerMove={handlePointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
onPointerUpOutside={handlePointerUp}
|
onPointerUpOutside={handlePointerUp}
|
||||||
width={48}
|
width={48}
|
||||||
height={48}
|
height={48}
|
||||||
x={coordinates.x * UP_SCALE}
|
x={coordinates.x * UP_SCALE}
|
||||||
y={coordinates.y * UP_SCALE}
|
y={coordinates.y * UP_SCALE}
|
||||||
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={{
|
||||||
fontSize: 48,
|
fontSize: 26,
|
||||||
fontWeight: 'bold',
|
fontWeight: "bold",
|
||||||
fill: "#ffffff"
|
fill: "#ffffff",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</pixiContainer>
|
|
||||||
);
|
{ruLabel && (
|
||||||
}
|
<pixiText
|
||||||
|
anchor={{ x: 1, y: -1 }}
|
||||||
|
text={ruLabel}
|
||||||
|
position={{
|
||||||
|
x: position.x / scale + 24,
|
||||||
|
y: position.y / scale,
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "bold",
|
||||||
|
fill: "#CCCCCC",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</pixiContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -1,150 +1,204 @@
|
|||||||
import { createContext, ReactNode, useContext, useMemo, useState } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
} from "react";
|
||||||
import { SCALE_FACTOR, UP_SCALE } from "./Constants";
|
import { SCALE_FACTOR, UP_SCALE } from "./Constants";
|
||||||
|
|
||||||
|
|
||||||
const TransformContext = createContext<{
|
const TransformContext = createContext<{
|
||||||
position: { x: number, y: number },
|
position: { x: number; y: number };
|
||||||
scale: number,
|
scale: number;
|
||||||
rotation: number,
|
rotation: number;
|
||||||
screenCenter?: { x: number, y: number },
|
screenCenter?: { x: number; y: number };
|
||||||
|
|
||||||
setPosition: React.Dispatch<React.SetStateAction<{ x: number, y: number }>>,
|
setPosition: React.Dispatch<React.SetStateAction<{ x: number; y: number }>>;
|
||||||
setScale: React.Dispatch<React.SetStateAction<number>>,
|
setScale: React.Dispatch<React.SetStateAction<number>>;
|
||||||
setRotation: React.Dispatch<React.SetStateAction<number>>,
|
setRotation: React.Dispatch<React.SetStateAction<number>>;
|
||||||
screenToLocal: (x: number, y: number) => { x: number, y: number },
|
screenToLocal: (x: number, y: number) => { x: number; y: number };
|
||||||
localToScreen: (x: number, y: number) => { x: number, y: number },
|
localToScreen: (x: number, y: number) => { x: number; y: number };
|
||||||
rotateToAngle: (to: number, fromPosition?: {x: number, y: number}) => void,
|
rotateToAngle: (to: number, fromPosition?: { x: number; y: number }) => void;
|
||||||
setTransform: (latitude: number, longitude: number, rotationDegrees?: number, scale?: number) => void,
|
setTransform: (
|
||||||
setScreenCenter: React.Dispatch<React.SetStateAction<{ x: number, y: number } | undefined>>
|
latitude: number,
|
||||||
|
longitude: number,
|
||||||
|
rotationDegrees?: number,
|
||||||
|
scale?: number
|
||||||
|
) => void;
|
||||||
|
setScreenCenter: React.Dispatch<
|
||||||
|
React.SetStateAction<{ x: number; y: number } | undefined>
|
||||||
|
>;
|
||||||
}>({
|
}>({
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
scale: 1,
|
scale: 1,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
screenCenter: undefined,
|
screenCenter: undefined,
|
||||||
setPosition: () => {},
|
setPosition: () => {},
|
||||||
setScale: () => {},
|
setScale: () => {},
|
||||||
setRotation: () => {},
|
setRotation: () => {},
|
||||||
screenToLocal: () => ({ x: 0, y: 0 }),
|
screenToLocal: () => ({ x: 0, y: 0 }),
|
||||||
localToScreen: () => ({ x: 0, y: 0 }),
|
localToScreen: () => ({ x: 0, y: 0 }),
|
||||||
rotateToAngle: () => {},
|
rotateToAngle: () => {},
|
||||||
setTransform: () => {},
|
setTransform: () => {},
|
||||||
setScreenCenter: () => {}
|
setScreenCenter: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provider component
|
// Provider component
|
||||||
export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [scale, setScale] = useState(1);
|
const [scale, setScale] = useState(1);
|
||||||
const [rotation, setRotation] = useState(0);
|
const [rotation, setRotation] = useState(0);
|
||||||
const [screenCenter, setScreenCenter] = useState<{x: number, y: number}>();
|
const [screenCenter, setScreenCenter] = useState<{ x: number; y: number }>();
|
||||||
|
|
||||||
function screenToLocal(screenX: number, screenY: number) {
|
const screenToLocal = useCallback(
|
||||||
// Translate point relative to current pan position
|
(screenX: number, screenY: number) => {
|
||||||
const translatedX = (screenX - position.x) / scale;
|
// Translate point relative to current pan position
|
||||||
const translatedY = (screenY - position.y) / scale;
|
const translatedX = (screenX - position.x) / scale;
|
||||||
|
const translatedY = (screenY - position.y) / scale;
|
||||||
|
|
||||||
// Rotate point around center
|
// Rotate point around center
|
||||||
const cosRotation = Math.cos(-rotation); // Negative rotation to reverse transform
|
const cosRotation = Math.cos(-rotation); // Negative rotation to reverse transform
|
||||||
const sinRotation = Math.sin(-rotation);
|
const sinRotation = Math.sin(-rotation);
|
||||||
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
|
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
|
||||||
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
|
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: rotatedX / UP_SCALE,
|
x: rotatedX / UP_SCALE,
|
||||||
y: rotatedY / UP_SCALE
|
y: rotatedY / UP_SCALE,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
[position.x, position.y, scale, rotation]
|
||||||
|
);
|
||||||
|
|
||||||
// Inverse of screenToLocal
|
// Inverse of screenToLocal
|
||||||
function localToScreen(localX: number, localY: number) {
|
const localToScreen = useCallback(
|
||||||
|
(localX: number, localY: number) => {
|
||||||
|
const upscaledX = localX * UP_SCALE;
|
||||||
|
const upscaledY = localY * UP_SCALE;
|
||||||
|
|
||||||
const upscaledX = localX * UP_SCALE;
|
const cosRotation = Math.cos(rotation);
|
||||||
const upscaledY = localY * UP_SCALE;
|
const sinRotation = Math.sin(rotation);
|
||||||
|
const rotatedX = upscaledX * cosRotation - upscaledY * sinRotation;
|
||||||
|
const rotatedY = upscaledX * sinRotation + upscaledY * cosRotation;
|
||||||
|
|
||||||
const cosRotation = Math.cos(rotation);
|
const translatedX = rotatedX * scale + position.x;
|
||||||
const sinRotation = Math.sin(rotation);
|
const translatedY = rotatedY * scale + position.y;
|
||||||
const rotatedX = upscaledX * cosRotation - upscaledY * sinRotation;
|
|
||||||
const rotatedY = upscaledX * sinRotation + upscaledY * cosRotation;
|
|
||||||
|
|
||||||
const translatedX = rotatedX*scale + position.x;
|
return {
|
||||||
const translatedY = rotatedY*scale + position.y;
|
x: translatedX,
|
||||||
|
y: translatedY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[position.x, position.y, scale, rotation]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
const rotateToAngle = useCallback(
|
||||||
x: translatedX,
|
(to: number, fromPosition?: { x: number; y: number }) => {
|
||||||
y: translatedY
|
const rotationDiff = to - rotation;
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
const center = screenCenter ?? { x: 0, y: 0 };
|
||||||
|
const cosDelta = Math.cos(rotationDiff);
|
||||||
|
const sinDelta = Math.sin(rotationDiff);
|
||||||
|
|
||||||
function rotateToAngle(to: number, fromPosition?: {x: number, y: number}) {
|
const currentFromPosition = fromPosition ?? position;
|
||||||
setRotation(to);
|
|
||||||
const rotationDiff = to - rotation;
|
|
||||||
|
|
||||||
const center = screenCenter ?? {x: 0, y: 0};
|
|
||||||
const cosDelta = Math.cos(rotationDiff);
|
|
||||||
const sinDelta = Math.sin(rotationDiff);
|
|
||||||
|
|
||||||
fromPosition ??= position;
|
const newPosition = {
|
||||||
|
x:
|
||||||
|
center.x * (1 - cosDelta) +
|
||||||
|
currentFromPosition.x * cosDelta +
|
||||||
|
(center.y - currentFromPosition.y) * sinDelta,
|
||||||
|
y:
|
||||||
|
center.y * (1 - cosDelta) +
|
||||||
|
currentFromPosition.y * cosDelta +
|
||||||
|
(currentFromPosition.x - center.x) * sinDelta,
|
||||||
|
};
|
||||||
|
|
||||||
setPosition({
|
// Update both rotation and position in a single batch to avoid stale closure
|
||||||
x: center.x * (1 - cosDelta) + fromPosition.x * cosDelta + (center.y - fromPosition.y) * sinDelta,
|
setRotation(to);
|
||||||
y: center.y * (1 - cosDelta) + fromPosition.y * cosDelta + (fromPosition.x - center.x) * sinDelta
|
setPosition(newPosition);
|
||||||
});
|
},
|
||||||
}
|
[rotation, position, screenCenter]
|
||||||
|
);
|
||||||
|
|
||||||
function setTransform(latitude: number, longitude: number, rotationDegrees?: number, useScale ?: number) {
|
const setTransform = useCallback(
|
||||||
const selectedRotation = rotationDegrees ? (rotationDegrees * Math.PI / 180) : rotation;
|
(
|
||||||
const selectedScale = useScale ? useScale/SCALE_FACTOR : scale;
|
latitude: number,
|
||||||
const center = screenCenter ?? {x: 0, y: 0};
|
longitude: number,
|
||||||
console.log("center", center.x, center.y);
|
rotationDegrees?: number,
|
||||||
const newPosition = {
|
useScale?: number
|
||||||
x: -latitude * UP_SCALE * selectedScale,
|
) => {
|
||||||
y: -longitude * UP_SCALE * selectedScale
|
const selectedRotation =
|
||||||
};
|
rotationDegrees !== undefined
|
||||||
|
? (rotationDegrees * Math.PI) / 180
|
||||||
|
: rotation;
|
||||||
|
const selectedScale =
|
||||||
|
useScale !== undefined ? useScale / SCALE_FACTOR : scale;
|
||||||
|
const center = screenCenter ?? { x: 0, y: 0 };
|
||||||
|
|
||||||
const cos = Math.cos(selectedRotation);
|
console.log("center", center.x, center.y);
|
||||||
const sin = Math.sin(selectedRotation);
|
|
||||||
|
|
||||||
// Translate point relative to center, rotate, then translate back
|
|
||||||
const dx = newPosition.x;
|
|
||||||
const dy = newPosition.y;
|
|
||||||
newPosition.x = (dx * cos - dy * sin) + center.x;
|
|
||||||
newPosition.y = (dx * sin + dy * cos) + center.y;
|
|
||||||
|
|
||||||
|
const newPosition = {
|
||||||
setPosition(newPosition);
|
x: -latitude * UP_SCALE * selectedScale,
|
||||||
setRotation(selectedRotation);
|
y: -longitude * UP_SCALE * selectedScale,
|
||||||
setScale(selectedScale);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const cosRot = Math.cos(selectedRotation);
|
||||||
position,
|
const sinRot = Math.sin(selectedRotation);
|
||||||
scale,
|
|
||||||
rotation,
|
|
||||||
screenCenter,
|
|
||||||
setPosition,
|
|
||||||
setScale,
|
|
||||||
setRotation,
|
|
||||||
rotateToAngle,
|
|
||||||
screenToLocal,
|
|
||||||
localToScreen,
|
|
||||||
setTransform,
|
|
||||||
setScreenCenter
|
|
||||||
}), [position, scale, rotation, screenCenter]);
|
|
||||||
|
|
||||||
return (
|
// Translate point relative to center, rotate, then translate back
|
||||||
<TransformContext.Provider value={value}>
|
const dx = newPosition.x;
|
||||||
{children}
|
const dy = newPosition.y;
|
||||||
</TransformContext.Provider>
|
newPosition.x = dx * cosRot - dy * sinRot + center.x;
|
||||||
);
|
newPosition.y = dx * sinRot + dy * cosRot + center.y;
|
||||||
|
|
||||||
|
// Batch state updates to avoid intermediate renders
|
||||||
|
setPosition(newPosition);
|
||||||
|
setRotation(selectedRotation);
|
||||||
|
setScale(selectedScale);
|
||||||
|
},
|
||||||
|
[rotation, scale, screenCenter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
position,
|
||||||
|
scale,
|
||||||
|
rotation,
|
||||||
|
screenCenter,
|
||||||
|
setPosition,
|
||||||
|
setScale,
|
||||||
|
setRotation,
|
||||||
|
rotateToAngle,
|
||||||
|
screenToLocal,
|
||||||
|
localToScreen,
|
||||||
|
setTransform,
|
||||||
|
setScreenCenter,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
position,
|
||||||
|
scale,
|
||||||
|
rotation,
|
||||||
|
screenCenter,
|
||||||
|
rotateToAngle,
|
||||||
|
screenToLocal,
|
||||||
|
localToScreen,
|
||||||
|
setTransform,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransformContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</TransformContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom hook for easy access to transform values
|
// Custom hook for easy access to transform values
|
||||||
export const useTransform = () => {
|
export const useTransform = () => {
|
||||||
const context = useContext(TransformContext);
|
const context = useContext(TransformContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useTransform must be used within a TransformProvider');
|
throw new Error("useTransform must be used within a TransformProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
@ -3,37 +3,32 @@ import { useCallback } from "react";
|
|||||||
import { PATH_COLOR, PATH_WIDTH } from "./Constants";
|
import { PATH_COLOR, PATH_WIDTH } from "./Constants";
|
||||||
import { coordinatesToLocal } from "./utils";
|
import { coordinatesToLocal } from "./utils";
|
||||||
|
|
||||||
|
|
||||||
interface TravelPathProps {
|
interface TravelPathProps {
|
||||||
points: {x: number, y: number}[];
|
points: { x: number; y: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TravelPath({
|
export function TravelPath({ points }: Readonly<TravelPathProps>) {
|
||||||
points
|
const draw = useCallback(
|
||||||
}: Readonly<TravelPathProps>) {
|
(g: Graphics) => {
|
||||||
|
g.clear();
|
||||||
|
const coordStart = coordinatesToLocal(points[0].x, points[0].y);
|
||||||
|
g.moveTo(coordStart.x, coordStart.y);
|
||||||
|
for (let i = 1; i <= points.length - 1; i++) {
|
||||||
|
const coordinates = coordinatesToLocal(points[i].x, points[i].y);
|
||||||
|
g.lineTo(coordinates.x, coordinates.y);
|
||||||
|
}
|
||||||
|
g.stroke({
|
||||||
|
color: PATH_COLOR,
|
||||||
|
width: PATH_WIDTH,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[points]
|
||||||
|
);
|
||||||
|
|
||||||
const draw = useCallback((g: Graphics) => {
|
if (points.length === 0) {
|
||||||
g.clear();
|
console.error("points is empty");
|
||||||
const coordStart = coordinatesToLocal(points[0].x, points[0].y);
|
return null;
|
||||||
g.moveTo(coordStart.x, coordStart.y);
|
}
|
||||||
for (let i = 1; i < points.length - 1; i++) {
|
|
||||||
const coordinates = coordinatesToLocal(points[i].x, points[i].y);
|
|
||||||
g.lineTo(coordinates.x, coordinates.y);
|
|
||||||
}
|
|
||||||
g.stroke({
|
|
||||||
color: PATH_COLOR,
|
|
||||||
width: PATH_WIDTH
|
|
||||||
});
|
|
||||||
}, [points]);
|
|
||||||
|
|
||||||
if(points.length === 0) {
|
return <pixiGraphics draw={draw} />;
|
||||||
console.error("points is empty");
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<pixiGraphics
|
|
||||||
draw={draw}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -1,31 +1,43 @@
|
|||||||
import { Stack, Typography } from "@mui/material";
|
import { Stack, Typography } from "@mui/material";
|
||||||
|
|
||||||
export function Widgets() {
|
export function Widgets() {
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction="column" spacing={2}
|
direction="column"
|
||||||
position="absolute"
|
spacing={2}
|
||||||
top={32} left={32}
|
position="absolute"
|
||||||
sx={{ pointerEvents: 'none' }}
|
top={32}
|
||||||
>
|
left={32}
|
||||||
<Stack bgcolor="primary.main"
|
sx={{ pointerEvents: "none" }}
|
||||||
width={361} height={96}
|
>
|
||||||
p={2} m={2}
|
<Stack
|
||||||
borderRadius={2}
|
bgcolor="primary.main"
|
||||||
alignItems="center"
|
width={361}
|
||||||
justifyContent="center"
|
height={96}
|
||||||
>
|
p={2}
|
||||||
<Typography variant="h6">Станция</Typography>
|
m={2}
|
||||||
</Stack>
|
borderRadius={2}
|
||||||
<Stack bgcolor="primary.main"
|
alignItems="center"
|
||||||
width={223} height={262}
|
justifyContent="center"
|
||||||
p={2} m={2}
|
>
|
||||||
borderRadius={2}
|
<Typography variant="h6" sx={{ color: "#fff" }}>
|
||||||
alignItems="center"
|
Станция
|
||||||
justifyContent="center"
|
</Typography>
|
||||||
>
|
</Stack>
|
||||||
<Typography variant="h6">Погода</Typography>
|
<Stack
|
||||||
</Stack>
|
bgcolor="primary.main"
|
||||||
</Stack>
|
width={223}
|
||||||
)
|
height={262}
|
||||||
|
p={2}
|
||||||
|
m={2}
|
||||||
|
borderRadius={2}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ color: "#fff" }}>
|
||||||
|
Погода
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import { useRef, useEffect, useState } from "react";
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Application, ApplicationRef, extend } from "@pixi/react";
|
||||||
import {
|
import {
|
||||||
Application,
|
Container,
|
||||||
ApplicationRef,
|
Graphics,
|
||||||
extend
|
Sprite,
|
||||||
} from '@pixi/react';
|
Texture,
|
||||||
import {
|
TilingSprite,
|
||||||
Container,
|
Text,
|
||||||
Graphics,
|
} from "pixi.js";
|
||||||
Sprite,
|
|
||||||
Texture,
|
|
||||||
TilingSprite,
|
|
||||||
Text
|
|
||||||
} from 'pixi.js';
|
|
||||||
import { Stack } from "@mui/material";
|
import { Stack } from "@mui/material";
|
||||||
import { MapDataProvider, useMapData } from "./MapDataContext";
|
import { MapDataProvider, useMapData } from "./MapDataContext";
|
||||||
import { TransformProvider, useTransform } from "./TransformContext";
|
import { TransformProvider, useTransform } from "./TransformContext";
|
||||||
@ -25,128 +21,156 @@ import { LeftSidebar } from "./LeftSidebar";
|
|||||||
import { RightSidebar } from "./RightSidebar";
|
import { RightSidebar } from "./RightSidebar";
|
||||||
import { Widgets } from "./Widgets";
|
import { Widgets } from "./Widgets";
|
||||||
import { coordinatesToLocal } from "./utils";
|
import { coordinatesToLocal } from "./utils";
|
||||||
|
import { LanguageSwitch } from "@/components/LanguageSwitch";
|
||||||
|
import { languageStore } from "@stores";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
extend({
|
extend({
|
||||||
Container,
|
Container,
|
||||||
Graphics,
|
Graphics,
|
||||||
Sprite,
|
Sprite,
|
||||||
Texture,
|
Texture,
|
||||||
TilingSprite,
|
TilingSprite,
|
||||||
Text
|
Text,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RoutePreview = () => {
|
export const RoutePreview = () => {
|
||||||
return (
|
return (
|
||||||
<MapDataProvider>
|
<MapDataProvider>
|
||||||
<TransformProvider>
|
<TransformProvider>
|
||||||
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
||||||
<LeftSidebar />
|
<div
|
||||||
<Stack direction="row" flex={1} position="relative" height="100%">
|
style={{
|
||||||
<Widgets />
|
position: "absolute",
|
||||||
<RouteMap />
|
top: 0,
|
||||||
<RightSidebar />
|
left: "50%",
|
||||||
</Stack>
|
transform: "translateX(-50%)",
|
||||||
|
zIndex: 1000,
|
||||||
</Stack>
|
}}
|
||||||
</TransformProvider>
|
>
|
||||||
</MapDataProvider>
|
<LanguageSwitch />
|
||||||
);
|
</div>
|
||||||
|
<LeftSidebar />
|
||||||
|
<Stack direction="row" flex={1} position="relative" height="100%">
|
||||||
|
<Widgets />
|
||||||
|
<RouteMap />
|
||||||
|
<RightSidebar />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</TransformProvider>
|
||||||
|
</MapDataProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const RouteMap = observer(() => {
|
||||||
|
const { language } = languageStore;
|
||||||
|
const { setPosition, screenToLocal, setTransform, screenCenter } =
|
||||||
|
useTransform();
|
||||||
|
const { routeData, stationData, sightData, originalRouteData } = useMapData();
|
||||||
|
console.log(stationData);
|
||||||
|
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
||||||
|
const [isSetup, setIsSetup] = useState(false);
|
||||||
|
|
||||||
export function RouteMap() {
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const { setPosition, screenToLocal, setTransform, screenCenter } = useTransform();
|
|
||||||
const {
|
|
||||||
routeData, stationData, sightData, originalRouteData
|
|
||||||
} = useMapData();
|
|
||||||
const [points, setPoints] = useState<{x: number, y: number}[]>([]);
|
|
||||||
const [isSetup, setIsSetup] = useState(false);
|
|
||||||
|
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (originalRouteData) {
|
|
||||||
const path = originalRouteData?.path;
|
|
||||||
const points = path?.map(([x, y]: [number, number]) => ({x: x * UP_SCALE, y: y * UP_SCALE})) ?? [];
|
|
||||||
setPoints(points);
|
|
||||||
}
|
|
||||||
}, [originalRouteData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(isSetup || !screenCenter) {
|
if (originalRouteData) {
|
||||||
return;
|
const path = originalRouteData?.path;
|
||||||
}
|
const points =
|
||||||
|
path?.map(([x, y]: [number, number]) => ({
|
||||||
|
x: x * UP_SCALE,
|
||||||
|
y: y * UP_SCALE,
|
||||||
|
})) ?? [];
|
||||||
|
setPoints(points);
|
||||||
|
}
|
||||||
|
}, [originalRouteData]);
|
||||||
|
|
||||||
if (
|
useEffect(() => {
|
||||||
originalRouteData?.center_latitude === originalRouteData?.center_longitude &&
|
if (isSetup || !screenCenter) {
|
||||||
originalRouteData?.center_latitude === 0
|
return;
|
||||||
) {
|
}
|
||||||
if (points.length > 0) {
|
|
||||||
let boundingBox = {
|
|
||||||
from: {x: Infinity, y: Infinity},
|
|
||||||
to: {x: -Infinity, y: -Infinity}
|
|
||||||
};
|
|
||||||
for (const point of points) {
|
|
||||||
boundingBox.from.x = Math.min(boundingBox.from.x, point.x);
|
|
||||||
boundingBox.from.y = Math.min(boundingBox.from.y, point.y);
|
|
||||||
boundingBox.to.x = Math.max(boundingBox.to.x, point.x);
|
|
||||||
boundingBox.to.y = Math.max(boundingBox.to.y, point.y);
|
|
||||||
}
|
|
||||||
const newCenter = {
|
|
||||||
x: -(boundingBox.from.x + boundingBox.to.x) / 2,
|
|
||||||
y: -(boundingBox.from.y + boundingBox.to.y) / 2
|
|
||||||
};
|
|
||||||
setPosition(newCenter);
|
|
||||||
setIsSetup(true);
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
originalRouteData?.center_latitude &&
|
|
||||||
originalRouteData?.center_longitude
|
|
||||||
) {
|
|
||||||
const coordinates = coordinatesToLocal(originalRouteData?.center_latitude, originalRouteData?.center_longitude);
|
|
||||||
|
|
||||||
setTransform(
|
|
||||||
coordinates.x,
|
|
||||||
coordinates.y,
|
|
||||||
originalRouteData?.rotate,
|
|
||||||
originalRouteData?.scale_min
|
|
||||||
);
|
|
||||||
setIsSetup(true);
|
|
||||||
}
|
|
||||||
}, [points, originalRouteData?.center_latitude, originalRouteData?.center_longitude, originalRouteData?.rotate, isSetup, screenCenter]);
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
originalRouteData?.center_latitude ===
|
||||||
|
originalRouteData?.center_longitude &&
|
||||||
|
originalRouteData?.center_latitude === 0
|
||||||
|
) {
|
||||||
|
if (points.length > 0) {
|
||||||
|
let boundingBox = {
|
||||||
|
from: { x: Infinity, y: Infinity },
|
||||||
|
to: { x: -Infinity, y: -Infinity },
|
||||||
|
};
|
||||||
|
for (const point of points) {
|
||||||
|
boundingBox.from.x = Math.min(boundingBox.from.x, point.x);
|
||||||
|
boundingBox.from.y = Math.min(boundingBox.from.y, point.y);
|
||||||
|
boundingBox.to.x = Math.max(boundingBox.to.x, point.x);
|
||||||
|
boundingBox.to.y = Math.max(boundingBox.to.y, point.y);
|
||||||
|
}
|
||||||
|
const newCenter = {
|
||||||
|
x: -(boundingBox.from.x + boundingBox.to.x) / 2,
|
||||||
|
y: -(boundingBox.from.y + boundingBox.to.y) / 2,
|
||||||
|
};
|
||||||
|
setPosition(newCenter);
|
||||||
|
setIsSetup(true);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
originalRouteData?.center_latitude &&
|
||||||
|
originalRouteData?.center_longitude
|
||||||
|
) {
|
||||||
|
const coordinates = coordinatesToLocal(
|
||||||
|
originalRouteData?.center_latitude,
|
||||||
|
originalRouteData?.center_longitude
|
||||||
|
);
|
||||||
|
|
||||||
if (!routeData || !stationData || !sightData) {
|
setTransform(
|
||||||
console.error("routeData, stationData or sightData is null");
|
coordinates.x,
|
||||||
return <div>Loading...</div>;
|
coordinates.y,
|
||||||
}
|
originalRouteData?.rotate,
|
||||||
|
originalRouteData?.scale_min
|
||||||
|
);
|
||||||
|
setIsSetup(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
points,
|
||||||
|
originalRouteData?.center_latitude,
|
||||||
|
originalRouteData?.center_longitude,
|
||||||
|
originalRouteData?.rotate,
|
||||||
|
isSetup,
|
||||||
|
screenCenter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!routeData || !stationData || !sightData) {
|
||||||
|
console.error("routeData, stationData or sightData is null");
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{width: "100%", height:"100%"}} ref={parentRef}>
|
<div style={{ width: "100%", height: "100%" }} ref={parentRef}>
|
||||||
<Application
|
<Application resizeTo={parentRef} background="#fff">
|
||||||
resizeTo={parentRef}
|
<InfiniteCanvas>
|
||||||
background="#fff"
|
<TravelPath points={points} />
|
||||||
>
|
{stationData[language].map((obj, index) => (
|
||||||
<InfiniteCanvas>
|
<Station
|
||||||
<TravelPath points={points}/>
|
station={obj}
|
||||||
{stationData?.map((obj) => (
|
key={obj.id}
|
||||||
<Station station={obj} key={obj.id}/>
|
ruLabel={
|
||||||
))}
|
language === "ru"
|
||||||
{sightData?.map((obj, index) => (
|
? stationData.en[index].name
|
||||||
<Sight sight={obj} id={index} key={obj.id}/>
|
: stationData.ru[index].name
|
||||||
))}
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<pixiGraphics
|
<pixiGraphics
|
||||||
draw={(g) => {
|
draw={(g) => {
|
||||||
g.clear();
|
g.clear();
|
||||||
const localCenter = screenToLocal(0,0);
|
const localCenter = screenToLocal(0, 0);
|
||||||
g.circle(localCenter.x, localCenter.y, 10);
|
g.circle(localCenter.x, localCenter.y, 10);
|
||||||
g.fill("#fff");
|
g.fill("#fff");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</InfiniteCanvas>
|
</InfiniteCanvas>
|
||||||
</Application>
|
</Application>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
});
|
||||||
|
@ -280,13 +280,19 @@ export const RouteCreate = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
{...register("scale_min", {
|
{...register("scale_min", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
setValueAs: (value) => Number(value),
|
setValueAs: (value) => {
|
||||||
|
if (Number(value) < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Number(value);
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.scale_min}
|
error={!!(errors as any)?.scale_min}
|
||||||
helperText={(errors as any)?.scale_min?.message}
|
helperText={(errors as any)?.scale_min?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
slotProps={{ inputLabel: { shrink: true } }}
|
slotProps={{ inputLabel: { shrink: true } }}
|
||||||
|
inputProps={{ min: 0 }}
|
||||||
type="number"
|
type="number"
|
||||||
label={"Масштаб (мин)"}
|
label={"Масштаб (мин)"}
|
||||||
name="scale_min"
|
name="scale_min"
|
||||||
@ -295,13 +301,19 @@ export const RouteCreate = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
{...register("scale_max", {
|
{...register("scale_max", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
setValueAs: (value) => Number(value),
|
setValueAs: (value) => {
|
||||||
|
if (Number(value) < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Number(value);
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.scale_max}
|
error={!!(errors as any)?.scale_max}
|
||||||
helperText={(errors as any)?.scale_max?.message}
|
helperText={(errors as any)?.scale_max?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
slotProps={{ inputLabel: { shrink: true } }}
|
slotProps={{ inputLabel: { shrink: true } }}
|
||||||
|
inputProps={{ min: 0 }}
|
||||||
type="number"
|
type="number"
|
||||||
label={"Масштаб (макс)"}
|
label={"Масштаб (макс)"}
|
||||||
name="scale_max"
|
name="scale_max"
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Edit, useAutocomplete } from "@refinedev/mui";
|
import { Edit, 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, useWatch } from "react-hook-form";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { LinkedItems } from "../../components/LinkedItems";
|
import { LinkedItems } from "../../components/LinkedItems";
|
||||||
import {
|
import {
|
||||||
@ -76,6 +76,11 @@ export const RouteEdit = observer(() => {
|
|||||||
...META_LANGUAGE(language),
|
...META_LANGUAGE(language),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const carrierId = useWatch({ control, name: "carrier_id" });
|
||||||
|
const cityId = carrierAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === carrierId
|
||||||
|
)?.city_id;
|
||||||
|
|
||||||
const { autocompleteProps: governorAppealAutocompleteProps } =
|
const { autocompleteProps: governorAppealAutocompleteProps } =
|
||||||
useAutocomplete({
|
useAutocomplete({
|
||||||
resource: "article",
|
resource: "article",
|
||||||
@ -312,7 +317,12 @@ export const RouteEdit = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
{...register("scale_min", {
|
{...register("scale_min", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
setValueAs: (value) => Number(value),
|
setValueAs: (value) => {
|
||||||
|
if (Number(value) < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Number(value);
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.scale_min}
|
error={!!(errors as any)?.scale_min}
|
||||||
helperText={(errors as any)?.scale_min?.message}
|
helperText={(errors as any)?.scale_min?.message}
|
||||||
@ -327,7 +337,12 @@ export const RouteEdit = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
{...register("scale_max", {
|
{...register("scale_max", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
setValueAs: (value) => Number(value),
|
setValueAs: (value) => {
|
||||||
|
if (Number(value) < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Number(value);
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.scale_max}
|
error={!!(errors as any)?.scale_max}
|
||||||
helperText={(errors as any)?.scale_max?.message}
|
helperText={(errors as any)?.scale_max?.message}
|
||||||
@ -393,18 +408,19 @@ export const RouteEdit = observer(() => {
|
|||||||
parentResource="route"
|
parentResource="route"
|
||||||
childResource="station"
|
childResource="station"
|
||||||
fields={stationFields}
|
fields={stationFields}
|
||||||
title="станции"
|
title="остановки"
|
||||||
dragAllowed={true}
|
dragAllowed={true}
|
||||||
|
cityId={cityId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LinkedItems<VehicleItem>
|
{/* <LinkedItems<VehicleItem>
|
||||||
type="edit"
|
type="edit"
|
||||||
parentId={routeId}
|
parentId={routeId}
|
||||||
parentResource="route"
|
parentResource="route"
|
||||||
childResource="vehicle"
|
childResource="vehicle"
|
||||||
fields={vehicleFields}
|
fields={vehicleFields}
|
||||||
title="транспортные средства"
|
title="транспортные средства"
|
||||||
/>
|
/> */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -80,17 +80,17 @@ export const RouteShow = observer(() => {
|
|||||||
parentResource="route"
|
parentResource="route"
|
||||||
childResource="station"
|
childResource="station"
|
||||||
fields={stationFields}
|
fields={stationFields}
|
||||||
title="станции"
|
title="остановки"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LinkedItems<VehicleItem>
|
{/* <LinkedItems<VehicleItem>
|
||||||
type="show"
|
type="show"
|
||||||
parentId={record.id}
|
parentId={record.id}
|
||||||
parentResource="route"
|
parentResource="route"
|
||||||
childResource="vehicle"
|
childResource="vehicle"
|
||||||
fields={vehicleFields}
|
fields={vehicleFields}
|
||||||
title="транспортные средства"
|
title="транспортные средства"
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<LinkedItems<SightItem>
|
<LinkedItems<SightItem>
|
||||||
type="show"
|
type="show"
|
||||||
@ -103,10 +103,9 @@ export const RouteShow = observer(() => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "flex-start" }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start' }}>
|
<Button
|
||||||
<Button
|
variant="contained"
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => navigate(`/route-preview/${id}`)}
|
onClick={() => navigate(`/route-preview/${id}`)}
|
||||||
>
|
>
|
||||||
|
@ -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,
|
||||||
})}
|
})}
|
||||||
@ -433,7 +431,7 @@ export const SightCreate = observer(() => {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Выберите водный знак (Левый верх)"
|
label="Выберите водяной знак (Левый верх)"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.watermark_lu}
|
error={!!errors.watermark_lu}
|
||||||
@ -475,7 +473,7 @@ export const SightCreate = observer(() => {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Выберите водный знак (Правый верх)"
|
label="Выберите водяной знак (Правый верх)"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.watermark_rd}
|
error={!!errors.watermark_rd}
|
||||||
@ -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 }}>
|
||||||
|
@ -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,
|
||||||
})}
|
})}
|
||||||
@ -606,7 +594,7 @@ export const SightEdit = observer(() => {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Выберите водный знак (Левый верх)"
|
label="Выберите водяной знак (Левый верх)"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.arms}
|
||||||
@ -650,7 +638,7 @@ export const SightEdit = observer(() => {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Выберите водный знак (Правый вверх)"
|
label="Выберите водяной знак (Правый вверх)"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.arms}
|
||||||
@ -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())
|
||||||
);
|
);
|
||||||
@ -1479,7 +1468,7 @@ export const SightEdit = observer(() => {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Выберите водный знак (Левый верх)"
|
label="Выберите водяной знак (Левый верх)"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.arms}
|
||||||
@ -1523,7 +1512,7 @@ export const SightEdit = observer(() => {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Выберите водный знак (Правый вверх)"
|
label="Выберите водяной знак (Правый вверх)"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.arms}
|
||||||
@ -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>
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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) => [
|
||||||
@ -192,6 +204,8 @@ export const StationEdit = observer(() => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cityId = watch("city_id");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Edit saveButtonProps={saveButtonProps}>
|
<Edit saveButtonProps={saveButtonProps}>
|
||||||
<Box
|
<Box
|
||||||
@ -280,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}
|
||||||
@ -293,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
|
||||||
@ -353,6 +383,7 @@ export const StationEdit = observer(() => {
|
|||||||
fields={sightFields}
|
fields={sightFields}
|
||||||
title="достопримечательности"
|
title="достопримечательности"
|
||||||
dragAllowed={false}
|
dragAllowed={false}
|
||||||
|
cityId={cityId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Edit>
|
</Edit>
|
||||||
|
@ -7,14 +7,41 @@ import {
|
|||||||
ShowButton,
|
ShowButton,
|
||||||
useDataGrid,
|
useDataGrid,
|
||||||
} from "@refinedev/mui";
|
} from "@refinedev/mui";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { VEHICLE_TYPES } from "../../lib/constants";
|
import { VEHICLE_TYPES } from "../../lib/constants";
|
||||||
|
|
||||||
import { localeText } from "../../locales/ru/localeText";
|
import { localeText } from "../../locales/ru/localeText";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { languageStore } from "../../store/LanguageStore";
|
import { languageStore } from "../../store/LanguageStore";
|
||||||
|
import { axiosInstance } from "@providers";
|
||||||
|
|
||||||
export const VehicleList = observer(() => {
|
export const VehicleList = observer(() => {
|
||||||
|
const [carriers, setCarriers] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const [cities, setCities] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axiosInstance
|
||||||
|
.get("/carrier")
|
||||||
|
.then((res) => {
|
||||||
|
setCarriers(res.data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axiosInstance
|
||||||
|
.get("/city")
|
||||||
|
.then((res) => {
|
||||||
|
setCities(res.data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
const { dataGridProps } = useDataGrid({
|
const { dataGridProps } = useDataGrid({
|
||||||
@ -71,6 +98,38 @@ export const VehicleList = observer(() => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: "carrier-name",
|
||||||
|
headerName: "Перевозчик",
|
||||||
|
type: "string",
|
||||||
|
minWidth: 150,
|
||||||
|
display: "flex",
|
||||||
|
flex: 1,
|
||||||
|
align: "left",
|
||||||
|
headerAlign: "left",
|
||||||
|
renderCell: (params) => {
|
||||||
|
const value = params.row.carrier_id;
|
||||||
|
return carriers.find((carrier) => carrier.id === value)?.full_name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "city-name",
|
||||||
|
headerName: "Город",
|
||||||
|
type: "string",
|
||||||
|
minWidth: 150,
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
align: "left",
|
||||||
|
headerAlign: "left",
|
||||||
|
renderCell: (params) => {
|
||||||
|
const value = params.row.carrier_id;
|
||||||
|
return cities.find(
|
||||||
|
(city) =>
|
||||||
|
city.id ===
|
||||||
|
carriers.find((carrier) => carrier.id === value)?.city_id
|
||||||
|
)?.name;
|
||||||
|
},
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// field: "city",
|
// field: "city",
|
||||||
// headerName: "Город",
|
// headerName: "Город",
|
||||||
@ -93,7 +152,7 @@ export const VehicleList = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EditButton hideText recordItemId={row.id} />
|
<EditButton hideText recordItemId={row.id} />
|
||||||
<ShowButton hideText recordItemId={row.id} />
|
{/* <ShowButton hideText recordItemId={row.id} /> */}
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
hideText
|
hideText
|
||||||
confirmTitle="Вы уверены?"
|
confirmTitle="Вы уверены?"
|
||||||
@ -104,7 +163,7 @@ export const VehicleList = observer(() => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[]
|
[carriers, cities]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -3,7 +3,7 @@ import dataProvider from "@refinedev/simple-rest";
|
|||||||
import { TOKEN_KEY } from "@providers";
|
import { TOKEN_KEY } from "@providers";
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { languageStore } from "@stores";
|
|
||||||
export const axiosInstance = axios.create({
|
export const axiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_KRBL_API,
|
baseURL: import.meta.env.VITE_KRBL_API,
|
||||||
});
|
});
|
||||||
@ -22,6 +22,22 @@ axiosInstance.interceptors.request.use((config) => {
|
|||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const axiosInstanceForGet = (language: string) => {
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_KRBL_API,
|
||||||
|
});
|
||||||
|
axiosInstance.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.headers["X-Language"] = language;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
return axiosInstance;
|
||||||
|
};
|
||||||
|
|
||||||
const apiUrl = import.meta.env.VITE_KRBL_API;
|
const apiUrl = import.meta.env.VITE_KRBL_API;
|
||||||
|
|
||||||
export const customDataProvider = dataProvider(apiUrl, axiosInstance);
|
export const customDataProvider = dataProvider(apiUrl, axiosInstance);
|
||||||
|
Reference in New Issue
Block a user