Compare commits
14 Commits
1917b2cf5a
...
feature/we
| Author | SHA1 | Date | |
|---|---|---|---|
| 39e11ad5ca | |||
| 7e068e49f5 | |||
| 79539d0583 | |||
| c5c5f835bc | |||
| 5481d264e0 | |||
| d6772b1e3a | |||
| 11133b6839 | |||
| aaeaed3fa5 | |||
| 95fe297aae | |||
| 04a9ac452e | |||
| 85c71563c1 | |||
| 6f32c6e671 | |||
| 0a6192c7da | |||
| b1ba3b4cd5 |
5
.env
5
.env
@@ -1,3 +1,4 @@
|
||||
VITE_API_URL='https://wn.krbl.ru'
|
||||
VITE_REACT_APP ='https://wn.krbl.ru/'
|
||||
VITE_KRBL_MEDIA='https://wn.krbl.ru/media/'
|
||||
VITE_REACT_APP ='https://wn.krbl.ru'
|
||||
VITE_KRBL_MEDIA='https://wn.krbl.ru/media/'
|
||||
VITE_NEED_AUTH='true'
|
||||
@@ -41,7 +41,8 @@
|
||||
"react-toastify": "^11.0.5",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"three": "^0.177.0"
|
||||
"three": "^0.177.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
|
||||
@@ -51,7 +51,9 @@ import {
|
||||
|
||||
const PublicRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const { isAuthenticated } = authStore;
|
||||
if (isAuthenticated) {
|
||||
const need_auth = import.meta.env.VITE_NEED_AUTH == "true";
|
||||
|
||||
if (isAuthenticated || !need_auth) {
|
||||
return <Navigate to="/map" replace />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
@@ -59,13 +61,18 @@ const PublicRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const { isAuthenticated } = authStore;
|
||||
const need_auth = import.meta.env.VITE_NEED_AUTH == "true";
|
||||
|
||||
const location = useLocation();
|
||||
if (!isAuthenticated) {
|
||||
|
||||
if (!isAuthenticated && need_auth) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
if (location.pathname === "/") {
|
||||
return <Navigate to="/map" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -33,8 +33,10 @@ export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const { payload } = authStore;
|
||||
|
||||
const need_auth = import.meta.env.VITE_NEED_AUTH == "true";
|
||||
|
||||
// @ts-ignore
|
||||
const isAdmin = payload?.is_admin || false;
|
||||
const isAdmin = payload?.is_admin || false || !need_auth;
|
||||
|
||||
const isActive = item.path ? location.pathname.startsWith(item.path) : false;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import { PreviewLeftWidget } from "./PreviewLeftWidget";
|
||||
import { PreviewRightWidget } from "./PreviewRightWidget";
|
||||
import { articlesStore, languageStore } from "@shared";
|
||||
import { articlesStore, languageStore, LoadingSpinner } from "@shared";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
export const ArticlePreviewPage = () => {
|
||||
@@ -11,18 +11,41 @@ export const ArticlePreviewPage = () => {
|
||||
const { id } = useParams();
|
||||
const { getArticle, getArticleMedia, getArticlePreview } = articlesStore;
|
||||
const { language } = languageStore;
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (id) {
|
||||
await getArticle(Number(id), language);
|
||||
await getArticleMedia(Number(id));
|
||||
await getArticlePreview(Number(id));
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
await getArticle(Number(id), language);
|
||||
await getArticleMedia(Number(id));
|
||||
await getArticlePreview(Number(id));
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [id, language]);
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных статьи..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-4 mb-10">
|
||||
|
||||
@@ -6,13 +6,20 @@ import {
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { carrierStore, cityStore, mediaStore, languageStore } from "@shared";
|
||||
import {
|
||||
carrierStore,
|
||||
cityStore,
|
||||
mediaStore,
|
||||
languageStore,
|
||||
LoadingSpinner,
|
||||
} from "@shared";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ImageUploadCard, LanguageSwitcher, DeleteModal } from "@widgets";
|
||||
import {
|
||||
@@ -28,6 +35,7 @@ export const CarrierEditPage = observer(() => {
|
||||
const { language } = languageStore;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||
@@ -39,39 +47,48 @@ export const CarrierEditPage = observer(() => {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await cityStore.getCities("ru");
|
||||
await cityStore.getCities("en");
|
||||
await cityStore.getCities("zh");
|
||||
const carrierData = await getCarrier(Number(id));
|
||||
|
||||
if (carrierData) {
|
||||
setEditCarrierData(
|
||||
carrierData.ru?.full_name || "",
|
||||
carrierData.ru?.short_name || "",
|
||||
carrierData.ru?.city_id || 0,
|
||||
carrierData.ru?.slogan || "",
|
||||
carrierData.ru?.logo || "",
|
||||
"ru"
|
||||
);
|
||||
setEditCarrierData(
|
||||
carrierData.en?.full_name || "",
|
||||
carrierData.en?.short_name || "",
|
||||
carrierData.en?.city_id || 0,
|
||||
carrierData.en?.slogan || "",
|
||||
carrierData.en?.logo || "",
|
||||
"en"
|
||||
);
|
||||
setEditCarrierData(
|
||||
carrierData.zh?.full_name || "",
|
||||
carrierData.zh?.short_name || "",
|
||||
carrierData.zh?.city_id || 0,
|
||||
carrierData.zh?.slogan || "",
|
||||
carrierData.zh?.logo || "",
|
||||
"zh"
|
||||
);
|
||||
if (!id) {
|
||||
setIsLoadingData(false);
|
||||
return;
|
||||
}
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
await cityStore.getCities("ru");
|
||||
await cityStore.getCities("en");
|
||||
await cityStore.getCities("zh");
|
||||
const carrierData = await getCarrier(Number(id));
|
||||
|
||||
mediaStore.getMedia();
|
||||
if (carrierData) {
|
||||
setEditCarrierData(
|
||||
carrierData.ru?.full_name || "",
|
||||
carrierData.ru?.short_name || "",
|
||||
carrierData.ru?.city_id || 0,
|
||||
carrierData.ru?.slogan || "",
|
||||
carrierData.ru?.logo || "",
|
||||
"ru"
|
||||
);
|
||||
setEditCarrierData(
|
||||
carrierData.en?.full_name || "",
|
||||
carrierData.en?.short_name || "",
|
||||
carrierData.en?.city_id || 0,
|
||||
carrierData.en?.slogan || "",
|
||||
carrierData.en?.logo || "",
|
||||
"en"
|
||||
);
|
||||
setEditCarrierData(
|
||||
carrierData.zh?.full_name || "",
|
||||
carrierData.zh?.short_name || "",
|
||||
carrierData.zh?.city_id || 0,
|
||||
carrierData.zh?.slogan || "",
|
||||
carrierData.zh?.logo || "",
|
||||
"zh"
|
||||
);
|
||||
}
|
||||
|
||||
await mediaStore.getMedia();
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
})();
|
||||
|
||||
languageStore.setLanguage("ru");
|
||||
@@ -110,6 +127,21 @@ export const CarrierEditPage = observer(() => {
|
||||
? mediaStore.media.find((m) => m.id === editCarrierData.logo)
|
||||
: null;
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных перевозчика..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<LanguageSwitcher />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
languageStore,
|
||||
mediaStore,
|
||||
CashedCities,
|
||||
LoadingSpinner,
|
||||
} from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
export const CityEditPage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||
@@ -62,19 +65,26 @@ export const CityEditPage = observer(() => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
await getCountries("ru");
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
await getCountries("ru");
|
||||
|
||||
const ruData = await getCity(id as string, "ru");
|
||||
const enData = await getCity(id as string, "en");
|
||||
const zhData = await getCity(id as string, "zh");
|
||||
const ruData = await getCity(id as string, "ru");
|
||||
const enData = await getCity(id as string, "en");
|
||||
const zhData = await getCity(id as string, "zh");
|
||||
|
||||
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
||||
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
||||
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
||||
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
||||
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
||||
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
||||
|
||||
await getOneMedia(ruData.arms as string);
|
||||
await getOneMedia(ruData.arms as string);
|
||||
|
||||
await getMedia();
|
||||
await getMedia();
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
})();
|
||||
}, [id]);
|
||||
@@ -97,6 +107,21 @@ export const CityEditPage = observer(() => {
|
||||
? mediaStore.media.find((m) => m.id === editCityData.arms)
|
||||
: null;
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных города..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<LanguageSwitcher />
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Button, Paper, TextField } from "@mui/material";
|
||||
import { Button, Paper, TextField, Box } from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { countryStore, languageStore } from "@shared";
|
||||
import { countryStore, languageStore, LoadingSpinner } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const CountryEditPage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const { language } = languageStore;
|
||||
const { id } = useParams();
|
||||
const { editCountryData, editCountry, getCountry, setEditCountryData } =
|
||||
@@ -35,17 +36,39 @@ export const CountryEditPage = observer(() => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
const ruData = await getCountry(id as string, "ru");
|
||||
const enData = await getCountry(id as string, "en");
|
||||
const zhData = await getCountry(id as string, "zh");
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
const ruData = await getCountry(id as string, "ru");
|
||||
const enData = await getCountry(id as string, "en");
|
||||
const zhData = await getCountry(id as string, "zh");
|
||||
|
||||
setEditCountryData(ruData.name, "ru");
|
||||
setEditCountryData(enData.name, "en");
|
||||
setEditCountryData(zhData.name, "zh");
|
||||
setEditCountryData(ruData.name, "ru");
|
||||
setEditCountryData(enData.name, "en");
|
||||
setEditCountryData(zhData.name, "zh");
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных страны..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<LanguageSwitcher />
|
||||
|
||||
@@ -3,7 +3,12 @@ import { InformationTab, LeaveAgree, RightWidgetTab } from "@widgets";
|
||||
import { LeftWidgetTab } from "@widgets";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { articlesStore, cityStore, editSightStore } from "@shared";
|
||||
import {
|
||||
articlesStore,
|
||||
cityStore,
|
||||
editSightStore,
|
||||
LoadingSpinner,
|
||||
} from "@shared";
|
||||
import { useBlocker, useParams } from "react-router-dom";
|
||||
|
||||
function a11yProps(index: number) {
|
||||
@@ -15,7 +20,8 @@ function a11yProps(index: number) {
|
||||
|
||||
export const EditSightPage = observer(() => {
|
||||
const [value, setValue] = useState(0);
|
||||
const { sight, getSightInfo, needLeaveAgree } = editSightStore;
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const { sight, getSightInfo, needLeaveAgree, getRightArticles } = editSightStore;
|
||||
const { getArticles } = articlesStore;
|
||||
|
||||
const { id } = useParams();
|
||||
@@ -33,13 +39,22 @@ export const EditSightPage = observer(() => {
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (id) {
|
||||
await getCities("ru");
|
||||
await getSightInfo(+id, "ru");
|
||||
await getSightInfo(+id, "en");
|
||||
await getSightInfo(+id, "zh");
|
||||
await getArticles("ru");
|
||||
await getArticles("en");
|
||||
await getArticles("zh");
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
await getCities("ru");
|
||||
await getSightInfo(+id, "ru");
|
||||
await getSightInfo(+id, "en");
|
||||
await getSightInfo(+id, "zh");
|
||||
await getArticles("ru");
|
||||
await getArticles("en");
|
||||
await getArticles("zh");
|
||||
// Загружаем данные правого виджета перед завершением загрузки
|
||||
await getRightArticles(+id);
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
@@ -79,12 +94,25 @@ export const EditSightPage = observer(() => {
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{sight.common.id !== 0 && (
|
||||
<div className="flex-1">
|
||||
<InformationTab value={value} index={0} />
|
||||
<LeftWidgetTab value={value} index={1} />
|
||||
<RightWidgetTab value={value} index={2} />
|
||||
</div>
|
||||
{isLoadingData ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных достопримечательности..." />
|
||||
</Box>
|
||||
) : (
|
||||
sight.common.id !== 0 && (
|
||||
<div className="flex-1">
|
||||
<InformationTab value={value} index={0} />
|
||||
<LeftWidgetTab value={value} index={1} />
|
||||
<RightWidgetTab value={value} index={2} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{blocker.state === "blocked" ? <LeaveAgree blocker={blocker} /> : null}
|
||||
|
||||
@@ -124,6 +124,7 @@ export const clearMapCaches = () => {
|
||||
interface ApiRoute {
|
||||
id: number;
|
||||
route_number: string;
|
||||
route_name: string;
|
||||
path: [number, number][];
|
||||
center_latitude: number;
|
||||
center_longitude: number;
|
||||
@@ -370,6 +371,7 @@ class MapStore {
|
||||
this.routes = routeResponses.map((res) => ({
|
||||
id: res.data.id,
|
||||
route_number: res.data.route_number,
|
||||
route_name: res.data.route_name || "",
|
||||
path: res.data.path,
|
||||
center_latitude: res.data.center_latitude,
|
||||
center_longitude: res.data.center_longitude,
|
||||
@@ -486,16 +488,6 @@ class MapStore {
|
||||
const route_number = properties.name || "Маршрут 1";
|
||||
const path = geometry.coordinates.map((c: any) => [c[1], c[0]]);
|
||||
|
||||
const lineGeom = new GeoJSON().readGeometry(geometry, {
|
||||
dataProjection: "EPSG:4326",
|
||||
featureProjection: "EPSG:3857",
|
||||
});
|
||||
const centerCoords = getCenter(lineGeom.getExtent());
|
||||
const [center_longitude, center_latitude] = toLonLat(
|
||||
centerCoords,
|
||||
"EPSG:3857"
|
||||
);
|
||||
|
||||
let carrier_id = 0;
|
||||
let carrier = "";
|
||||
|
||||
@@ -513,8 +505,8 @@ class MapStore {
|
||||
const routeData = {
|
||||
route_number,
|
||||
path,
|
||||
center_latitude,
|
||||
center_longitude,
|
||||
center_latitude: path[0][0],
|
||||
center_longitude: path[0][1],
|
||||
carrier,
|
||||
carrier_id,
|
||||
governor_appeal: 0,
|
||||
@@ -2302,6 +2294,8 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
});
|
||||
feature.setId(`route-${route.id}`);
|
||||
feature.set("featureType", "route");
|
||||
feature.set("routeName", route.route_name);
|
||||
feature.set("routeNumber", route.route_number);
|
||||
return feature;
|
||||
});
|
||||
|
||||
@@ -2317,11 +2311,18 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
|
||||
const filteredFeatures = useMemo(() => {
|
||||
if (!searchQuery.trim()) return allFeatures;
|
||||
return allFeatures.filter((f) =>
|
||||
((f.get("name") as string) || "")
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
);
|
||||
const normalizedQuery = searchQuery.toLowerCase();
|
||||
return allFeatures.filter((f) => {
|
||||
const candidates = [
|
||||
(f.get("name") as string) || "",
|
||||
(f.get("description") as string) || "",
|
||||
(f.get("routeName") as string) || "",
|
||||
(f.get("routeNumber") as string) || "",
|
||||
];
|
||||
return candidates.some((value) =>
|
||||
value.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
});
|
||||
}, [allFeatures, searchQuery]);
|
||||
|
||||
const handleFeatureClick = useCallback(
|
||||
@@ -2649,6 +2650,38 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
featureType === "station" &&
|
||||
description &&
|
||||
description.trim() !== "";
|
||||
const routeName =
|
||||
featureType === "route"
|
||||
? (feature.get("routeName") as string) || ""
|
||||
: "";
|
||||
const routeNumber =
|
||||
featureType === "route"
|
||||
? (feature.get("routeNumber") as string) || fName
|
||||
: "";
|
||||
const routeNumberTrimmed = routeNumber.trim();
|
||||
const routeNameTrimmed = routeName.trim();
|
||||
const displayName =
|
||||
featureType === "route" ? routeNumberTrimmed || fName : fName;
|
||||
const showRouteName =
|
||||
featureType === "route" &&
|
||||
routeNameTrimmed !== "" &&
|
||||
routeNameTrimmed !== displayName;
|
||||
const titleParts: string[] = [];
|
||||
if (featureType === "route") {
|
||||
if (routeNumberTrimmed) {
|
||||
titleParts.push(routeNumberTrimmed);
|
||||
}
|
||||
if (routeNameTrimmed) {
|
||||
titleParts.push(routeNameTrimmed);
|
||||
}
|
||||
}
|
||||
const titleText =
|
||||
featureType === "route"
|
||||
? titleParts.join(" • ") ||
|
||||
routeNumberTrimmed ||
|
||||
routeNameTrimmed ||
|
||||
fName
|
||||
: fName;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -2667,7 +2700,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
checked={isChecked}
|
||||
onChange={() => handleCheckboxChange(fId)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={`Выбрать ${fName}`}
|
||||
aria-label={`Выбрать ${titleText}`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -2692,9 +2725,9 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
<span
|
||||
className={`font-medium whitespace-nowrap overflow-x-auto block
|
||||
scrollbar-visible`}
|
||||
title={fName}
|
||||
title={titleText}
|
||||
>
|
||||
{fName}
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
{showDescription && (
|
||||
@@ -2702,6 +2735,11 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{showRouteName && (
|
||||
<div className="mt-1 text-xs text-gray-600">
|
||||
{routeNameTrimmed}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center space-x-1 opacity-60 group-hover:opacity-100">
|
||||
<button
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
mediaStore,
|
||||
MEDIA_TYPE_LABELS,
|
||||
languageStore,
|
||||
LoadingSpinner,
|
||||
} from "@shared";
|
||||
import { MediaViewer } from "@widgets";
|
||||
|
||||
@@ -138,8 +139,15 @@ export const MediaEditPage = observer(() => {
|
||||
|
||||
if (!media && id) {
|
||||
return (
|
||||
<Box className="flex justify-center items-center h-screen">
|
||||
<CircularProgress />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных медиа..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
|
||||
import {
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
FormControl,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
@@ -35,10 +34,12 @@ import {
|
||||
} from "@hello-pangea/dnd";
|
||||
|
||||
import {
|
||||
AnimatedCircleButton,
|
||||
authInstance,
|
||||
languageStore,
|
||||
routeStore,
|
||||
selectedCityStore,
|
||||
stationsStore,
|
||||
} from "@shared";
|
||||
import { EditStationModal } from "../../widgets/modals/EditStationModal";
|
||||
|
||||
@@ -141,6 +142,9 @@ const LinkedItemsContentsInner = <
|
||||
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isLinkingSingle, setIsLinkingSingle] = useState(false);
|
||||
const [isLinkingBulk, setIsLinkingBulk] = useState(false);
|
||||
const [detachingIds, setDetachingIds] = useState<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {}, [error]);
|
||||
|
||||
@@ -182,6 +186,19 @@ const LinkedItemsContentsInner = <
|
||||
setPosition(linkedItems.length + 1);
|
||||
}, [linkedItems.length]);
|
||||
|
||||
const getStationTransfers = (stationId: number, fallbackTransfers?: any) => {
|
||||
const { stationLists } = stationsStore;
|
||||
for (const lang of ["ru", "en", "zh"] as const) {
|
||||
const station = stationLists[lang].data.find(
|
||||
(s: any) => s.id === stationId
|
||||
);
|
||||
if (station?.transfers) {
|
||||
return station.transfers;
|
||||
}
|
||||
}
|
||||
return fallbackTransfers;
|
||||
};
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
@@ -195,7 +212,14 @@ const LinkedItemsContentsInner = <
|
||||
|
||||
authInstance
|
||||
.post(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
stations: reorderedItems.map((item) => ({ id: item.id })),
|
||||
stations: reorderedItems.map((item) => {
|
||||
const stationData: any = { id: item.id };
|
||||
const transfers = getStationTransfers(item.id, item.transfers);
|
||||
if (transfers) {
|
||||
stationData.transfers = transfers;
|
||||
}
|
||||
return stationData;
|
||||
}),
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating station order:", error);
|
||||
@@ -242,14 +266,33 @@ const LinkedItemsContentsInner = <
|
||||
const linkItem = () => {
|
||||
if (selectedItemId !== null) {
|
||||
setError(null);
|
||||
const selectedItem = allItems.find((item) => item.id === selectedItemId);
|
||||
const requestData = {
|
||||
stations: insertAtPosition(
|
||||
linkedItems.map((item) => ({ id: item.id })),
|
||||
linkedItems.map((item) => {
|
||||
const stationData: any = { id: item.id };
|
||||
const transfers = getStationTransfers(item.id, item.transfers);
|
||||
if (transfers) {
|
||||
stationData.transfers = transfers;
|
||||
}
|
||||
return stationData;
|
||||
}),
|
||||
position,
|
||||
{ id: selectedItemId }
|
||||
(() => {
|
||||
const newStationData: any = { id: selectedItemId };
|
||||
const transfers = getStationTransfers(
|
||||
selectedItemId,
|
||||
selectedItem?.transfers
|
||||
);
|
||||
if (transfers) {
|
||||
newStationData.transfers = transfers;
|
||||
}
|
||||
return newStationData;
|
||||
})()
|
||||
),
|
||||
};
|
||||
|
||||
setIsLinkingSingle(true);
|
||||
authInstance
|
||||
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
||||
.then(() => {
|
||||
@@ -268,12 +311,20 @@ const LinkedItemsContentsInner = <
|
||||
.catch((error) => {
|
||||
console.error("Error linking item:", error);
|
||||
setError("Failed to link station");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLinkingSingle(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = (itemId: number) => {
|
||||
setError(null);
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(itemId);
|
||||
return next;
|
||||
});
|
||||
authInstance
|
||||
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
data: { [`${childResource}_id`]: itemId },
|
||||
@@ -285,6 +336,13 @@ const LinkedItemsContentsInner = <
|
||||
.catch((error) => {
|
||||
console.error("Error deleting item:", error);
|
||||
setError("Failed to delete station");
|
||||
})
|
||||
.finally(() => {
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(itemId);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -311,10 +369,26 @@ const LinkedItemsContentsInner = <
|
||||
if (selectedItems.size === 0) return;
|
||||
|
||||
setError(null);
|
||||
const selectedStations = Array.from(selectedItems).map((id) => ({ id }));
|
||||
setIsLinkingBulk(true);
|
||||
const selectedStations = Array.from(selectedItems).map((id) => {
|
||||
const item = allItems.find((item) => item.id === id);
|
||||
const stationData: any = { id };
|
||||
const transfers = getStationTransfers(id, item?.transfers);
|
||||
if (transfers) {
|
||||
stationData.transfers = transfers;
|
||||
}
|
||||
return stationData;
|
||||
});
|
||||
const requestData = {
|
||||
stations: [
|
||||
...linkedItems.map((item) => ({ id: item.id })),
|
||||
...linkedItems.map((item) => {
|
||||
const stationData: any = { id: item.id };
|
||||
const transfers = getStationTransfers(item.id, item.transfers);
|
||||
if (transfers) {
|
||||
stationData.transfers = transfers;
|
||||
}
|
||||
return stationData;
|
||||
}),
|
||||
...selectedStations,
|
||||
],
|
||||
};
|
||||
@@ -330,6 +404,9 @@ const LinkedItemsContentsInner = <
|
||||
.catch((error) => {
|
||||
console.error("Error linking items:", error);
|
||||
setError("Failed to link stations");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLinkingBulk(false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -399,7 +476,7 @@ const LinkedItemsContentsInner = <
|
||||
))}
|
||||
{type === "edit" && (
|
||||
<TableCell>
|
||||
<Button
|
||||
<AnimatedCircleButton
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
@@ -407,9 +484,11 @@ const LinkedItemsContentsInner = <
|
||||
e.stopPropagation();
|
||||
deleteItem(item.id);
|
||||
}}
|
||||
disabled={detachingIds.has(item.id)}
|
||||
loading={detachingIds.has(item.id)}
|
||||
>
|
||||
Отвязать
|
||||
</Button>
|
||||
</AnimatedCircleButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
@@ -520,14 +599,15 @@ const LinkedItemsContentsInner = <
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
<AnimatedCircleButton
|
||||
variant="contained"
|
||||
onClick={linkItem}
|
||||
disabled={!selectedItemId}
|
||||
disabled={!selectedItemId || isLinkingSingle}
|
||||
loading={isLinkingSingle}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</AnimatedCircleButton>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -587,14 +667,15 @@ const LinkedItemsContentsInner = <
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Button
|
||||
<AnimatedCircleButton
|
||||
variant="contained"
|
||||
onClick={handleBulkLink}
|
||||
disabled={selectedItems.size === 0}
|
||||
disabled={selectedItems.size === 0 || isLinkingBulk}
|
||||
loading={isLinkingBulk}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Добавить выбранные ({selectedItems.size})
|
||||
</Button>
|
||||
</AnimatedCircleButton>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save, Plus } from "lucide-react";
|
||||
import { ArrowLeft, Loader2, Save, Plus, X } from "lucide-react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -174,11 +174,6 @@ export const RouteCreatePage = observer(() => {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!governorAppeal) {
|
||||
toast.error("Выберите статью для обращения к пассажирам");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const validationResult = validateCoordinates(routeCoords);
|
||||
if (validationResult !== true) {
|
||||
@@ -213,7 +208,9 @@ export const RouteCreatePage = observer(() => {
|
||||
}
|
||||
|
||||
const carrier_id = Number(carrier);
|
||||
const governor_appeal = Number(governorAppeal);
|
||||
const governor_appeal = governorAppeal
|
||||
? Number(governorAppeal)
|
||||
: undefined;
|
||||
const rotate = turn ? Number(turn) : undefined;
|
||||
const center_latitude = centerLat ? Number(centerLat) : undefined;
|
||||
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
||||
@@ -238,7 +235,6 @@ export const RouteCreatePage = observer(() => {
|
||||
carrier_id,
|
||||
route_number: routeNumber,
|
||||
route_sys_number: govRouteNumber,
|
||||
governor_appeal,
|
||||
route_name: routeName,
|
||||
route_direction,
|
||||
scale_min: scale_min !== null ? scale_min : 0,
|
||||
@@ -251,6 +247,10 @@ export const RouteCreatePage = observer(() => {
|
||||
videoPreview && videoPreview !== "" ? videoPreview : undefined,
|
||||
};
|
||||
|
||||
if (governor_appeal !== undefined) {
|
||||
newRoute.governor_appeal = governor_appeal;
|
||||
}
|
||||
|
||||
await routeStore.createRoute(newRoute);
|
||||
toast.success("Маршрут успешно создан");
|
||||
navigate(-1);
|
||||
@@ -382,6 +382,17 @@ export const RouteCreatePage = observer(() => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{selectedArticle && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setGovernorAppeal("")}
|
||||
startIcon={<X size={16} />}
|
||||
sx={{ minWidth: "auto", px: 2 }}
|
||||
>
|
||||
Сбросить
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||
@@ -420,7 +431,15 @@ export const RouteCreatePage = observer(() => {
|
||||
type="number"
|
||||
value={scaleMin}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
let value = e.target.value;
|
||||
if (Number(value) > 297) {
|
||||
value = "297";
|
||||
}
|
||||
|
||||
if (Number(value) < 10) {
|
||||
value = "10";
|
||||
}
|
||||
|
||||
setScaleMin(value);
|
||||
if (value && scaleMax && Number(value) > Number(scaleMax)) {
|
||||
setScaleMax(value);
|
||||
@@ -447,6 +466,10 @@ export const RouteCreatePage = observer(() => {
|
||||
value={scaleMax}
|
||||
required
|
||||
onChange={(e) => {
|
||||
if (Number(e.target.value) > 300) {
|
||||
e.target.value = "300";
|
||||
}
|
||||
|
||||
const value = e.target.value;
|
||||
setScaleMax(value);
|
||||
}}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Copy, Save, Plus } from "lucide-react";
|
||||
import { ArrowLeft, Copy, Save, Plus, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ArticleSelectOrCreateDialog,
|
||||
SelectMediaDialog,
|
||||
UploadMediaDialog,
|
||||
LoadingSpinner,
|
||||
} from "@shared";
|
||||
import { toast } from "react-toastify";
|
||||
import { stationsStore } from "@shared";
|
||||
@@ -37,6 +38,7 @@ export const RouteEditPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { editRouteData, copyRouteAction } = routeStore;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
|
||||
useState(false);
|
||||
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
||||
@@ -48,18 +50,27 @@ export const RouteEditPage = observer(() => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const response = await routeStore.getRoute(Number(id));
|
||||
routeStore.setEditRouteData(response);
|
||||
languageStore.setLanguage("ru");
|
||||
if (!id) {
|
||||
setIsLoadingData(false);
|
||||
return;
|
||||
}
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
const response = await routeStore.getRoute(Number(id));
|
||||
routeStore.setEditRouteData(response);
|
||||
languageStore.setLanguage("ru");
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
carrierStore.getCarriers(language);
|
||||
stationsStore.getStations();
|
||||
articlesStore.getArticleList();
|
||||
await carrierStore.getCarriers(language);
|
||||
await stationsStore.getStations();
|
||||
await articlesStore.getArticleList();
|
||||
};
|
||||
fetchData();
|
||||
}, [id, language]);
|
||||
@@ -91,10 +102,6 @@ export const RouteEditPage = observer(() => {
|
||||
toast.error("Заполните номер маршрута в Говорящем Городе");
|
||||
return;
|
||||
}
|
||||
if (!editRouteData.governor_appeal) {
|
||||
toast.error("Выберите статью для обращения к пассажирам");
|
||||
return;
|
||||
}
|
||||
|
||||
const validationResult = validateCoordinates(coordinates);
|
||||
if (validationResult !== true) {
|
||||
@@ -233,6 +240,21 @@ export const RouteEditPage = observer(() => {
|
||||
(article) => article.id === editRouteData.governor_appeal
|
||||
);
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных маршрута..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -393,20 +415,29 @@ export const RouteEditPage = observer(() => {
|
||||
type="number"
|
||||
value={editRouteData.scale_min ?? ""}
|
||||
onChange={(e) => {
|
||||
const value =
|
||||
e.target.value === "" ? null : parseFloat(e.target.value);
|
||||
let value = e.target.value === "" ? null : e.target.value;
|
||||
|
||||
if (value && Number(value) > 297) {
|
||||
value = "297";
|
||||
}
|
||||
|
||||
if (value && Number(value) < 10) {
|
||||
value = "10";
|
||||
}
|
||||
|
||||
routeStore.setEditRouteData({
|
||||
scale_min: value,
|
||||
scale_min: value ? Number(value) : null,
|
||||
});
|
||||
// Если максимальный масштаб стал меньше минимального, обновляем его
|
||||
if (
|
||||
value !== null &&
|
||||
editRouteData.scale_max !== null &&
|
||||
editRouteData.scale_max !== undefined &&
|
||||
value > editRouteData.scale_max
|
||||
value &&
|
||||
Number(value) > (editRouteData.scale_max ?? 0)
|
||||
) {
|
||||
routeStore.setEditRouteData({
|
||||
scale_max: value,
|
||||
scale_max: value ? Number(value) : null,
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -418,12 +449,17 @@ export const RouteEditPage = observer(() => {
|
||||
label="Масштаб (макс)"
|
||||
type="number"
|
||||
value={editRouteData.scale_max ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
let value = e.target.value;
|
||||
|
||||
if (Number(value) > 300) {
|
||||
value = "300";
|
||||
}
|
||||
|
||||
routeStore.setEditRouteData({
|
||||
scale_max:
|
||||
e.target.value === "" ? null : parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
scale_max: value === "" ? null : parseFloat(value),
|
||||
});
|
||||
}}
|
||||
error={
|
||||
editRouteData.scale_min !== null &&
|
||||
editRouteData.scale_min !== undefined &&
|
||||
@@ -491,6 +527,21 @@ export const RouteEditPage = observer(() => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{selectedArticle && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() =>
|
||||
routeStore.setEditRouteData({
|
||||
governor_appeal: 0,
|
||||
})
|
||||
}
|
||||
startIcon={<X size={16} />}
|
||||
sx={{ minWidth: "auto", px: 2 }}
|
||||
>
|
||||
Сбросить
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||
|
||||
@@ -5,5 +5,5 @@ export const STATION_OUTLINE_WIDTH = 4;
|
||||
export const SIGHT_SIZE = 40;
|
||||
export const SCALE_FACTOR = 50;
|
||||
|
||||
export const BACKGROUND_COLOR = 0x111111;
|
||||
export const BACKGROUND_COLOR = 0x000;
|
||||
export const PATH_COLOR = 0xff4d4d;
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Stack, Typography, Button } from "@mui/material";
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import { useNavigate, useNavigationType } from "react-router";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import { authInstance } from "@shared";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import LanguageSelector from "./web-gl/LanguageSelector";
|
||||
|
||||
export const LeftSidebar = observer(() => {
|
||||
type LeftSidebarProps = {
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
};
|
||||
|
||||
export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
|
||||
const navigate = useNavigate();
|
||||
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
|
||||
const { routeData } = useMapData();
|
||||
@@ -35,101 +42,131 @@ export const LeftSidebar = observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column" width="300px" p={2} bgcolor="primary.main">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
type="button"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
color: "#fff",
|
||||
backgroundColor: "#222",
|
||||
borderRadius: 10,
|
||||
height: 40,
|
||||
width: "100%",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<p>Назад</p>
|
||||
</button>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
color: "#fff",
|
||||
transition: "padding 0.3s ease",
|
||||
p: open ? 2 : 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "stretch",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
my={10}
|
||||
height="100%"
|
||||
width="100%"
|
||||
spacing={4}
|
||||
alignItems="stretch"
|
||||
justifyContent="space-between"
|
||||
sx={{
|
||||
opacity: open ? 1 : 0,
|
||||
transition: "opacity 0.25s ease",
|
||||
pointerEvents: open ? "auto" : "none",
|
||||
display: open ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 200,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
}}
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{
|
||||
backgroundColor: "#222",
|
||||
color: "#fff",
|
||||
borderRadius: 1.5,
|
||||
px: 2,
|
||||
py: 1,
|
||||
marginBottom: 10,
|
||||
"&:hover": {
|
||||
backgroundColor: "#2d2d2d",
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
startIcon={<ArrowBackIcon />}
|
||||
>
|
||||
Назад
|
||||
</Button>
|
||||
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
spacing={3}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 150,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{carrierThumbnail && (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: carrierThumbnail,
|
||||
media_type: 1, // Тип "Фото" для логотипа
|
||||
filename: "route_thumbnail",
|
||||
}}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
)}
|
||||
<Typography sx={{ color: "#fff" }} textAlign="center">
|
||||
При поддержке Правительства
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-2 mt-10">
|
||||
<button className="bg-[#fcd500] text-black px-4 py-2 rounded-md w-full font-medium my-10">
|
||||
Обращение губернатора
|
||||
</button>
|
||||
<button className="bg-white text-black px-4 py-2 rounded-md w-full font-medium mx-5">
|
||||
Достопримечательности
|
||||
</button>
|
||||
<button className="bg-white text-black px-4 py-2 rounded-md w-full font-medium mx-5">
|
||||
Остановки
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Stack
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
maxHeight={150}
|
||||
justifyContent="center"
|
||||
flexGrow={1}
|
||||
>
|
||||
{carrierThumbnail && (
|
||||
{carrierLogo && (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: carrierThumbnail,
|
||||
id: carrierLogo,
|
||||
media_type: 1, // Тип "Фото" для логотипа
|
||||
filename: "route_thumbnail",
|
||||
filename: "route_thumbnail_logo",
|
||||
}}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
)}
|
||||
<Typography sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||
При поддержке Правительства
|
||||
</Typography>{" "}
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
textAlign="center"
|
||||
sx={{ color: "#fff", marginTop: "auto" }}
|
||||
>
|
||||
#ВсемПоПути
|
||||
</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"
|
||||
maxHeight={150}
|
||||
justifyContent="center"
|
||||
my={10}
|
||||
>
|
||||
{carrierLogo && (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: carrierLogo,
|
||||
media_type: 1, // Тип "Фото" для логотипа
|
||||
filename: "route_thumbnail_logo",
|
||||
}}
|
||||
fullHeight
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
textAlign="center"
|
||||
mt="auto"
|
||||
sx={{ color: "#fff" }}
|
||||
>
|
||||
#ВсемПоПути
|
||||
</Typography>
|
||||
</Stack>
|
||||
<div className="absolute bottom-[20px] -right-[520px] z-10">
|
||||
<LanguageSelector onBack={onToggle} isSidebarOpen={open} />
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -41,6 +41,8 @@ const MapDataContext = createContext<{
|
||||
latitude: number,
|
||||
longitude: number
|
||||
) => void;
|
||||
setIconSize: (size: number) => void;
|
||||
setFontSize: (size: number) => void;
|
||||
saveChanges: () => void;
|
||||
}>({
|
||||
originalRouteData: undefined,
|
||||
@@ -61,6 +63,8 @@ const MapDataContext = createContext<{
|
||||
setStationOffset: () => {},
|
||||
setStationAlign: () => {},
|
||||
setSightCoordinates: () => {},
|
||||
setIconSize: () => {},
|
||||
setFontSize: () => {},
|
||||
saveChanges: () => {},
|
||||
});
|
||||
|
||||
@@ -164,9 +168,57 @@ export const MapDataProvider = observer(
|
||||
});
|
||||
}
|
||||
|
||||
function setMapCenter(x: number, y: number) {
|
||||
function setIconSize(size: number) {
|
||||
const clamped = Math.max(50, Math.min(300, size));
|
||||
setRouteChanges((prev) => {
|
||||
return { ...prev, center_latitude: x, center_longitude: y };
|
||||
if (prev.icon_size === clamped) {
|
||||
return prev;
|
||||
}
|
||||
return { ...prev, icon_size: clamped };
|
||||
});
|
||||
}
|
||||
|
||||
function setFontSize(size: number) {
|
||||
const clamped = Math.max(50, Math.min(300, size));
|
||||
setRouteChanges((prev) => {
|
||||
if (prev.font_size === clamped) {
|
||||
return prev;
|
||||
}
|
||||
return { ...prev, font_size: clamped };
|
||||
});
|
||||
}
|
||||
|
||||
function setMapCenter(latitude: number, longitude: number) {
|
||||
const epsilon = 1e-6;
|
||||
|
||||
setRouteChanges((prev) => {
|
||||
const prevLat = prev.center_latitude;
|
||||
const prevLon = prev.center_longitude;
|
||||
|
||||
if (
|
||||
prevLat !== undefined &&
|
||||
prevLon !== undefined &&
|
||||
Math.abs(prevLat - latitude) < epsilon &&
|
||||
Math.abs(prevLon - longitude) < epsilon
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
center_latitude: latitude,
|
||||
center_longitude: longitude,
|
||||
};
|
||||
});
|
||||
|
||||
setRouteData((routePrev) => {
|
||||
if (!routePrev) return routePrev;
|
||||
|
||||
return {
|
||||
...routePrev,
|
||||
center_latitude: latitude,
|
||||
center_longitude: longitude,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,12 +231,42 @@ export const MapDataProvider = observer(
|
||||
async function saveStationChanges() {
|
||||
for (const station of stationChanges) {
|
||||
await authInstance.patch(`/route/${routeId}/station`, station);
|
||||
|
||||
setStationData((prev) => {
|
||||
const updated = { ...prev };
|
||||
Object.keys(updated).forEach((lang) => {
|
||||
updated[lang] = updated[lang].map((s) =>
|
||||
s.id === station.station_id
|
||||
? {
|
||||
...s,
|
||||
offset_x: station.offset_x,
|
||||
offset_y: station.offset_y,
|
||||
}
|
||||
: s
|
||||
);
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSightChanges() {
|
||||
for (const sight of sightChanges) {
|
||||
await authInstance.patch(`/route/${routeId}/sight`, sight);
|
||||
|
||||
setSightData((prev) =>
|
||||
prev
|
||||
? prev.map((s) =>
|
||||
s.id === sight.sight_id
|
||||
? {
|
||||
...s,
|
||||
latitude: sight.latitude,
|
||||
longitude: sight.longitude,
|
||||
}
|
||||
: s
|
||||
)
|
||||
: prev
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +402,14 @@ export const MapDataProvider = observer(
|
||||
latitude: number,
|
||||
longitude: number
|
||||
) {
|
||||
setSightData((prev) =>
|
||||
prev
|
||||
? prev.map((sight) =>
|
||||
sight.id === sightId ? { ...sight, latitude, longitude } : sight
|
||||
)
|
||||
: prev
|
||||
);
|
||||
|
||||
setSightChanges((prev) => {
|
||||
const existingIndex = prev.findIndex(
|
||||
(sight) => sight.sight_id === sightId
|
||||
@@ -375,6 +465,8 @@ export const MapDataProvider = observer(
|
||||
setStationOffset,
|
||||
setStationAlign,
|
||||
setSightCoordinates,
|
||||
setIconSize,
|
||||
setFontSize,
|
||||
}),
|
||||
[
|
||||
originalRouteData,
|
||||
@@ -387,6 +479,8 @@ export const MapDataProvider = observer(
|
||||
isStationLoading,
|
||||
isSightLoading,
|
||||
selectedSight,
|
||||
setIconSize,
|
||||
setFontSize,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Button, Stack, TextField, Typography, Slider } from "@mui/material";
|
||||
import {
|
||||
Button,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
Slider,
|
||||
CircularProgress,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTransform } from "./TransformContext";
|
||||
import { coordinatesToLocal, localToCoordinates } from "./utils";
|
||||
import { SCALE_FACTOR } from "./Constants";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
@@ -14,17 +21,10 @@ export function RightSidebar() {
|
||||
originalRouteData,
|
||||
setMapRotation,
|
||||
setMapCenter,
|
||||
setIconSize: updateIconSize,
|
||||
setFontSize: updateFontSize,
|
||||
} = useMapData();
|
||||
const {
|
||||
rotation,
|
||||
position,
|
||||
screenToLocal,
|
||||
screenCenter,
|
||||
rotateToAngle,
|
||||
setTransform,
|
||||
scale,
|
||||
setScaleAtCenter,
|
||||
} = useTransform();
|
||||
const { rotation, rotateToAngle, scale, setScaleAtCenter } = useTransform();
|
||||
|
||||
const [minScale, setMinScale] = useState<number>(1);
|
||||
const [maxScale, setMaxScale] = useState<number>(5);
|
||||
@@ -34,6 +34,9 @@ export function RightSidebar() {
|
||||
});
|
||||
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
||||
const [isUserEditing, setIsUserEditing] = useState<boolean>(false);
|
||||
const [iconSize, setIconSize] = useState<number>(100);
|
||||
const [fontSize, setFontSize] = useState<number>(100);
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (originalRouteData) {
|
||||
@@ -50,6 +53,8 @@ export function RightSidebar() {
|
||||
x: originalRouteData.center_latitude ?? 0,
|
||||
y: originalRouteData.center_longitude ?? 0,
|
||||
});
|
||||
setIconSize(originalRouteData.icon_size ?? 100);
|
||||
setFontSize(originalRouteData.font_size ?? 100);
|
||||
}
|
||||
}, [originalRouteData]);
|
||||
|
||||
@@ -70,33 +75,55 @@ export function RightSidebar() {
|
||||
}, [rotationDegrees]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUserEditing) {
|
||||
const center = screenCenter ?? { x: 0, y: 0 };
|
||||
const localCenter = screenToLocal(center.x, center.y);
|
||||
const coordinates = localToCoordinates(localCenter.x, localCenter.y);
|
||||
setLocalCenter({ x: coordinates.latitude, y: coordinates.longitude });
|
||||
if (isUserEditing) {
|
||||
return;
|
||||
}
|
||||
}, [
|
||||
position,
|
||||
screenCenter,
|
||||
screenToLocal,
|
||||
localToCoordinates,
|
||||
setLocalCenter,
|
||||
isUserEditing,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setMapCenter(localCenter.x, localCenter.y);
|
||||
}, [localCenter]);
|
||||
const latitude = routeData?.center_latitude ?? 0;
|
||||
const longitude = routeData?.center_longitude ?? 0;
|
||||
|
||||
setLocalCenter((prev) => {
|
||||
if (
|
||||
Math.abs(prev.x - latitude) < 1e-6 &&
|
||||
Math.abs(prev.y - longitude) < 1e-6
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return { x: latitude, y: longitude };
|
||||
});
|
||||
}, [isUserEditing, routeData?.center_latitude, routeData?.center_longitude]);
|
||||
|
||||
function setRotationFromDegrees(degrees: number) {
|
||||
rotateToAngle((degrees * Math.PI) / 180);
|
||||
}
|
||||
|
||||
function pan({ x, y }: { x: number; y: number }) {
|
||||
const coordinates = coordinatesToLocal(x, y);
|
||||
setTransform(coordinates.x, coordinates.y);
|
||||
}
|
||||
const handleIconSizeChange = (value: number) => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return;
|
||||
}
|
||||
const clamped = Math.max(50, Math.min(300, Math.round(value)));
|
||||
setIconSize(clamped);
|
||||
updateIconSize(clamped);
|
||||
};
|
||||
|
||||
const handleFontSizeChange = (value: number) => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return;
|
||||
}
|
||||
const clamped = Math.max(50, Math.min(300, Math.round(value)));
|
||||
setFontSize(clamped);
|
||||
updateFontSize(clamped);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const next = routeData?.icon_size ?? originalRouteData?.icon_size ?? 100;
|
||||
setIconSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
|
||||
}, [routeData?.icon_size, originalRouteData?.icon_size]);
|
||||
|
||||
useEffect(() => {
|
||||
const next = routeData?.font_size ?? originalRouteData?.font_size ?? 100;
|
||||
setFontSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
|
||||
}, [routeData?.font_size, originalRouteData?.font_size]);
|
||||
|
||||
if (!routeData) {
|
||||
return null;
|
||||
@@ -128,8 +155,12 @@ export function RightSidebar() {
|
||||
onChange={(e) => {
|
||||
let newMinScale = Number(e.target.value);
|
||||
|
||||
if (newMinScale < 1) {
|
||||
newMinScale = 1;
|
||||
if (newMinScale < 10) {
|
||||
newMinScale = 10;
|
||||
}
|
||||
|
||||
if (newMinScale > 300) {
|
||||
newMinScale = 297;
|
||||
}
|
||||
|
||||
setMinScale(newMinScale);
|
||||
@@ -137,6 +168,10 @@ export function RightSidebar() {
|
||||
if (maxScale - newMinScale < 2) {
|
||||
let newMaxScale = newMinScale + 2;
|
||||
|
||||
if (newMaxScale > 300) {
|
||||
newMaxScale = 300;
|
||||
}
|
||||
|
||||
if (newMaxScale < 3) {
|
||||
newMaxScale = 3;
|
||||
setMinScale(1);
|
||||
@@ -172,8 +207,12 @@ export function RightSidebar() {
|
||||
onChange={(e) => {
|
||||
let newMaxScale = Number(e.target.value);
|
||||
|
||||
if (newMaxScale < 3) {
|
||||
newMaxScale = 3;
|
||||
if (newMaxScale < 13) {
|
||||
newMaxScale = 13;
|
||||
}
|
||||
|
||||
if (newMaxScale > 300) {
|
||||
newMaxScale = 300;
|
||||
}
|
||||
|
||||
setMaxScale(newMaxScale);
|
||||
@@ -204,7 +243,7 @@ export function RightSidebar() {
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 3,
|
||||
max: 10,
|
||||
max: 300,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -268,6 +307,62 @@ export function RightSidebar() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" sx={{ color: "#fff", textAlign: "center" }}>
|
||||
Размер иконок: {iconSize}%
|
||||
</Typography>
|
||||
|
||||
<Slider
|
||||
value={iconSize}
|
||||
onChange={(_, value) => {
|
||||
if (typeof value === "number") {
|
||||
handleIconSizeChange(value);
|
||||
}
|
||||
}}
|
||||
min={50}
|
||||
max={300}
|
||||
step={1}
|
||||
sx={{
|
||||
color: "#fff",
|
||||
"& .MuiSlider-thumb": {
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
"& .MuiSlider-track": {
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
"& .MuiSlider-rail": {
|
||||
backgroundColor: "#666",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" sx={{ color: "#fff", textAlign: "center" }}>
|
||||
Размер шрифта: {fontSize}%
|
||||
</Typography>
|
||||
|
||||
<Slider
|
||||
value={fontSize}
|
||||
onChange={(_, value) => {
|
||||
if (typeof value === "number") {
|
||||
handleFontSizeChange(value);
|
||||
}
|
||||
}}
|
||||
min={50}
|
||||
max={300}
|
||||
step={1}
|
||||
sx={{
|
||||
color: "#fff",
|
||||
"& .MuiSlider-thumb": {
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
"& .MuiSlider-track": {
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
"& .MuiSlider-rail": {
|
||||
backgroundColor: "#666",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
type="number"
|
||||
label="Поворот (в градусах)"
|
||||
@@ -309,10 +404,15 @@ export function RightSidebar() {
|
||||
value={Math.round(localCenter.x * 1000) / 1000}
|
||||
onChange={(e) => {
|
||||
setIsUserEditing(true);
|
||||
setLocalCenter((prev) => ({ ...prev, x: Number(e.target.value) }));
|
||||
pan({ x: Number(e.target.value), y: localCenter.y });
|
||||
const newValue = Number(e.target.value);
|
||||
setLocalCenter((prev) => ({ ...prev, x: newValue }));
|
||||
if (!isNaN(newValue) && localCenter.y !== undefined) {
|
||||
setMapCenter(newValue, localCenter.y);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsUserEditing(false);
|
||||
}}
|
||||
onBlur={() => setIsUserEditing(false)}
|
||||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||
sx={{
|
||||
"& .MuiInputLabel-root": {
|
||||
@@ -328,15 +428,20 @@ export function RightSidebar() {
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Центр карты, высота"
|
||||
label="Центр карты, долгота"
|
||||
variant="filled"
|
||||
value={Math.round(localCenter.y * 1000) / 1000}
|
||||
onChange={(e) => {
|
||||
setIsUserEditing(true);
|
||||
setLocalCenter((prev) => ({ ...prev, y: Number(e.target.value) }));
|
||||
pan({ x: localCenter.x, y: Number(e.target.value) });
|
||||
const newValue = Number(e.target.value);
|
||||
setLocalCenter((prev) => ({ ...prev, y: newValue }));
|
||||
if (!isNaN(newValue) && localCenter.x !== undefined) {
|
||||
setMapCenter(localCenter.x, newValue);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsUserEditing(false);
|
||||
}}
|
||||
onBlur={() => setIsUserEditing(false)}
|
||||
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||
sx={{
|
||||
"& .MuiInputLabel-root": {
|
||||
@@ -355,19 +460,51 @@ export function RightSidebar() {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{ mt: 2 }}
|
||||
sx={{ mt: 2, position: "relative" }}
|
||||
disabled={isSaving}
|
||||
onClick={async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await saveChanges();
|
||||
toast.success("Изменения сохранены");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Ошибка при сохранении изменений");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Сохранить изменения
|
||||
{isSaving ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={20} sx={{ color: "inherit" }} />
|
||||
<span>Сохранение...</span>
|
||||
</Box>
|
||||
) : (
|
||||
"Сохранить изменения"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
style={{ cursor: "pointer" }}
|
||||
className="absolute bottom-5 left-[-68px] z-100"
|
||||
>
|
||||
<path
|
||||
d="M24.0013 0C23.3513 0 22.7013 0.03 22.0413 0.08C10.4213 1 1.01127 10.41 0.0812683 22.03C-0.428732 28.39 1.55127 34.28 5.14127 38.83C5.60127 39.42 5.76127 40.21 5.46127 40.89C4.76127 42.43 3.63127 43.64 3.05127 44.23C2.50127 44.78 1.97127 45.27 1.38127 45.7C0.791268 46.13 0.841268 47.03 1.45127 47.42C2.08127 47.82 3.01127 47.99 4.12127 47.99C6.84127 47.99 10.5813 46.99 13.3013 46.06C13.5013 45.99 13.7013 45.96 13.9113 45.96C14.1813 45.96 14.4613 46.02 14.7113 46.13C17.5713 47.33 20.7113 48 24.0013 48C24.6513 48 25.3213 47.97 25.9813 47.92C37.6313 46.98 47.0613 37.51 47.9313 25.85C48.9913 11.76 37.8713 0 24.0013 0ZM29.5113 37.71C29.4813 37.82 29.3413 37.94 29.2313 37.98C27.7413 38.48 26.2713 39.12 24.7313 39.42C22.9513 39.77 21.1413 39.68 19.5513 38.58C18.2213 37.66 17.7313 36.36 17.8113 34.8C17.9013 32.91 18.5113 31.13 19.0013 29.33C19.5213 27.42 20.1113 25.53 20.4613 23.59C20.9413 20.94 19.7813 20.48 17.3913 20.74C16.8013 20.8 16.2313 21.04 15.5813 21.22C15.7213 20.62 15.8313 20.08 15.9913 19.55C16.0213 19.45 19.6313 17.94 21.4413 17.78C23.3513 17.61 25.2013 17.8 26.6013 19.32C27.3913 20.17 27.6113 21.21 27.5913 22.33C27.5413 24.8 26.5813 27.07 25.9813 29.42C25.6113 30.86 25.2513 32.3 24.9313 33.75C24.8413 34.15 24.8413 34.59 24.8813 35C24.9613 35.97 25.4413 36.39 26.4313 36.57C27.6213 36.78 28.7213 36.45 29.9313 36.04C29.7813 36.66 29.6613 37.19 29.5213 37.7L29.5113 37.71ZM26.8513 15.21C26.6513 15.23 26.4613 15.23 26.2013 15.25C24.4013 15.27 22.7313 14.15 22.2013 12.52C21.5913 10.65 22.5613 8.71 24.5313 7.86C26.7913 6.88 29.5813 8.07 30.2713 10.33C31.0513 12.87 29.0613 14.95 26.8613 15.21H26.8513Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { Widgets } from "./Widgets";
|
||||
import { Application, extend } from "@pixi/react";
|
||||
import { extend } from "@pixi/react";
|
||||
import {
|
||||
Container,
|
||||
Graphics,
|
||||
@@ -9,24 +9,18 @@ import {
|
||||
TilingSprite,
|
||||
Text,
|
||||
} from "pixi.js";
|
||||
import { Stack } from "@mui/material";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import { MapDataProvider, useMapData } from "./MapDataContext";
|
||||
import { TransformProvider, useTransform } from "./TransformContext";
|
||||
import { InfiniteCanvas } from "./InfiniteCanvas";
|
||||
|
||||
import { TravelPath } from "./TravelPath";
|
||||
import { LeftSidebar } from "./LeftSidebar";
|
||||
import { RightSidebar } from "./RightSidebar";
|
||||
|
||||
import { coordinatesToLocal } from "./utils";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { languageStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Sight } from "./Sight";
|
||||
import { SightData } from "./types";
|
||||
import { Station } from "./Station";
|
||||
import { UP_SCALE } from "./Constants";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { WebGLRouteMapPrototype } from "./webgl-prototype/WebGLRouteMapPrototype";
|
||||
import { CircularProgress } from "@mui/material";
|
||||
|
||||
extend({
|
||||
Container,
|
||||
@@ -42,7 +36,7 @@ const Loading = () => {
|
||||
|
||||
if (isRouteLoading || isStationLoading || isSightLoading) {
|
||||
return (
|
||||
<div className="fixed flex z-1 items-center justify-center h-screen w-screen bg-[#111]">
|
||||
<div className="fixed flex z-1000000000 items-center justify-center h-screen w-screen bg-[#111]">
|
||||
<CircularProgress />
|
||||
</div>
|
||||
);
|
||||
@@ -51,14 +45,33 @@ const Loading = () => {
|
||||
return null;
|
||||
};
|
||||
export const RoutePreview = () => {
|
||||
const { routeData, stationData, sightData } = useMapData();
|
||||
const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(true);
|
||||
return (
|
||||
<MapDataProvider>
|
||||
<TransformProvider>
|
||||
<Stack direction="row" height="100vh" width="100vw" overflow="hidden">
|
||||
{routeData && stationData && sightData ? <LanguageSwitcher /> : null}
|
||||
<Loading />
|
||||
<LeftSidebar />
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: isLeftSidebarOpen ? 300 : 0,
|
||||
transition: "width 0.3s ease",
|
||||
overflow: "visible",
|
||||
height: "100%",
|
||||
bgcolor: "primary.main",
|
||||
borderRight: isLeftSidebarOpen
|
||||
? "1px solid rgba(255,255,255,0.08)"
|
||||
: "none",
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<LeftSidebar
|
||||
open={isLeftSidebarOpen}
|
||||
onToggle={() => setIsLeftSidebarOpen((prev) => !prev)}
|
||||
/>
|
||||
</Box>
|
||||
<Stack direction="row" flex={1} position="relative" height="100%">
|
||||
<RouteMap />
|
||||
<Widgets />
|
||||
@@ -71,15 +84,8 @@ export const RoutePreview = () => {
|
||||
};
|
||||
|
||||
export const RouteMap = observer(() => {
|
||||
const { language } = languageStore;
|
||||
const { setPosition, setTransform, screenCenter } = useTransform();
|
||||
const {
|
||||
routeData,
|
||||
stationData,
|
||||
sightData,
|
||||
originalRouteData,
|
||||
originalSightData,
|
||||
} = useMapData();
|
||||
const { routeData, stationData, sightData, originalRouteData } = useMapData();
|
||||
|
||||
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
|
||||
const [isSetup, setIsSetup] = useState(false);
|
||||
@@ -165,8 +171,7 @@ export const RouteMap = observer(() => {
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%" }} ref={parentRef}>
|
||||
<LanguageSwitcher />
|
||||
<Application resizeTo={parentRef} background="#fff" preference="webgl">
|
||||
{/* <Application resizeTo={parentRef} background="#000" preference="webgl">
|
||||
<InfiniteCanvas>
|
||||
<TravelPath points={points} />
|
||||
{stationData[language].map((obj, index) => (
|
||||
@@ -184,7 +189,8 @@ export const RouteMap = observer(() => {
|
||||
return <Sight sight={sight} id={index} key={sight.id} />;
|
||||
})}
|
||||
</InfiniteCanvas>
|
||||
</Application>
|
||||
</Application> */}
|
||||
<WebGLRouteMapPrototype />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ export interface RouteData {
|
||||
carrier_id: number;
|
||||
center_latitude: number;
|
||||
center_longitude: number;
|
||||
icon_size: number;
|
||||
font_size: number;
|
||||
governor_appeal: number;
|
||||
id: number;
|
||||
path: [number, number][];
|
||||
|
||||
203
src/pages/Route/route-preview/web-gl/LanguageSelector.tsx
Normal file
203
src/pages/Route/route-preview/web-gl/LanguageSelector.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useEffect, useRef, useState, type ReactElement } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { languageStore } from "@shared";
|
||||
|
||||
const LANGUAGES = ["ru", "zh", "en"] as const;
|
||||
type Language = (typeof LANGUAGES)[number];
|
||||
|
||||
type LanguageSelectorProps = {
|
||||
onBack?: () => void;
|
||||
isSidebarOpen?: boolean;
|
||||
};
|
||||
|
||||
const renderLanguageIcon = (lang: Language): ReactElement => {
|
||||
switch (lang) {
|
||||
case "ru":
|
||||
return (
|
||||
<svg
|
||||
className="h-12 w-12 cursor-pointer"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="3" y="3" width="42" height="42" rx="21" fill="black" />
|
||||
<path
|
||||
d="M24 0C10.75 0 0 10.75 0 24C0 37.25 10.75 48 24 48C37.25 48 48 37.25 48 24C48 10.75 37.25 0 24 0ZM24.2 33.55H19.92L16.29 26.46H13.11V33.55H9.12V14.18H16.32C18.61 14.18 20.37 14.69 21.62 15.71C22.87 16.73 23.48 18.17 23.48 20.03C23.48 21.35 23.19 22.45 22.62 23.34C22.05 24.22 21.18 24.93 20.02 25.45L24.21 33.37V33.56L24.2 33.55ZM40.3 26.94C40.3 29.06 39.64 30.74 38.31 31.97C36.98 33.2 35.17 33.82 32.87 33.82C30.57 33.82 28.81 33.22 27.48 32.02C26.15 30.82 25.47 29.18 25.44 27.08V14.18H29.43V26.97C29.43 28.24 29.73 29.16 30.34 29.74C30.95 30.32 31.79 30.61 32.86 30.61C35.1 30.61 36.24 29.43 36.28 27.07V14.18H40.28V26.94H40.3Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M16.3086 17.4099H13.0986V23.2199H16.3186C17.3186 23.2199 18.0986 22.9599 18.6486 22.4499C19.1986 21.9399 19.4686 21.2399 19.4686 20.3399C19.4686 19.4399 19.2086 18.7099 18.6886 18.1799C18.1686 17.6499 17.3686 17.3899 16.2986 17.3899L16.3086 17.4099Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case "zh":
|
||||
return (
|
||||
<svg
|
||||
className="h-12 w-12 cursor-pointer"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="3" y="3" width="42" height="42" rx="21" fill="black" />
|
||||
<path d="M10.287 20.382H6.291V24.147H10.287V20.382Z" fill={"white"} />
|
||||
<path
|
||||
d="M13.704 24.147H17.721V20.382H13.704V24.147Z"
|
||||
fill={"white"}
|
||||
/>
|
||||
<path
|
||||
d="M36.1254 20.046H29.8575C30.6606 21.9406 31.7187 23.6442 33.0513 25.1217C34.3105 23.6927 35.3315 22.0126 36.1254 20.046Z"
|
||||
fill={"white"}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M24 48C37.2548 48 48 37.2548 48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24C0 37.2548 10.7452 48 24 48ZM10.287 13.5H13.704V17.1541H21.117V28.446H17.721V27.375H13.704V33.969H10.287V27.375H6.291V28.5511H3V17.1541H10.287V13.5ZM31.35 13.5H34.704V16.8181H43.083V20.046H39.8887C38.804 22.9506 37.3746 25.3834 35.581 27.4065C37.6488 28.9237 40.1651 30.0542 43.1682 30.7291L43.8469 30.8817L43.3465 31.3649C42.7753 31.9162 41.9777 33.0771 41.5886 33.7939L41.4484 34.0521L41.1642 33.9778C37.8385 33.1088 35.1249 31.7521 32.8974 29.9253C30.6296 31.6954 27.9389 33.0335 24.802 34.015L24.4889 34.1129L24.3502 33.8156C24.0724 33.2203 23.2933 32.029 22.8051 31.439L22.4307 30.9868L22.9986 30.8373C25.936 30.0648 28.4025 28.9702 30.4373 27.4935C28.6775 25.4061 27.319 22.9142 26.2412 20.046H23.097V16.8181H31.35V13.5Z"
|
||||
fill={"white"}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case "en":
|
||||
default:
|
||||
return (
|
||||
<svg
|
||||
className="h-12 w-12 cursor-pointer"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="3" y="3" width="42" height="42" rx="21" fill="black" />
|
||||
<path
|
||||
d="M24 0C10.75 0 0 10.75 0 24C0 37.25 10.75 48 24 48C37.25 48 48 37.25 48 24C48 10.75 37.25 0 24 0ZM21.57 33.79H8.41V14.15H21.55V17.43H12.45V22.11H20.22V25.28H12.45V30.54H21.57V33.79ZM39.54 33.79H35.49L27.61 20.87V33.79H23.56V14.15H27.61L35.5 27.1V14.15H39.53V33.79H39.54Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const CollapsedIcon = () => (
|
||||
<svg
|
||||
className="h-12 w-12 cursor-pointer"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="4" y="3" width="39" height="42" rx="19.5" fill="black" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0 24C0 10.75 10.75 0 24 0C37.25 0 48 10.75 48 24C48 37.25 37.25 48 24 48C10.75 48 0 37.25 0 24ZM36.05 19.41L38.27 25.47L40.47 19.39L39.17 19.77C39.16 19.75 39.16 19.35 39.16 19.32C38.58 12.41 32.99 7.15 25.94 7.15C25.42 7.15 24.99 7.58 24.99 8.1C24.99 8.62 25.42 9.05 25.94 9.05C31.92 9.05 36.68 13.47 37.26 19.31C37.26 19.325 37.2625 19.435 37.265 19.545C37.2675 19.655 37.27 19.765 37.27 19.78L36.05 19.41ZM8.90375 27.5369C11.3535 26.9568 13.6332 25.8438 15.5701 24.2838L15.5709 24.2846C17.2124 25.8526 19.2497 26.9784 21.4812 27.5521C21.5875 27.5649 21.6945 27.5649 21.8007 27.5521C22.3194 27.6074 22.8298 27.3911 23.1385 26.9848C23.448 26.5786 23.5086 26.0441 23.2986 25.5826C23.0879 25.1211 22.6389 24.803 22.1202 24.7477C20.4804 24.3182 18.9808 23.4937 17.7634 22.3495C20.2489 20.0131 22.1036 17.1245 23.1659 13.9363C23.2729 13.5077 23.165 13.0566 22.8754 12.716C22.6016 12.3627 22.1709 12.1552 21.7136 12.1552H16.6307V10.402C16.6307 9.90121 16.3535 9.43889 15.9045 9.1881C15.4556 8.9373 14.9012 8.9373 14.4522 9.1881C14.0033 9.43809 13.7261 9.90121 13.7261 10.402V12.1824H8.64317C8.12367 12.1824 7.64484 12.45 7.38509 12.8835C7.12534 13.317 7.12534 13.8522 7.38509 14.2857C7.64484 14.7192 8.1245 14.9868 8.64317 14.9868H19.6655C18.6804 16.9971 17.3443 18.828 15.7153 20.3993C14.8373 19.3977 14.0688 18.3128 13.4207 17.1598C13.2747 16.7896 12.9734 16.4972 12.5909 16.3529C12.2083 16.2087 11.7809 16.2279 11.4141 16.405C11.0473 16.5821 10.7751 16.901 10.6647 17.2824C10.5544 17.6638 10.6166 18.0724 10.8357 18.4074C11.6058 19.7423 12.5153 20.997 13.551 22.1516C12.0207 23.3728 10.2307 24.2533 8.30873 24.7317C7.92284 24.7709 7.56932 24.956 7.32617 25.2469C7.08219 25.5369 6.96767 25.9095 7.00833 26.2813C7.04899 26.6539 7.24069 26.9952 7.54193 27.23C7.84235 27.4656 8.22824 27.5761 8.61329 27.5369C8.70956 27.5513 8.80748 27.5513 8.90375 27.5369ZM34.9002 38.8803C35.2512 39.0301 35.6487 39.0397 36.0072 38.9067C36.3815 38.7865 36.6886 38.5237 36.8587 38.18C37.0288 37.8362 37.0462 37.4404 36.9077 37.0839L30.3434 20.7767C30.238 20.5131 30.0529 20.2863 29.8115 20.1261C29.57 19.9658 29.2845 19.8793 28.9924 19.8785C28.7019 19.8785 28.4173 19.9626 28.1766 20.1196C27.9359 20.2775 27.7501 20.501 27.6422 20.7623L21.136 36.523C20.9443 36.9885 21.024 37.5181 21.346 37.9116C21.668 38.305 22.1825 38.5029 22.6962 38.4308C23.2107 38.3579 23.6455 38.0269 23.8372 37.5606L25.4057 33.6489H32.3185L34.1342 38.1079C34.2737 38.4524 34.5492 38.7304 34.9002 38.8803ZM28.9052 25.1652L31.1857 30.8445H31.1849H26.5667L28.9052 25.1652Z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ArrowIcon = ({ rotation }: { rotation: number }) => (
|
||||
<svg
|
||||
style={{
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
transition: "transform 0.15s ease",
|
||||
}}
|
||||
className="h-12 w-12"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="8" y="7" width="31" height="33" fill="black" />
|
||||
<path
|
||||
d="M24.0001 0C10.7501 0 0.00012207 10.75 0.00012207 24C0.00012207 37.25 10.7501 48 24.0001 48C37.2501 48 48.0001 37.25 48.0001 24C48.0001 10.75 37.2501 0 24.0001 0ZM37.5401 25.84C37.5401 26.4 37.0901 26.85 36.5301 26.85H20.5901C20.1401 26.85 19.9201 27.39 20.2301 27.71L27.6801 35.16C28.0801 35.56 28.0801 36.2 27.6801 36.59L25.0801 39.19C24.6801 39.59 24.0401 39.59 23.6501 39.19L12.4901 28.03L9.17012 24.71C8.77012 24.31 8.77012 23.67 9.17012 23.28L12.4901 19.96L23.6501 8.8C24.0501 8.4 24.6901 8.4 25.0801 8.8L27.6801 11.4C28.0801 11.8 28.0801 12.44 27.6801 12.83L20.2301 20.28C19.9101 20.6 20.1401 21.14 20.5901 21.14H36.5301C37.0901 21.14 37.5401 21.59 37.5401 22.15V25.82V25.84Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LanguageSelector = observer(
|
||||
({ onBack, isSidebarOpen = true }: LanguageSelectorProps) => {
|
||||
const { setLanguage } = languageStore;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleOutside = (event: PointerEvent) => {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("pointerdown", handleOutside);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handleOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSelect = (code: Language) => {
|
||||
setLanguage(code);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const toggle = () => setIsOpen((prev) => !prev);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="pointer-events-auto"
|
||||
style={{
|
||||
width: "500px",
|
||||
transition: "width 0.25s ease",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 ">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onBack?.();
|
||||
}}
|
||||
className="flex h-12 w-12 items-center justify-center"
|
||||
aria-label={
|
||||
isOpen ? "Скрыть выбор языка" : "Показать выбор языка"
|
||||
}
|
||||
>
|
||||
<ArrowIcon rotation={isSidebarOpen ? 0 : 180} />
|
||||
</button>
|
||||
{isOpen ? (
|
||||
LANGUAGES.map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleSelect(lang);
|
||||
}}
|
||||
className="flex h-12 w-12 items-center justify-center"
|
||||
aria-label={`Переключить язык на ${lang.toUpperCase()}`}
|
||||
>
|
||||
{renderLanguageIcon(lang)}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
className="flex h-12 w-12 items-center justify-center"
|
||||
onClick={toggle}
|
||||
>
|
||||
<CollapsedIcon />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default LanguageSelector;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
|
||||
import {
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
@@ -16,11 +15,21 @@ import {
|
||||
TableRow,
|
||||
Paper,
|
||||
TableBody,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Tabs,
|
||||
Tab,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
|
||||
import { authInstance, languageStore, selectedCityStore } from "@shared";
|
||||
import {
|
||||
AnimatedCircleButton,
|
||||
authInstance,
|
||||
languageStore,
|
||||
selectedCityStore,
|
||||
} from "@shared";
|
||||
|
||||
type Field<T> = {
|
||||
label: string;
|
||||
@@ -93,12 +102,26 @@ const LinkedStationsContentsInner = <
|
||||
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedToDetach, setSelectedToDetach] = useState<Set<number>>(
|
||||
new Set()
|
||||
);
|
||||
const [isLinkingSingle, setIsLinkingSingle] = useState(false);
|
||||
const [isLinkingBulk, setIsLinkingBulk] = useState(false);
|
||||
const [isBulkDetaching, setIsBulkDetaching] = useState(false);
|
||||
const [detachingIds, setDetachingIds] = useState<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {}, [error]);
|
||||
|
||||
const parentResource = "sight";
|
||||
const childResource = "station";
|
||||
|
||||
const buildPayload = (ids: number[]) => ({
|
||||
[`${childResource}_ids`]: ids,
|
||||
});
|
||||
|
||||
const availableItems = allItems
|
||||
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||
.filter((item) => {
|
||||
@@ -110,6 +133,14 @@ const LinkedStationsContentsInner = <
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const filteredAvailableItems = availableItems.filter((item) => {
|
||||
if (!searchQuery.trim()) return true;
|
||||
const query = searchQuery.toLowerCase();
|
||||
const name = String(item.name || "").toLowerCase();
|
||||
const description = String(item.description || "").toLowerCase();
|
||||
return name.includes(query) || description.includes(query);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedLinkedItems) {
|
||||
setLinkedItems(updatedLinkedItems);
|
||||
@@ -120,13 +151,24 @@ const LinkedStationsContentsInner = <
|
||||
setItemsParent?.(linkedItems);
|
||||
}, [linkedItems, setItemsParent]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedToDetach((prev) => {
|
||||
const updated = new Set<number>();
|
||||
linkedItems.forEach((item) => {
|
||||
if (prev.has(item.id)) {
|
||||
updated.add(item.id);
|
||||
}
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}, [linkedItems]);
|
||||
|
||||
const linkItem = () => {
|
||||
if (selectedItemId !== null) {
|
||||
setError(null);
|
||||
const requestData = {
|
||||
station_id: selectedItemId,
|
||||
};
|
||||
const requestData = buildPayload([selectedItemId]);
|
||||
|
||||
setIsLinkingSingle(true);
|
||||
authInstance
|
||||
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
||||
.then(() => {
|
||||
@@ -140,15 +182,23 @@ const LinkedStationsContentsInner = <
|
||||
.catch((error) => {
|
||||
console.error("Error linking station:", error);
|
||||
setError("Failed to link station");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLinkingSingle(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = (itemId: number) => {
|
||||
setError(null);
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(itemId);
|
||||
return next;
|
||||
});
|
||||
authInstance
|
||||
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
data: { [`${childResource}_id`]: itemId },
|
||||
data: buildPayload([itemId]),
|
||||
})
|
||||
.then(() => {
|
||||
setLinkedItems(linkedItems.filter((item) => item.id !== itemId));
|
||||
@@ -157,9 +207,125 @@ const LinkedStationsContentsInner = <
|
||||
.catch((error) => {
|
||||
console.error("Error deleting station:", error);
|
||||
setError("Failed to delete station");
|
||||
})
|
||||
.finally(() => {
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(itemId);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (itemId: number) => {
|
||||
const updated = new Set(selectedItems);
|
||||
if (updated.has(itemId)) {
|
||||
updated.delete(itemId);
|
||||
} else {
|
||||
updated.add(itemId);
|
||||
}
|
||||
setSelectedItems(updated);
|
||||
};
|
||||
|
||||
const handleBulkLink = async () => {
|
||||
if (selectedItems.size === 0) return;
|
||||
setError(null);
|
||||
|
||||
setIsLinkingBulk(true);
|
||||
const idsToLink = Array.from(selectedItems);
|
||||
|
||||
try {
|
||||
await authInstance.post(
|
||||
`/${parentResource}/${parentId}/${childResource}`,
|
||||
buildPayload(idsToLink)
|
||||
);
|
||||
|
||||
const newItems = allItems.filter((item) => idsToLink.includes(item.id));
|
||||
setLinkedItems((prev) => {
|
||||
const existingIds = new Set(prev.map((item) => item.id));
|
||||
const additions = newItems.filter((item) => !existingIds.has(item.id));
|
||||
return [...prev, ...additions];
|
||||
});
|
||||
setSelectedItems((prev) => {
|
||||
const remaining = new Set(prev);
|
||||
idsToLink.forEach((id) => remaining.delete(id));
|
||||
return remaining;
|
||||
});
|
||||
onUpdate?.();
|
||||
} catch (error) {
|
||||
console.error("Error linking stations:", error);
|
||||
setError("Failed to link stations");
|
||||
}
|
||||
|
||||
setIsLinkingBulk(false);
|
||||
};
|
||||
|
||||
const toggleDetachSelection = (itemId: number) => {
|
||||
const updated = new Set(selectedToDetach);
|
||||
if (updated.has(itemId)) {
|
||||
updated.delete(itemId);
|
||||
} else {
|
||||
updated.add(itemId);
|
||||
}
|
||||
setSelectedToDetach(updated);
|
||||
};
|
||||
|
||||
const handleToggleAllDetach = (checked: boolean) => {
|
||||
if (!checked) {
|
||||
setSelectedToDetach(new Set());
|
||||
return;
|
||||
}
|
||||
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
||||
};
|
||||
|
||||
const handleBulkDetach = async () => {
|
||||
const idsToDetach = Array.from(selectedToDetach);
|
||||
if (idsToDetach.length === 0) return;
|
||||
setError(null);
|
||||
|
||||
setIsBulkDetaching(true);
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
idsToDetach.forEach((id) => next.add(id));
|
||||
return next;
|
||||
});
|
||||
|
||||
try {
|
||||
await authInstance.delete(
|
||||
`/${parentResource}/${parentId}/${childResource}`,
|
||||
{
|
||||
data: buildPayload(idsToDetach),
|
||||
}
|
||||
);
|
||||
|
||||
setLinkedItems((prev) =>
|
||||
prev.filter((item) => !idsToDetach.includes(item.id))
|
||||
);
|
||||
setSelectedToDetach((prev) => {
|
||||
const remaining = new Set(prev);
|
||||
idsToDetach.forEach((id) => remaining.delete(id));
|
||||
return remaining;
|
||||
});
|
||||
onUpdate?.();
|
||||
} catch (error) {
|
||||
console.error("Error deleting stations:", error);
|
||||
setError("Failed to delete stations");
|
||||
}
|
||||
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
idsToDetach.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
setIsBulkDetaching(false);
|
||||
};
|
||||
|
||||
const allSelectedForDetach =
|
||||
linkedItems.length > 0 &&
|
||||
linkedItems.every((item) => selectedToDetach.has(item.id));
|
||||
const isIndeterminateDetach =
|
||||
selectedToDetach.size > 0 && !allSelectedForDetach;
|
||||
|
||||
useEffect(() => {
|
||||
if (parentId) {
|
||||
setIsLoading(true);
|
||||
@@ -203,6 +369,16 @@ const LinkedStationsContentsInner = <
|
||||
<Table sx={{ width: "100%" }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{type === "edit" && (
|
||||
<TableCell width="50px">
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={allSelectedForDetach}
|
||||
indeterminate={isIndeterminateDetach}
|
||||
onChange={(e) => handleToggleAllDetach(e.target.checked)}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell key="id" width="60px">
|
||||
№
|
||||
</TableCell>
|
||||
@@ -218,6 +394,15 @@ const LinkedStationsContentsInner = <
|
||||
<TableBody>
|
||||
{linkedItems.map((item, index) => (
|
||||
<TableRow key={item.id} hover>
|
||||
{type === "edit" && (
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selectedToDetach.has(item.id)}
|
||||
onChange={() => toggleDetachSelection(item.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
{fields.map((field, idx) => (
|
||||
<TableCell key={String(field.data) + String(idx)}>
|
||||
@@ -228,7 +413,7 @@ const LinkedStationsContentsInner = <
|
||||
))}
|
||||
{type === "edit" && (
|
||||
<TableCell>
|
||||
<Button
|
||||
<AnimatedCircleButton
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
@@ -236,9 +421,11 @@ const LinkedStationsContentsInner = <
|
||||
e.stopPropagation();
|
||||
deleteItem(item.id);
|
||||
}}
|
||||
disabled={detachingIds.has(item.id)}
|
||||
loading={detachingIds.has(item.id)}
|
||||
>
|
||||
Отвязать
|
||||
</Button>
|
||||
</AnimatedCircleButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
@@ -248,6 +435,20 @@ const LinkedStationsContentsInner = <
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{type === "edit" && linkedItems.length > 0 && (
|
||||
<Stack direction="row" gap={2} mt={2}>
|
||||
<AnimatedCircleButton
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleBulkDetach}
|
||||
disabled={selectedToDetach.size === 0 || isBulkDetaching}
|
||||
loading={isBulkDetaching}
|
||||
>
|
||||
Отвязать выбранные ({selectedToDetach.size})
|
||||
</AnimatedCircleButton>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{linkedItems.length === 0 && !isLoading && (
|
||||
<Typography color="textSecondary" textAlign="center" py={2}>
|
||||
Остановки не найдены
|
||||
@@ -256,48 +457,145 @@ const LinkedStationsContentsInner = <
|
||||
|
||||
{type === "edit" && !disableCreation && (
|
||||
<Stack gap={2} mt={2}>
|
||||
<Typography variant="subtitle1">Добавить остановку</Typography>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
value={
|
||||
availableItems?.find((item) => item.id === selectedItemId) || null
|
||||
}
|
||||
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
|
||||
options={availableItems}
|
||||
getOptionLabel={(item) => String(item.name)}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Выберите остановку" fullWidth />
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) => option.id === value?.id}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
const searchWords = inputValue
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter(Boolean);
|
||||
return options.filter((option) => {
|
||||
const optionWords = String(option.name)
|
||||
.toLowerCase()
|
||||
.split(" ");
|
||||
return searchWords.every((searchWord) =>
|
||||
optionWords.some((word) => word.startsWith(searchWord))
|
||||
);
|
||||
});
|
||||
}}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} key={option.id}>
|
||||
{String(option.name)}
|
||||
</li>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={linkItem}
|
||||
disabled={!selectedItemId}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
<Typography variant="subtitle1">Добавить остановки</Typography>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, value) => setActiveTab(value)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
<Tab label="По одной" />
|
||||
<Tab label="Массово" />
|
||||
</Tabs>
|
||||
|
||||
<Box sx={{ mt: 1 }}>
|
||||
{activeTab === 0 && (
|
||||
<Stack gap={2}>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
value={
|
||||
availableItems?.find(
|
||||
(item) => item.id === selectedItemId
|
||||
) || null
|
||||
}
|
||||
onChange={(_, newValue) =>
|
||||
setSelectedItemId(newValue?.id || null)
|
||||
}
|
||||
options={availableItems}
|
||||
getOptionLabel={(item) => String(item.name)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Выберите остановку"
|
||||
placeholder="Введите название или описание остановки..."
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
option.id === value?.id
|
||||
}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
if (!inputValue.trim()) return options;
|
||||
const query = inputValue.toLowerCase();
|
||||
return options.filter((option) => {
|
||||
const name = String(option.name || "").toLowerCase();
|
||||
const description = String(
|
||||
option.description || ""
|
||||
).toLowerCase();
|
||||
return (
|
||||
name.includes(query) || description.includes(query)
|
||||
);
|
||||
});
|
||||
}}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} key={option.id}>
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<p>{String(option.name)}</p>
|
||||
<p className="text-xs text-gray-500 max-w-[300px] mr-4 truncate">
|
||||
{String(option.description)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AnimatedCircleButton
|
||||
variant="contained"
|
||||
onClick={linkItem}
|
||||
disabled={!selectedItemId || isLinkingSingle}
|
||||
loading={isLinkingSingle}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Добавить
|
||||
</AnimatedCircleButton>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && (
|
||||
<Stack gap={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Поиск остановок"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Введите название или описание остановки..."
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<Paper sx={{ maxHeight: 300, overflow: "auto", p: 2 }}>
|
||||
<Stack gap={1}>
|
||||
{filteredAvailableItems.map((item) => (
|
||||
<FormControlLabel
|
||||
key={item.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={selectedItems.has(item.id)}
|
||||
onChange={() => handleCheckboxChange(item.id)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div className="flex justify-between items-center w-full gap-10">
|
||||
<p>{String(item.name)}</p>
|
||||
<p className="text-xs text-gray-500 max-w-[300px] truncate text-ellipsis">
|
||||
{String(item.description)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
sx={{
|
||||
margin: 0,
|
||||
"& .MuiFormControlLabel-label": {
|
||||
fontSize: "0.9rem",
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{filteredAvailableItems.length === 0 && (
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
textAlign="center"
|
||||
py={1}
|
||||
>
|
||||
{searchQuery.trim()
|
||||
? "Остановки не найдены"
|
||||
: "Нет доступных остановок"}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<AnimatedCircleButton
|
||||
variant="contained"
|
||||
onClick={handleBulkLink}
|
||||
disabled={selectedItems.size === 0 || isLinkingBulk}
|
||||
loading={isLinkingBulk}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Добавить выбранные ({selectedItems.size})
|
||||
</AnimatedCircleButton>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { runInAction } from "mobx";
|
||||
|
||||
export const SnapshotCreatePage = observer(() => {
|
||||
const { createSnapshot } = snapshotStore;
|
||||
const { createSnapshot, getSnapshotStatus, snapshotStatus } = snapshotStore;
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -42,10 +43,24 @@ export const SnapshotCreatePage = observer(() => {
|
||||
onClick={async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await createSnapshot(name);
|
||||
setIsLoading(false);
|
||||
toast.success("Снапшот успешно создан");
|
||||
navigate(-1);
|
||||
const id = await createSnapshot(name);
|
||||
|
||||
await getSnapshotStatus(id);
|
||||
|
||||
while (snapshotStore.snapshotStatus?.Status != "done") {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await getSnapshotStatus(id);
|
||||
}
|
||||
|
||||
if (snapshotStore.snapshotStatus?.Status === "done") {
|
||||
toast.success("Снапшот успешно создан");
|
||||
|
||||
runInAction(() => {
|
||||
snapshotStore.snapshotStatus = null;
|
||||
});
|
||||
|
||||
navigate(-1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Ошибка при создании снапшота");
|
||||
@@ -56,7 +71,15 @@ export const SnapshotCreatePage = observer(() => {
|
||||
disabled={isLoading || !name.trim()}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
<span>
|
||||
{snapshotStatus?.Progress
|
||||
? (snapshotStatus.Progress * 100).toFixed(2)
|
||||
: 0}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
"Сохранить"
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
|
||||
import {
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
@@ -16,11 +15,21 @@ import {
|
||||
TableRow,
|
||||
Paper,
|
||||
TableBody,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Tabs,
|
||||
Tab,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
|
||||
import { authInstance, languageStore, selectedCityStore } from "@shared";
|
||||
import {
|
||||
AnimatedCircleButton,
|
||||
authInstance,
|
||||
languageStore,
|
||||
selectedCityStore,
|
||||
} from "@shared";
|
||||
|
||||
type Field<T> = {
|
||||
label: string;
|
||||
@@ -93,12 +102,26 @@ const LinkedSightsContentsInner = <
|
||||
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedToDetach, setSelectedToDetach] = useState<Set<number>>(
|
||||
new Set()
|
||||
);
|
||||
const [isLinkingSingle, setIsLinkingSingle] = useState(false);
|
||||
const [isLinkingBulk, setIsLinkingBulk] = useState(false);
|
||||
const [isBulkDetaching, setIsBulkDetaching] = useState(false);
|
||||
const [detachingIds, setDetachingIds] = useState<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {}, [error]);
|
||||
|
||||
const parentResource = "station";
|
||||
const childResource = "sight";
|
||||
|
||||
const buildPayload = (ids: number[]) => ({
|
||||
[`${childResource}_ids`]: ids,
|
||||
});
|
||||
|
||||
const availableItems = allItems
|
||||
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||
.filter((item) => {
|
||||
@@ -111,6 +134,11 @@ const LinkedSightsContentsInner = <
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const filteredAvailableItems = availableItems.filter((item) => {
|
||||
if (!searchQuery.trim()) return true;
|
||||
return String(item.name).toLowerCase().includes(searchQuery.toLowerCase());
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedLinkedItems) {
|
||||
setLinkedItems(updatedLinkedItems);
|
||||
@@ -121,13 +149,24 @@ const LinkedSightsContentsInner = <
|
||||
setItemsParent?.(linkedItems);
|
||||
}, [linkedItems, setItemsParent]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedToDetach((prev) => {
|
||||
const updated = new Set<number>();
|
||||
linkedItems.forEach((item) => {
|
||||
if (prev.has(item.id)) {
|
||||
updated.add(item.id);
|
||||
}
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}, [linkedItems]);
|
||||
|
||||
const linkItem = () => {
|
||||
if (selectedItemId !== null) {
|
||||
setError(null);
|
||||
const requestData = {
|
||||
sight_id: selectedItemId,
|
||||
};
|
||||
const requestData = buildPayload([selectedItemId]);
|
||||
|
||||
setIsLinkingSingle(true);
|
||||
authInstance
|
||||
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
||||
.then(() => {
|
||||
@@ -141,15 +180,23 @@ const LinkedSightsContentsInner = <
|
||||
.catch((error) => {
|
||||
console.error("Error linking sight:", error);
|
||||
setError("Failed to link sight");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLinkingSingle(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = (itemId: number) => {
|
||||
setError(null);
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(itemId);
|
||||
return next;
|
||||
});
|
||||
authInstance
|
||||
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
data: { [`${childResource}_id`]: itemId },
|
||||
data: buildPayload([itemId]),
|
||||
})
|
||||
.then(() => {
|
||||
setLinkedItems(linkedItems.filter((item) => item.id !== itemId));
|
||||
@@ -158,9 +205,125 @@ const LinkedSightsContentsInner = <
|
||||
.catch((error) => {
|
||||
console.error("Error deleting sight:", error);
|
||||
setError("Failed to delete sight");
|
||||
})
|
||||
.finally(() => {
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(itemId);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (itemId: number) => {
|
||||
const updated = new Set(selectedItems);
|
||||
if (updated.has(itemId)) {
|
||||
updated.delete(itemId);
|
||||
} else {
|
||||
updated.add(itemId);
|
||||
}
|
||||
setSelectedItems(updated);
|
||||
};
|
||||
|
||||
const handleBulkLink = async () => {
|
||||
if (selectedItems.size === 0) return;
|
||||
setError(null);
|
||||
|
||||
setIsLinkingBulk(true);
|
||||
const idsToLink = Array.from(selectedItems);
|
||||
|
||||
try {
|
||||
await authInstance.post(
|
||||
`/${parentResource}/${parentId}/${childResource}`,
|
||||
buildPayload(idsToLink)
|
||||
);
|
||||
|
||||
const newItems = allItems.filter((item) => idsToLink.includes(item.id));
|
||||
setLinkedItems((prev) => {
|
||||
const existingIds = new Set(prev.map((item) => item.id));
|
||||
const additions = newItems.filter((item) => !existingIds.has(item.id));
|
||||
return [...prev, ...additions];
|
||||
});
|
||||
setSelectedItems((prev) => {
|
||||
const remaining = new Set(prev);
|
||||
idsToLink.forEach((id) => remaining.delete(id));
|
||||
return remaining;
|
||||
});
|
||||
onUpdate?.();
|
||||
} catch (error) {
|
||||
console.error("Error linking sights:", error);
|
||||
setError("Failed to link sights");
|
||||
}
|
||||
|
||||
setIsLinkingBulk(false);
|
||||
};
|
||||
|
||||
const toggleDetachSelection = (itemId: number) => {
|
||||
const updated = new Set(selectedToDetach);
|
||||
if (updated.has(itemId)) {
|
||||
updated.delete(itemId);
|
||||
} else {
|
||||
updated.add(itemId);
|
||||
}
|
||||
setSelectedToDetach(updated);
|
||||
};
|
||||
|
||||
const handleToggleAllDetach = (checked: boolean) => {
|
||||
if (!checked) {
|
||||
setSelectedToDetach(new Set());
|
||||
return;
|
||||
}
|
||||
setSelectedToDetach(new Set(linkedItems.map((item) => item.id)));
|
||||
};
|
||||
|
||||
const handleBulkDetach = async () => {
|
||||
const idsToDetach = Array.from(selectedToDetach);
|
||||
if (idsToDetach.length === 0) return;
|
||||
setError(null);
|
||||
|
||||
setIsBulkDetaching(true);
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
idsToDetach.forEach((id) => next.add(id));
|
||||
return next;
|
||||
});
|
||||
|
||||
try {
|
||||
await authInstance.delete(
|
||||
`/${parentResource}/${parentId}/${childResource}`,
|
||||
{
|
||||
data: buildPayload(idsToDetach),
|
||||
}
|
||||
);
|
||||
|
||||
setLinkedItems((prev) =>
|
||||
prev.filter((item) => !idsToDetach.includes(item.id))
|
||||
);
|
||||
setSelectedToDetach((prev) => {
|
||||
const remaining = new Set(prev);
|
||||
idsToDetach.forEach((id) => remaining.delete(id));
|
||||
return remaining;
|
||||
});
|
||||
onUpdate?.();
|
||||
} catch (error) {
|
||||
console.error("Error deleting sights:", error);
|
||||
setError("Failed to delete sights");
|
||||
}
|
||||
|
||||
setDetachingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
idsToDetach.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
setIsBulkDetaching(false);
|
||||
};
|
||||
|
||||
const allSelectedForDetach =
|
||||
linkedItems.length > 0 &&
|
||||
linkedItems.every((item) => selectedToDetach.has(item.id));
|
||||
const isIndeterminateDetach =
|
||||
selectedToDetach.size > 0 && !allSelectedForDetach;
|
||||
|
||||
useEffect(() => {
|
||||
if (parentId) {
|
||||
setIsLoading(true);
|
||||
@@ -204,6 +367,16 @@ const LinkedSightsContentsInner = <
|
||||
<Table sx={{ width: "100%" }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{type === "edit" && (
|
||||
<TableCell width="50px">
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={allSelectedForDetach}
|
||||
indeterminate={isIndeterminateDetach}
|
||||
onChange={(e) => handleToggleAllDetach(e.target.checked)}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell key="id" width="60px">
|
||||
№
|
||||
</TableCell>
|
||||
@@ -219,6 +392,15 @@ const LinkedSightsContentsInner = <
|
||||
<TableBody>
|
||||
{linkedItems.map((item, index) => (
|
||||
<TableRow key={item.id} hover>
|
||||
{type === "edit" && (
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selectedToDetach.has(item.id)}
|
||||
onChange={() => toggleDetachSelection(item.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
{fields.map((field, idx) => (
|
||||
<TableCell key={String(field.data) + String(idx)}>
|
||||
@@ -229,7 +411,7 @@ const LinkedSightsContentsInner = <
|
||||
))}
|
||||
{type === "edit" && (
|
||||
<TableCell>
|
||||
<Button
|
||||
<AnimatedCircleButton
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
@@ -237,9 +419,11 @@ const LinkedSightsContentsInner = <
|
||||
e.stopPropagation();
|
||||
deleteItem(item.id);
|
||||
}}
|
||||
disabled={detachingIds.has(item.id)}
|
||||
loading={detachingIds.has(item.id)}
|
||||
>
|
||||
Отвязать
|
||||
</Button>
|
||||
</AnimatedCircleButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
@@ -249,6 +433,20 @@ const LinkedSightsContentsInner = <
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{type === "edit" && linkedItems.length > 0 && (
|
||||
<Stack direction="row" gap={2} mt={2}>
|
||||
<AnimatedCircleButton
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleBulkDetach}
|
||||
disabled={selectedToDetach.size === 0 || isBulkDetaching}
|
||||
loading={isBulkDetaching}
|
||||
>
|
||||
Отвязать выбранные ({selectedToDetach.size})
|
||||
</AnimatedCircleButton>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{linkedItems.length === 0 && !isLoading && (
|
||||
<Typography color="textSecondary" textAlign="center" py={2}>
|
||||
Достопримечательности не найдены
|
||||
@@ -258,53 +456,133 @@ const LinkedSightsContentsInner = <
|
||||
{type === "edit" && !disableCreation && (
|
||||
<Stack gap={2} mt={2}>
|
||||
<Typography variant="subtitle1">
|
||||
Добавить достопримечательность
|
||||
Добавить достопримечательности
|
||||
</Typography>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
value={
|
||||
availableItems?.find((item) => item.id === selectedItemId) || null
|
||||
}
|
||||
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
|
||||
options={availableItems}
|
||||
getOptionLabel={(item) => String(item.name)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Выберите достопримечательность"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) => option.id === value?.id}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
const searchWords = inputValue
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter(Boolean);
|
||||
return options.filter((option) => {
|
||||
const optionWords = String(option.name)
|
||||
.toLowerCase()
|
||||
.split(" ");
|
||||
return searchWords.every((searchWord) =>
|
||||
optionWords.some((word) => word.startsWith(searchWord))
|
||||
);
|
||||
});
|
||||
}}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} key={option.id}>
|
||||
{String(option.name)}
|
||||
</li>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={linkItem}
|
||||
disabled={!selectedItemId}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, value) => setActiveTab(value)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
<Tab label="По одной" />
|
||||
<Tab label="Массово" />
|
||||
</Tabs>
|
||||
|
||||
<Box sx={{ mt: 1 }}>
|
||||
{activeTab === 0 && (
|
||||
<Stack gap={2}>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
value={
|
||||
availableItems?.find(
|
||||
(item) => item.id === selectedItemId
|
||||
) || null
|
||||
}
|
||||
onChange={(_, newValue) =>
|
||||
setSelectedItemId(newValue?.id || null)
|
||||
}
|
||||
options={availableItems}
|
||||
getOptionLabel={(item) => String(item.name)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Выберите достопримечательность"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
option.id === value?.id
|
||||
}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
const searchWords = inputValue
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter(Boolean);
|
||||
return options.filter((option) => {
|
||||
const optionWords = String(option.name)
|
||||
.toLowerCase()
|
||||
.split(" ");
|
||||
return searchWords.every((searchWord) =>
|
||||
optionWords.some((word) => word.startsWith(searchWord))
|
||||
);
|
||||
});
|
||||
}}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} key={option.id}>
|
||||
{String(option.name)}
|
||||
</li>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AnimatedCircleButton
|
||||
variant="contained"
|
||||
onClick={linkItem}
|
||||
disabled={!selectedItemId || isLinkingSingle}
|
||||
loading={isLinkingSingle}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Добавить
|
||||
</AnimatedCircleButton>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{activeTab === 1 && (
|
||||
<Stack gap={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Поиск достопримечательностей"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Введите название..."
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<Paper sx={{ maxHeight: 300, overflow: "auto", p: 2 }}>
|
||||
<Stack gap={1}>
|
||||
{filteredAvailableItems.map((item) => (
|
||||
<FormControlLabel
|
||||
key={item.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={selectedItems.has(item.id)}
|
||||
onChange={() => handleCheckboxChange(item.id)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={String(item.name)}
|
||||
sx={{
|
||||
margin: 0,
|
||||
"& .MuiFormControlLabel-label": {
|
||||
fontSize: "0.9rem",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{filteredAvailableItems.length === 0 && (
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
textAlign="center"
|
||||
py={1}
|
||||
>
|
||||
{searchQuery.trim()
|
||||
? "Достопримечательности не найдены"
|
||||
: "Нет доступных достопримечательностей"}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<AnimatedCircleButton
|
||||
variant="contained"
|
||||
onClick={handleBulkLink}
|
||||
disabled={selectedItems.size === 0 || isLinkingBulk}
|
||||
loading={isLinkingBulk}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Добавить выбранные ({selectedItems.size})
|
||||
</AnimatedCircleButton>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
|
||||
@@ -6,13 +6,19 @@ import {
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { stationsStore, languageStore, cityStore } from "@shared";
|
||||
import {
|
||||
stationsStore,
|
||||
languageStore,
|
||||
cityStore,
|
||||
LoadingSpinner,
|
||||
} from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { LinkedSights } from "../LinkedSights";
|
||||
@@ -21,6 +27,7 @@ import { SaveWithoutCityAgree } from "@widgets";
|
||||
export const StationEditPage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const { language } = languageStore;
|
||||
const { id } = useParams();
|
||||
const {
|
||||
@@ -90,18 +97,41 @@ export const StationEditPage = observer(() => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAndSetStationData = async () => {
|
||||
if (!id) return;
|
||||
if (!id) {
|
||||
setIsLoadingData(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const stationId = Number(id);
|
||||
await getEditStation(stationId);
|
||||
await getCities("ru");
|
||||
await getCities("en");
|
||||
await getCities("zh");
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
const stationId = Number(id);
|
||||
await getEditStation(stationId);
|
||||
await getCities("ru");
|
||||
await getCities("en");
|
||||
await getCities("zh");
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAndSetStationData();
|
||||
}, [id]);
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных станции..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<LanguageSwitcher />
|
||||
|
||||
@@ -8,9 +8,14 @@ import {
|
||||
} from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
|
||||
import { Eye, Pencil, Trash2, Minus, Route } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
import {
|
||||
CreateButton,
|
||||
DeleteModal,
|
||||
LanguageSwitcher,
|
||||
EditStationTransfersModal,
|
||||
} from "@widgets";
|
||||
import { Box, CircularProgress } from "@mui/material";
|
||||
|
||||
export const StationListPage = observer(() => {
|
||||
@@ -18,7 +23,11 @@ export const StationListPage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||
const [isTransfersModalOpen, setIsTransfersModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<number | null>(null);
|
||||
const [selectedStationId, setSelectedStationId] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const [ids, setIds] = useState<number[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { language } = languageStore;
|
||||
@@ -51,8 +60,8 @@ export const StationListPage = observer(() => {
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "system_name",
|
||||
headerName: "Системное название",
|
||||
field: "description",
|
||||
headerName: "Описание",
|
||||
flex: 1,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
@@ -88,7 +97,7 @@ export const StationListPage = observer(() => {
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
width: 140,
|
||||
width: 200,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
sortable: false,
|
||||
@@ -102,6 +111,15 @@ export const StationListPage = observer(() => {
|
||||
<button onClick={() => navigate(`/station/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedStationId(params.row.id);
|
||||
setIsTransfersModalOpen(true);
|
||||
}}
|
||||
title="Редактировать пересадки"
|
||||
>
|
||||
<Route size={20} className="text-purple-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -130,7 +148,7 @@ export const StationListPage = observer(() => {
|
||||
const rows = filteredStations().map((station: any) => ({
|
||||
id: station.id,
|
||||
name: station.name,
|
||||
system_name: station.system_name,
|
||||
description: station.description,
|
||||
direction: station.direction,
|
||||
}));
|
||||
|
||||
@@ -205,6 +223,15 @@ export const StationListPage = observer(() => {
|
||||
setIsBulkDeleteModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<EditStationTransfersModal
|
||||
open={isTransfersModalOpen}
|
||||
onClose={() => {
|
||||
setIsTransfersModalOpen(false);
|
||||
setSelectedStationId(null);
|
||||
}}
|
||||
stationId={selectedStationId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Paper } from "@mui/material";
|
||||
import { languageStore, stationsStore } from "@shared";
|
||||
import { Paper, Box } from "@mui/material";
|
||||
import { languageStore, stationsStore, LoadingSpinner } from "@shared";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { LinkedSights } from "../LinkedSights";
|
||||
|
||||
@@ -12,15 +12,38 @@ export const StationPreviewPage = observer(() => {
|
||||
const { stationPreview, getStationPreview } = stationsStore;
|
||||
const navigate = useNavigate();
|
||||
const { language } = languageStore;
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
await getStationPreview(Number(id));
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
await getStationPreview(Number(id));
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
})();
|
||||
}, [id, language]);
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных станции..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper className="w-full p-3 py-5 flex flex-col gap-10">
|
||||
<LanguageSwitcher />
|
||||
|
||||
@@ -4,18 +4,20 @@ import {
|
||||
Checkbox,
|
||||
Paper,
|
||||
TextField,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { userStore, languageStore } from "@shared";
|
||||
import { userStore, languageStore, LoadingSpinner } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const UserEditPage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
|
||||
const { id } = useParams();
|
||||
const { editUserData, editUser, getUser, setEditUserData } = userStore;
|
||||
@@ -41,18 +43,40 @@ export const UserEditPage = observer(() => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
const data = await getUser(Number(id));
|
||||
setIsLoadingData(true);
|
||||
try {
|
||||
const data = await getUser(Number(id));
|
||||
|
||||
setEditUserData(
|
||||
data?.name || "",
|
||||
data?.email || "",
|
||||
data?.password || "",
|
||||
data?.is_admin || false
|
||||
);
|
||||
setEditUserData(
|
||||
data?.name || "",
|
||||
data?.email || "",
|
||||
data?.password || "",
|
||||
data?.is_admin || false
|
||||
);
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoadingData(false);
|
||||
}
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
if (isLoadingData) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "60vh",
|
||||
}}
|
||||
>
|
||||
<LoadingSpinner message="Загрузка данных пользователя..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@@ -86,28 +86,35 @@ class EditSightStore {
|
||||
}
|
||||
|
||||
hasLoadedCommon = false;
|
||||
isLoading = false;
|
||||
|
||||
getSightInfo = async (id: number, language: Language) => {
|
||||
const response = await languageInstance(language).get(`/sight/${id}`);
|
||||
const data = response.data;
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await languageInstance(language).get(`/sight/${id}`);
|
||||
const data = response.data;
|
||||
|
||||
if (data.left_article != 0 && data.left_article != null) {
|
||||
await this.getLeftArticle(data.left_article);
|
||||
}
|
||||
if (data.left_article != 0 && data.left_article != null) {
|
||||
await this.getLeftArticle(data.left_article);
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.sight[language] = {
|
||||
...this.sight[language],
|
||||
...data,
|
||||
};
|
||||
|
||||
if (!this.hasLoadedCommon) {
|
||||
this.sight.common = {
|
||||
...this.sight.common,
|
||||
runInAction(() => {
|
||||
this.sight[language] = {
|
||||
...this.sight[language],
|
||||
...data,
|
||||
};
|
||||
this.hasLoadedCommon = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.hasLoadedCommon) {
|
||||
this.sight.common = {
|
||||
...this.sight.common,
|
||||
...data,
|
||||
};
|
||||
this.hasLoadedCommon = true;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
||||
@@ -168,6 +175,8 @@ class EditSightStore {
|
||||
|
||||
clearSightInfo = () => {
|
||||
this.needLeaveAgree = false;
|
||||
this.hasLoadedCommon = false;
|
||||
this.isLoading = false;
|
||||
this.sight = {
|
||||
common: {
|
||||
id: 0,
|
||||
@@ -479,18 +488,19 @@ class EditSightStore {
|
||||
formData.append("media_name", media_name);
|
||||
}
|
||||
formData.append("type", type.toString());
|
||||
try {
|
||||
const response = await authInstance.post(`/media`, formData);
|
||||
this.fileToUpload = null;
|
||||
this.uploadMediaOpen = false;
|
||||
mediaStore.getMedia();
|
||||
return {
|
||||
id: response.data.id,
|
||||
filename: filename,
|
||||
media_name: media_name,
|
||||
media_type: type,
|
||||
};
|
||||
} catch (error) {}
|
||||
|
||||
const response = await authInstance.post(`/media`, formData);
|
||||
this.fileToUpload = null;
|
||||
this.uploadMediaOpen = false;
|
||||
|
||||
mediaStore.getMedia();
|
||||
|
||||
return {
|
||||
id: response.data.id,
|
||||
filename: filename,
|
||||
media_name: media_name,
|
||||
media_type: type,
|
||||
};
|
||||
};
|
||||
|
||||
createLinkWithArticle = async (media: {
|
||||
|
||||
@@ -6,10 +6,24 @@ class LanguageStore {
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const storedLanguage = window.localStorage.getItem("appLanguage");
|
||||
if (
|
||||
storedLanguage &&
|
||||
["ru", "en", "zh"].includes(storedLanguage.toLowerCase())
|
||||
) {
|
||||
this.language = storedLanguage.toLowerCase() as Language;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLanguage = (language: Language) => {
|
||||
this.language = language;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem("appLanguage", language);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import { authInstance } from "@shared";
|
||||
import { authInstance, languageInstance, languageStore } from "@shared";
|
||||
|
||||
export type Route = {
|
||||
route_name: string;
|
||||
@@ -89,11 +89,43 @@ class RouteStore {
|
||||
};
|
||||
|
||||
saveRouteStations = async (routeId: number, stationId: number) => {
|
||||
await authInstance.patch(`/route/${routeId}/station`, {
|
||||
...this.routeStations[routeId]?.find(
|
||||
(station) => station.id === stationId
|
||||
),
|
||||
const { language } = languageStore;
|
||||
|
||||
// Получаем актуальные данные станции с сервера
|
||||
const stationResponse = await languageInstance(language).get(
|
||||
`/station/${stationId}`
|
||||
);
|
||||
const fullStationData = stationResponse.data;
|
||||
|
||||
// Получаем отредактированные данные из локального кеша
|
||||
const editedStationData = this.routeStations[routeId]?.find(
|
||||
(station) => station.id === stationId
|
||||
);
|
||||
|
||||
// Формируем данные для отправки: все поля станции + отредактированные offset
|
||||
const dataToSend: any = {
|
||||
station_id: stationId,
|
||||
offset_x: editedStationData?.offset_x ?? fullStationData.offset_x ?? 0,
|
||||
offset_y: editedStationData?.offset_y ?? fullStationData.offset_y ?? 0,
|
||||
align: editedStationData?.align ?? fullStationData.align ?? 0,
|
||||
transfers: fullStationData.transfers || {},
|
||||
};
|
||||
|
||||
await authInstance.patch(`/route/${routeId}/station`, dataToSend);
|
||||
|
||||
// Обновляем локальный кеш после успешного сохранения
|
||||
runInAction(() => {
|
||||
if (this.routeStations[routeId]) {
|
||||
this.routeStations[routeId] = this.routeStations[routeId].map(
|
||||
(station) =>
|
||||
station.id === stationId
|
||||
? {
|
||||
...station,
|
||||
...dataToSend,
|
||||
}
|
||||
: station
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -123,11 +155,18 @@ class RouteStore {
|
||||
if (!this.editRouteData.video_preview) {
|
||||
delete this.editRouteData.video_preview;
|
||||
}
|
||||
const response = await authInstance.patch(`/route/${id}`, {
|
||||
const dataToSend: any = {
|
||||
...this.editRouteData,
|
||||
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
||||
center_longitude: parseFloat(this.editRouteData.center_longitude),
|
||||
});
|
||||
};
|
||||
if (
|
||||
this.editRouteData.governor_appeal === 0 ||
|
||||
!this.editRouteData.governor_appeal
|
||||
) {
|
||||
dataToSend.governor_appeal = null;
|
||||
}
|
||||
const response = await authInstance.patch(`/route/${id}`, dataToSend);
|
||||
|
||||
runInAction(() => {
|
||||
this.route[id] = response.data;
|
||||
|
||||
@@ -132,12 +132,16 @@ class SightsStore {
|
||||
common: boolean
|
||||
) => {
|
||||
if (common) {
|
||||
// @ts-ignore
|
||||
this.sight!.common = {
|
||||
// @ts-ignore
|
||||
...this.sight!.common,
|
||||
...content,
|
||||
};
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.sight![language] = {
|
||||
// @ts-ignore
|
||||
...this.sight![language],
|
||||
...content,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { authInstance } from "@shared";
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import {
|
||||
articlesStore,
|
||||
@@ -25,9 +25,18 @@ type Snapshot = {
|
||||
CreationTime: string;
|
||||
};
|
||||
|
||||
type SnapshotStatus = {
|
||||
ID: string;
|
||||
Status: string;
|
||||
Progress: number;
|
||||
Error: string;
|
||||
};
|
||||
|
||||
class SnapshotStore {
|
||||
snapshots: Snapshot[] = [];
|
||||
snapshot: Snapshot | null = null;
|
||||
lastRequestId: string | null = null;
|
||||
snapshotStatus: SnapshotStatus | null = null;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
@@ -266,7 +275,23 @@ class SnapshotStore {
|
||||
};
|
||||
|
||||
createSnapshot = async (name: string) => {
|
||||
await authInstance.post(`/snapshots`, { name });
|
||||
this.lastRequestId = uuidv4();
|
||||
|
||||
const response = await authInstance.post(
|
||||
`/snapshots`,
|
||||
{ name },
|
||||
{ headers: { "X-Request-ID": this.lastRequestId } }
|
||||
);
|
||||
|
||||
return response.data.ID;
|
||||
};
|
||||
|
||||
getSnapshotStatus = async (id: string) => {
|
||||
const response = await authInstance.get(`/snapshots/status/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.snapshotStatus = response.data;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { authInstance, languageInstance, languageStore } from "@shared";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import { routeStore } from "../RouteStore";
|
||||
|
||||
type Language = "ru" | "en" | "zh";
|
||||
|
||||
@@ -546,6 +547,99 @@ class StationsStore {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
updateStationTransfers = async (
|
||||
id: number,
|
||||
transfers: {
|
||||
bus: string;
|
||||
metro_blue: string;
|
||||
metro_green: string;
|
||||
metro_orange: string;
|
||||
metro_purple: string;
|
||||
metro_red: string;
|
||||
train: string;
|
||||
tram: string;
|
||||
trolleybus: string;
|
||||
}
|
||||
) => {
|
||||
const { language } = languageStore;
|
||||
|
||||
// Получаем данные станции для текущего языка
|
||||
const response = await languageInstance(language).get(`/station/${id}`);
|
||||
const stationData = response.data as Station;
|
||||
|
||||
if (!stationData) {
|
||||
throw new Error("Station not found");
|
||||
}
|
||||
|
||||
// Формируем commonDataPayload как в editStation, с обновленными transfers
|
||||
const commonDataPayload = {
|
||||
city_id: stationData.city_id,
|
||||
direction: stationData.direction,
|
||||
latitude: stationData.latitude,
|
||||
longitude: stationData.longitude,
|
||||
offset_x: stationData.offset_x,
|
||||
offset_y: stationData.offset_y,
|
||||
transfers: transfers,
|
||||
city: stationData.city || "",
|
||||
};
|
||||
|
||||
// Отправляем один PATCH запрос, так как пересадки общие для всех языков
|
||||
const patchResponse = await languageInstance(language).patch(
|
||||
`/station/${id}`,
|
||||
{
|
||||
name: stationData.name || "",
|
||||
system_name: stationData.system_name || "",
|
||||
description: stationData.description || "",
|
||||
address: stationData.address || "",
|
||||
...commonDataPayload,
|
||||
}
|
||||
);
|
||||
|
||||
// Обновляем данные для всех языков в локальном состоянии
|
||||
runInAction(() => {
|
||||
const updatedTransfers = patchResponse.data.transfers;
|
||||
|
||||
for (const lang of ["ru", "en", "zh"] as const) {
|
||||
if (this.stationPreview[id]) {
|
||||
this.stationPreview[id][lang] = {
|
||||
...this.stationPreview[id][lang],
|
||||
data: {
|
||||
...this.stationPreview[id][lang].data,
|
||||
transfers: updatedTransfers,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (this.stationLists[lang].data) {
|
||||
this.stationLists[lang].data = this.stationLists[lang].data.map(
|
||||
(station: Station) =>
|
||||
station.id === id
|
||||
? {
|
||||
...station,
|
||||
transfers: updatedTransfers,
|
||||
}
|
||||
: station
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем пересадки в RouteStore.routeStations для всех маршрутов
|
||||
if (routeStore?.routeStations) {
|
||||
for (const routeId in routeStore.routeStations) {
|
||||
routeStore.routeStations[routeId] = routeStore.routeStations[
|
||||
routeId
|
||||
].map((station: any) =>
|
||||
station.id === id
|
||||
? {
|
||||
...station,
|
||||
transfers: updatedTransfers,
|
||||
}
|
||||
: station
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const stationsStore = new StationsStore();
|
||||
|
||||
171
src/shared/ui/AnimatedCircleButton.tsx
Normal file
171
src/shared/ui/AnimatedCircleButton.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { forwardRef } from "react";
|
||||
import { Button, ButtonProps, CircularProgress } from "@mui/material";
|
||||
import { alpha, keyframes, styled } from "@mui/material/styles";
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
|
||||
type AnimatedCircleButtonProps = ButtonProps & {
|
||||
disableAnimation?: boolean;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
type StyledButtonProps = AnimatedCircleButtonProps & { theme: Theme };
|
||||
|
||||
const loadingPulse = keyframes`
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0.6);
|
||||
opacity: 0.35;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.45);
|
||||
opacity: 0.15;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(0.6);
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button, {
|
||||
shouldForwardProp: (prop) =>
|
||||
prop !== "disableAnimation" && prop !== "loading",
|
||||
})<AnimatedCircleButtonProps>((props: StyledButtonProps) => {
|
||||
const {
|
||||
theme,
|
||||
disableAnimation = false,
|
||||
color,
|
||||
variant = "text",
|
||||
disabled = false,
|
||||
loading = false,
|
||||
} = props;
|
||||
|
||||
const shouldAnimate = !disableAnimation && (!disabled || loading);
|
||||
const pointerBlocked = loading;
|
||||
|
||||
const paletteMainMap: Record<string, string> = {
|
||||
primary: theme.palette.primary.main,
|
||||
secondary: theme.palette.secondary.main,
|
||||
error: theme.palette.error.main,
|
||||
warning: theme.palette.warning.main,
|
||||
info: theme.palette.info.main,
|
||||
success: theme.palette.success.main,
|
||||
inherit: theme.palette.primary.main,
|
||||
};
|
||||
|
||||
const paletteMain =
|
||||
(color && paletteMainMap[String(color)]) ?? theme.palette.primary.main;
|
||||
|
||||
const pulseColor =
|
||||
variant === "outlined" || variant === "text"
|
||||
? alpha(paletteMain, 0.18)
|
||||
: alpha(paletteMain, 0.3);
|
||||
|
||||
return {
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
borderRadius: 5,
|
||||
zIndex: 0,
|
||||
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
||||
pointerEvents: pointerBlocked ? "none" : undefined,
|
||||
"&::after": shouldAnimate
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
backgroundColor: pulseColor,
|
||||
borderRadius: "50%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
pointerEvents: "none",
|
||||
zIndex: 0,
|
||||
...(loading
|
||||
? {
|
||||
opacity: 0.35,
|
||||
transform: "translate(-50%, -50%) scale(0.6)",
|
||||
animation: `${loadingPulse} 1.2s ease-in-out infinite`,
|
||||
}
|
||||
: {
|
||||
opacity: 0,
|
||||
transform: "translate(-50%, -50%) scale(0)",
|
||||
transition: "transform 0.45s ease, opacity 0.45s ease",
|
||||
}),
|
||||
}
|
||||
: {},
|
||||
...(loading
|
||||
? {}
|
||||
: {
|
||||
"&:hover": {
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: theme.shadows[4],
|
||||
},
|
||||
"&:hover::after": shouldAnimate
|
||||
? {
|
||||
transform: "translate(-50%, -50%) scale(15)",
|
||||
opacity: 1,
|
||||
}
|
||||
: {},
|
||||
"&:active": {
|
||||
transform: "translateY(0)",
|
||||
boxShadow: theme.shadows[2],
|
||||
},
|
||||
"&:active::after": shouldAnimate
|
||||
? {
|
||||
transform: "translate(-50%, -50%) scale(18)",
|
||||
opacity: 0.4,
|
||||
}
|
||||
: {},
|
||||
}),
|
||||
"&.Mui-disabled": {
|
||||
boxShadow: "none",
|
||||
transform: "none",
|
||||
...(loading && shouldAnimate
|
||||
? {}
|
||||
: {
|
||||
"&::after": {
|
||||
opacity: 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
...(disabled && {
|
||||
boxShadow: "none",
|
||||
transform: "none",
|
||||
}),
|
||||
"& > *": {
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const AnimatedCircleButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
AnimatedCircleButtonProps
|
||||
>((props, ref) => {
|
||||
const {
|
||||
loading = false,
|
||||
disabled,
|
||||
children,
|
||||
startIcon,
|
||||
endIcon,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const effectiveStartIcon = loading ? (
|
||||
<CircularProgress size={16} color="inherit" />
|
||||
) : (
|
||||
startIcon
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
ref={ref}
|
||||
loading={loading}
|
||||
disabled={loading ? true : disabled}
|
||||
startIcon={effectiveStartIcon}
|
||||
endIcon={loading ? undefined : endIcon}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</StyledButton>
|
||||
);
|
||||
});
|
||||
@@ -2,3 +2,5 @@ export * from "./TabPanel";
|
||||
export * from "./BackButton";
|
||||
export * from "./Modal";
|
||||
export * from "./CoordinatesInput";
|
||||
export * from "./AnimatedCircleButton";
|
||||
export * from "./LoadingSpinner";
|
||||
|
||||
@@ -5,7 +5,7 @@ import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { Check, Copy, RotateCcw, X } from "lucide-react";
|
||||
import { Check, Copy, RotateCcw, Trash2, X } from "lucide-react";
|
||||
import {
|
||||
authInstance,
|
||||
devicesStore,
|
||||
@@ -19,6 +19,7 @@ import { Button, Checkbox, Typography } from "@mui/material";
|
||||
import { Vehicle } from "@shared";
|
||||
import { toast } from "react-toastify";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { DeleteModal } from "@widgets";
|
||||
|
||||
export type ConnectedDevice = string;
|
||||
|
||||
@@ -108,10 +109,11 @@ export const DevicesTable = observer(() => {
|
||||
} = devicesStore;
|
||||
|
||||
const { snapshots, getSnapshots } = snapshotStore;
|
||||
const { getVehicles, vehicles } = vehicleStore;
|
||||
const { getVehicles, vehicles, deleteVehicle } = vehicleStore;
|
||||
const { devices } = devicesStore;
|
||||
const navigate = useNavigate();
|
||||
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const currentTableRows = transformDevicesToRows(vehicles.data as Vehicle[]);
|
||||
|
||||
@@ -200,6 +202,30 @@ export const DevicesTable = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
const getVehicleIdsByUuids = (uuids: string[]): number[] => {
|
||||
return vehicles.data
|
||||
.filter((vehicle) => uuids.includes(vehicle.vehicle.uuid ?? ""))
|
||||
.map((vehicle) => vehicle.vehicle.id);
|
||||
};
|
||||
|
||||
const handleDeleteVehicles = async () => {
|
||||
if (selectedDeviceUuids.length === 0) return;
|
||||
|
||||
const vehicleIds = getVehicleIdsByUuids(selectedDeviceUuids);
|
||||
|
||||
try {
|
||||
await Promise.all(vehicleIds.map((id) => deleteVehicle(id)));
|
||||
await getVehicles();
|
||||
await getDevices();
|
||||
setSelectedDeviceUuids([]);
|
||||
setIsDeleteModalOpen(false);
|
||||
toast.success(`Удалено устройств: ${vehicleIds.length}`);
|
||||
} catch (error) {
|
||||
console.error("Error deleting vehicles:", error);
|
||||
toast.error("Ошибка при удалении устройств");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
||||
@@ -213,7 +239,7 @@ export const DevicesTable = observer(() => {
|
||||
Добавить устройство
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-end p-3 gap-2">
|
||||
<div className="flex justify-end p-3 gap-2 items-center">
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleSelectAllDevices}
|
||||
@@ -221,6 +247,17 @@ export const DevicesTable = observer(() => {
|
||||
>
|
||||
{isAllSelected ? "Снять выбор" : "Выбрать все"}
|
||||
</Button>
|
||||
{selectedDeviceUuids.length > 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
size="small"
|
||||
startIcon={<Trash2 size={16} />}
|
||||
>
|
||||
Удалить ({selectedDeviceUuids.length})
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
@@ -451,6 +488,12 @@ export const DevicesTable = observer(() => {
|
||||
Отмена
|
||||
</Button>
|
||||
</Modal>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={handleDeleteVehicles}
|
||||
onCancel={() => setIsDeleteModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,23 +10,25 @@ import { observer } from "mobx-react-lite";
|
||||
import { useState, DragEvent, useRef } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
interface MediaAreaProps {
|
||||
articleId: number;
|
||||
mediaIds: { id: string; media_type: number; filename: string }[];
|
||||
deleteMedia: (id: number, media_id: string) => void;
|
||||
onFilesDrop?: (files: File[]) => void;
|
||||
setSelectMediaDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const MediaArea = observer(
|
||||
({
|
||||
articleId,
|
||||
mediaIds,
|
||||
deleteMedia,
|
||||
onFilesDrop, // 👈 Проп для обработки загруженных файлов
|
||||
onFilesDrop,
|
||||
setSelectMediaDialogOpen,
|
||||
}: {
|
||||
articleId: number;
|
||||
mediaIds: { id: string; media_type: number; filename: string }[];
|
||||
deleteMedia: (id: number, media_id: string) => void;
|
||||
onFilesDrop?: (files: File[]) => void;
|
||||
setSelectMediaDialogOpen: (open: boolean) => void;
|
||||
}) => {
|
||||
}: MediaAreaProps) => {
|
||||
const [mediaModal, setMediaModal] = useState<boolean>(false);
|
||||
const [mediaId, setMediaId] = useState<string>("");
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleMediaModal = (mediaId: string) => {
|
||||
@@ -34,23 +36,29 @@ export const MediaArea = observer(
|
||||
setMediaId(mediaId);
|
||||
};
|
||||
|
||||
const processFiles = (files: File[]) => {
|
||||
if (!files.length || !onFilesDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
onFilesDrop(validFiles);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length && onFilesDrop) {
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
onFilesDrop(validFiles);
|
||||
}
|
||||
}
|
||||
processFiles(files);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
@@ -68,19 +76,11 @@ export const MediaArea = observer(
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
if (files.length && onFilesDrop) {
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
processFiles(files);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
onFilesDrop(validFiles);
|
||||
}
|
||||
if (event.target) {
|
||||
event.target.value = "";
|
||||
}
|
||||
// Сбрасываем значение input, чтобы можно было выбрать тот же файл снова
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -96,7 +96,7 @@ export const MediaArea = observer(
|
||||
<Box className="w-full flex flex-col items-center justify-center border rounded-md p-4">
|
||||
<div className="w-full flex flex-col items-center justify-center">
|
||||
<div
|
||||
className={`w-full h-40 flex flex-col justify-center items-center text-gray-400 border-dashed border-2 rounded-md border-gray-400 p-4 cursor-pointer hover:bg-gray-50 ${
|
||||
className={`w-full h-40 flex flex-col justify-center items-center text-gray-400 border-dashed border-2 rounded-md border-gray-400 p-4 cursor-pointer hover:bg-gray-50 transition-colors ${
|
||||
isDragging ? "bg-blue-100 border-blue-400" : ""
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
@@ -105,9 +105,11 @@ export const MediaArea = observer(
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Upload size={32} className="mb-2" />
|
||||
Перетащите медиа файлы сюда или нажмите для выбора
|
||||
<span className="text-center">
|
||||
Перетащите медиа файлы сюда или нажмите для выбора
|
||||
</span>
|
||||
</div>
|
||||
<div>или</div>
|
||||
<div className="my-2">или</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
@@ -117,33 +119,38 @@ export const MediaArea = observer(
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-start flex-wrap gap-2 mt-4 py-10">
|
||||
{mediaIds.map((m) => (
|
||||
<button
|
||||
className="relative w-20 h-20"
|
||||
key={m.id}
|
||||
onClick={() => handleMediaModal(m.id)}
|
||||
>
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: m.id,
|
||||
media_type: m.media_type,
|
||||
filename: m.filename,
|
||||
}}
|
||||
height="40px"
|
||||
/>
|
||||
{mediaIds.length > 0 && (
|
||||
<div className="w-full flex flex-start flex-wrap gap-2 mt-4 py-10">
|
||||
{mediaIds.map((m) => (
|
||||
<button
|
||||
className="absolute top-2 right-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteMedia(articleId, m.id);
|
||||
}}
|
||||
className="relative w-20 h-20"
|
||||
key={m.id}
|
||||
onClick={() => handleMediaModal(m.id)}
|
||||
type="button"
|
||||
>
|
||||
<X size={16} color="red" />
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: m.id,
|
||||
media_type: m.media_type,
|
||||
filename: m.filename,
|
||||
}}
|
||||
height="40px"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md hover:shadow-lg transition-shadow"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteMedia(articleId, m.id);
|
||||
}}
|
||||
type="button"
|
||||
aria-label="Удалить медиа"
|
||||
>
|
||||
<X size={16} color="red" />
|
||||
</button>
|
||||
</button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<PreviewMediaDialog
|
||||
|
||||
@@ -11,52 +11,72 @@ import { observer } from "mobx-react-lite";
|
||||
import { useState, DragEvent, useRef } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
type ContextType =
|
||||
| "sight"
|
||||
| "city"
|
||||
| "carrier"
|
||||
| "country"
|
||||
| "vehicle"
|
||||
| "station";
|
||||
|
||||
interface MediaAreaForSightProps {
|
||||
onFilesDrop?: (files: File[]) => void;
|
||||
onFinishUpload?: (mediaId: string) => void;
|
||||
contextObjectName?: string;
|
||||
contextType?: ContextType;
|
||||
isArticle?: boolean;
|
||||
articleName?: string;
|
||||
}
|
||||
|
||||
export const MediaAreaForSight = observer(
|
||||
({
|
||||
onFilesDrop, // 👈 Проп для обработки загруженных файлов
|
||||
onFilesDrop,
|
||||
onFinishUpload,
|
||||
contextObjectName,
|
||||
contextType,
|
||||
isArticle,
|
||||
articleName,
|
||||
}: {
|
||||
onFilesDrop?: (files: File[]) => void;
|
||||
onFinishUpload?: (mediaId: string) => void;
|
||||
contextObjectName?: string;
|
||||
contextType?:
|
||||
| "sight"
|
||||
| "city"
|
||||
| "carrier"
|
||||
| "country"
|
||||
| "vehicle"
|
||||
| "station";
|
||||
isArticle?: boolean;
|
||||
articleName?: string;
|
||||
}) => {
|
||||
const [selectMediaDialogOpen, setSelectMediaDialogOpen] = useState(false);
|
||||
const [uploadMediaDialogOpen, setUploadMediaDialogOpen] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
}: MediaAreaForSightProps) => {
|
||||
const [selectMediaDialogOpen, setSelectMediaDialogOpen] =
|
||||
useState<boolean>(false);
|
||||
const [uploadMediaDialogOpen, setUploadMediaDialogOpen] =
|
||||
useState<boolean>(false);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { setFileToUpload } = editSightStore;
|
||||
|
||||
const processFiles = (files: File[]) => {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error: string) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
// Сохраняем первый файл для загрузки
|
||||
setFileToUpload(validFiles[0]);
|
||||
|
||||
// Вызываем колбэк, если он передан
|
||||
if (onFilesDrop) {
|
||||
onFilesDrop(validFiles);
|
||||
}
|
||||
|
||||
// Открываем диалог загрузки
|
||||
setUploadMediaDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length) {
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error: string) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0 && onFilesDrop) {
|
||||
setFileToUpload(validFiles[0]);
|
||||
setUploadMediaDialogOpen(true);
|
||||
}
|
||||
}
|
||||
processFiles(files);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
@@ -74,22 +94,12 @@ export const MediaAreaForSight = observer(
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
if (files.length) {
|
||||
const { validFiles, errors } = filterValidFiles(files);
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error: string) => toast.error(error));
|
||||
}
|
||||
|
||||
if (validFiles.length > 0 && onFilesDrop) {
|
||||
setFileToUpload(validFiles[0]);
|
||||
onFilesDrop(validFiles);
|
||||
setUploadMediaDialogOpen(true);
|
||||
}
|
||||
}
|
||||
processFiles(files);
|
||||
|
||||
// Сбрасываем значение input, чтобы можно было выбрать тот же файл снова
|
||||
event.target.value = "";
|
||||
if (event.target) {
|
||||
event.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -105,7 +115,7 @@ export const MediaAreaForSight = observer(
|
||||
<Box className="w-full flex flex-col items-center justify-center border rounded-md p-4">
|
||||
<div className="w-full flex flex-col items-center justify-center">
|
||||
<div
|
||||
className={`w-full h-40 flex text-center flex-col justify-center items-center text-gray-400 border-dashed border-2 rounded-md border-gray-400 p-4 cursor-pointer hover:bg-gray-50 ${
|
||||
className={`w-full h-40 flex flex-col justify-center items-center text-gray-400 border-dashed border-2 rounded-md border-gray-400 p-4 cursor-pointer hover:bg-gray-50 transition-colors ${
|
||||
isDragging ? "bg-blue-100 border-blue-400" : ""
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
@@ -114,9 +124,11 @@ export const MediaAreaForSight = observer(
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Upload size={32} className="mb-2" />
|
||||
Перетащите медиа файлы сюда или нажмите для выбора
|
||||
<span className="text-center">
|
||||
Перетащите медиа файлы сюда или нажмите для выбора
|
||||
</span>
|
||||
</div>
|
||||
<div>или</div>
|
||||
<div className="my-2">или</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
|
||||
@@ -117,6 +117,7 @@ export function MediaViewer({
|
||||
}/download?token=${token}`}
|
||||
alt={media?.filename}
|
||||
style={{
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
@@ -127,8 +128,8 @@ export function MediaViewer({
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
media?.id
|
||||
}/download?token=${token}`}
|
||||
width={width ? width : "500px"}
|
||||
height={height ? height : "300px"}
|
||||
width={fullWidth ? "100%" : width ? width : "500px"}
|
||||
height={fullHeight ? "100%" : height ? height : "300px"}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import {
|
||||
BackButton,
|
||||
createSightStore,
|
||||
editSightStore,
|
||||
languageStore,
|
||||
SelectArticleModal,
|
||||
TabPanel,
|
||||
@@ -51,9 +52,6 @@ export const CreateRightTab = observer(
|
||||
unlinkPreviewMedia,
|
||||
createLinkWithRightArticle,
|
||||
deleteRightArticleMedia,
|
||||
setFileToUpload,
|
||||
setUploadMediaOpen,
|
||||
uploadMediaOpen,
|
||||
unlinkRightAritcle,
|
||||
deleteRightArticle,
|
||||
linkExistingRightArticle,
|
||||
@@ -62,6 +60,8 @@ export const CreateRightTab = observer(
|
||||
updateRightArticles,
|
||||
} = createSightStore;
|
||||
const { language } = languageStore;
|
||||
const { setFileToUpload, setUploadMediaOpen, uploadMediaOpen } =
|
||||
editSightStore;
|
||||
|
||||
const [selectArticleDialogOpen, setSelectArticleDialogOpen] =
|
||||
useState(false);
|
||||
@@ -434,49 +434,61 @@ export const CreateRightTab = observer(
|
||||
</Box>
|
||||
) : type === "media" ? (
|
||||
<Box className="w-[80%] border border-gray-300 rounded-2xl relative flex items-center justify-center">
|
||||
{sight.preview_media && (
|
||||
<>
|
||||
{type === "media" && (
|
||||
<Box className="w-[80%] h-full rounded-2xl relative flex items-center justify-center">
|
||||
{previewMedia && (
|
||||
<>
|
||||
<Box className="absolute top-4 right-4 z-10">
|
||||
<button
|
||||
className="w-10 h-10 flex items-center justify-center z-10"
|
||||
onClick={handleUnlinkPreviewMedia}
|
||||
>
|
||||
<X size={20} color="red" />
|
||||
</button>
|
||||
</Box>
|
||||
<>
|
||||
{type === "media" && (
|
||||
<Box className="w-[80%] h-full rounded-2xl relative flex items-center justify-center">
|
||||
{previewMedia && (
|
||||
<>
|
||||
<Box className="absolute top-4 right-4 z-10">
|
||||
<button
|
||||
className="w-10 h-10 flex items-center justify-center z-10"
|
||||
onClick={handleUnlinkPreviewMedia}
|
||||
>
|
||||
<X size={20} color="red" />
|
||||
</button>
|
||||
</Box>
|
||||
|
||||
<Box className="w-1/2 h-1/2">
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: previewMedia.id || "",
|
||||
media_type: previewMedia.media_type,
|
||||
filename: previewMedia.filename || "",
|
||||
}}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!previewMedia && (
|
||||
<MediaAreaForSight
|
||||
onFinishUpload={(mediaId) => {
|
||||
linkPreviewMedia(mediaId);
|
||||
}}
|
||||
onFilesDrop={() => {}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={false}
|
||||
/>
|
||||
)}
|
||||
<Box className="w-1/2 h-1/2">
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: previewMedia.id || "",
|
||||
media_type: previewMedia.media_type,
|
||||
filename: previewMedia.filename || "",
|
||||
}}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!previewMedia && (
|
||||
<Box className="w-full h-full flex justify-center items-center">
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "500px",
|
||||
maxHeight: "100%",
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
margin: "0 auto",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<MediaAreaForSight
|
||||
onFinishUpload={(mediaId) => {
|
||||
linkPreviewMedia(mediaId);
|
||||
}}
|
||||
onFilesDrop={() => {}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={false}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className="w-[80%] h-[70vh] border border-gray-300 rounded-2xl p-3 flex justify-center items-center">
|
||||
|
||||
@@ -275,7 +275,10 @@ export const InformationTab = observer(
|
||||
{sight.common.id !== 0 && (
|
||||
<LinkedStations
|
||||
parentId={sight.common.id}
|
||||
fields={[{ label: "Название", data: "name" }]}
|
||||
fields={[
|
||||
{ label: "Название", data: "name" },
|
||||
{ label: "Описание", data: "description" },
|
||||
]}
|
||||
type="edit"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -415,21 +415,12 @@ export const RightWidgetTab = observer(
|
||||
media_type: previewMedia.media_type,
|
||||
filename: previewMedia.filename || "",
|
||||
}}
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{!previewMedia && (
|
||||
<MediaAreaForSight
|
||||
onFinishUpload={(mediaId) => {
|
||||
linkPreviewMedia(mediaId);
|
||||
}}
|
||||
onFilesDrop={() => {}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={false}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -20,18 +20,6 @@ interface EditStationModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const transferFields = [
|
||||
{ key: "bus", label: "Автобус" },
|
||||
{ key: "metro_blue", label: "Метро (синяя)" },
|
||||
{ key: "metro_green", label: "Метро (зеленая)" },
|
||||
{ key: "metro_orange", label: "Метро (оранжевая)" },
|
||||
{ key: "metro_purple", label: "Метро (фиолетовая)" },
|
||||
{ key: "metro_red", label: "Метро (красная)" },
|
||||
{ key: "train", label: "Электричка" },
|
||||
{ key: "tram", label: "Трамвай" },
|
||||
{ key: "trolleybus", label: "Троллейбус" },
|
||||
];
|
||||
|
||||
export const EditStationModal = observer(
|
||||
({ open, onClose }: EditStationModalProps) => {
|
||||
const { id: routeId } = useParams<{ id: string }>();
|
||||
@@ -95,37 +83,6 @@ export const EditStationModal = observer(
|
||||
defaultValue={station?.offset_y}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Пересадки
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{transferFields.map(({ key, label }) => (
|
||||
<TextField
|
||||
key={key}
|
||||
label={label}
|
||||
name={key}
|
||||
fullWidth
|
||||
defaultValue={station?.transfers?.[key]}
|
||||
onChange={(e) => {
|
||||
setRouteStations(Number(routeId), selectedStationId, {
|
||||
...station,
|
||||
transfers: {
|
||||
...station?.transfers,
|
||||
[key]: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, justifyContent: "flex-end" }}>
|
||||
<Button onClick={handleSave} variant="contained" color="primary">
|
||||
|
||||
168
src/widgets/modals/EditStationTransfersModal/index.tsx
Normal file
168
src/widgets/modals/EditStationTransfersModal/index.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Typography,
|
||||
IconButton,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { stationsStore, languageStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { toast } from "react-toastify";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface EditStationTransfersModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
stationId: number | null;
|
||||
}
|
||||
|
||||
const transferFields = [
|
||||
{ key: "bus", label: "Автобус" },
|
||||
{ key: "metro_blue", label: "Метро (синяя)" },
|
||||
{ key: "metro_green", label: "Метро (зеленая)" },
|
||||
{ key: "metro_orange", label: "Метро (оранжевая)" },
|
||||
{ key: "metro_purple", label: "Метро (фиолетовая)" },
|
||||
{ key: "metro_red", label: "Метро (красная)" },
|
||||
{ key: "train", label: "Электричка" },
|
||||
{ key: "tram", label: "Трамвай" },
|
||||
{ key: "trolleybus", label: "Троллейбус" },
|
||||
];
|
||||
|
||||
export const EditStationTransfersModal = observer(
|
||||
({ open, onClose, stationId }: EditStationTransfersModalProps) => {
|
||||
const { stationLists, updateStationTransfers } = stationsStore;
|
||||
const { language } = languageStore;
|
||||
const [transfers, setTransfers] = useState<{
|
||||
bus: string;
|
||||
metro_blue: string;
|
||||
metro_green: string;
|
||||
metro_orange: string;
|
||||
metro_purple: string;
|
||||
metro_red: string;
|
||||
train: string;
|
||||
tram: string;
|
||||
trolleybus: string;
|
||||
}>({
|
||||
bus: "",
|
||||
metro_blue: "",
|
||||
metro_green: "",
|
||||
metro_orange: "",
|
||||
metro_purple: "",
|
||||
metro_red: "",
|
||||
train: "",
|
||||
tram: "",
|
||||
trolleybus: "",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && stationId) {
|
||||
let station = stationLists[language].data.find(
|
||||
(s: any) => s.id === stationId
|
||||
);
|
||||
if (!station?.transfers) {
|
||||
for (const lang of ["ru", "en", "zh"] as const) {
|
||||
const foundStation = stationLists[lang].data.find(
|
||||
(s: any) => s.id === stationId
|
||||
);
|
||||
if (foundStation?.transfers) {
|
||||
station = foundStation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (station?.transfers) {
|
||||
setTransfers(station.transfers);
|
||||
}
|
||||
}
|
||||
}, [open, stationId, stationLists]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!stationId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await updateStationTransfers(stationId, transfers);
|
||||
toast.success("Пересадки успешно обновлены");
|
||||
const { getStationList } = stationsStore;
|
||||
await getStationList();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Error updating transfers:", error);
|
||||
toast.error("Ошибка при обновлении пересадок");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTransferChange = (key: string, value: string) => {
|
||||
setTransfers((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<IconButton onClick={onClose}>
|
||||
<ArrowLeft />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Станции / Редактировать пересадки
|
||||
</Typography>
|
||||
<Typography variant="h6">Редактирование пересадок</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Пересадки
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
{transferFields.map(({ key, label }) => (
|
||||
<TextField
|
||||
key={key}
|
||||
label={label}
|
||||
name={key}
|
||||
fullWidth
|
||||
value={transfers[key as keyof typeof transfers] || ""}
|
||||
onChange={(e) => handleTransferChange(key, e.target.value)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, justifyContent: "flex-end" }}>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Сохранение..." : "Сохранить"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./SelectArticleDialog";
|
||||
export * from "./EditStationModal";
|
||||
export * from "./EditStationTransfersModal";
|
||||
|
||||
File diff suppressed because one or more lines are too long
479
yarn.lock
479
yarn.lock
@@ -16,7 +16,7 @@
|
||||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz"
|
||||
integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==
|
||||
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.21.3", "@babel/core@^7.28.0":
|
||||
"@babel/core@^7.21.3", "@babel/core@^7.28.0":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz"
|
||||
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
|
||||
@@ -170,6 +170,28 @@
|
||||
resolved "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz"
|
||||
integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==
|
||||
|
||||
"@emnapi/core@^1.5.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.0.tgz#135de4e8858763989112281bdf38ca02439db7c3"
|
||||
integrity sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads" "1.1.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/runtime@^1.5.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.0.tgz#d7ef3832df8564fe5903bf0567aedbd19538ecbe"
|
||||
integrity sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
|
||||
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emotion/babel-plugin@^11.13.5":
|
||||
version "11.13.5"
|
||||
resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz"
|
||||
@@ -215,7 +237,7 @@
|
||||
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz"
|
||||
integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
|
||||
|
||||
"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.14.0", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.9.0":
|
||||
"@emotion/react@^11.14.0":
|
||||
version "11.14.0"
|
||||
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz"
|
||||
integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==
|
||||
@@ -245,7 +267,7 @@
|
||||
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz"
|
||||
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
|
||||
|
||||
"@emotion/styled@^11.14.0", "@emotion/styled@^11.3.0", "@emotion/styled@^11.8.1":
|
||||
"@emotion/styled@^11.14.0":
|
||||
version "11.14.1"
|
||||
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz"
|
||||
integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==
|
||||
@@ -277,11 +299,136 @@
|
||||
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz"
|
||||
integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
|
||||
|
||||
"@esbuild/aix-ppc64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49"
|
||||
integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==
|
||||
|
||||
"@esbuild/android-arm64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03"
|
||||
integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==
|
||||
|
||||
"@esbuild/android-arm@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae"
|
||||
integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==
|
||||
|
||||
"@esbuild/android-x64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6"
|
||||
integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==
|
||||
|
||||
"@esbuild/darwin-arm64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz"
|
||||
integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==
|
||||
|
||||
"@esbuild/darwin-x64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe"
|
||||
integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a"
|
||||
integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==
|
||||
|
||||
"@esbuild/freebsd-x64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb"
|
||||
integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==
|
||||
|
||||
"@esbuild/linux-arm64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5"
|
||||
integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==
|
||||
|
||||
"@esbuild/linux-arm@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f"
|
||||
integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==
|
||||
|
||||
"@esbuild/linux-ia32@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b"
|
||||
integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==
|
||||
|
||||
"@esbuild/linux-loong64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb"
|
||||
integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==
|
||||
|
||||
"@esbuild/linux-mips64el@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5"
|
||||
integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==
|
||||
|
||||
"@esbuild/linux-ppc64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74"
|
||||
integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==
|
||||
|
||||
"@esbuild/linux-riscv64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273"
|
||||
integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==
|
||||
|
||||
"@esbuild/linux-s390x@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263"
|
||||
integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==
|
||||
|
||||
"@esbuild/linux-x64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910"
|
||||
integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077"
|
||||
integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==
|
||||
|
||||
"@esbuild/netbsd-x64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034"
|
||||
integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad"
|
||||
integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==
|
||||
|
||||
"@esbuild/openbsd-x64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2"
|
||||
integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==
|
||||
|
||||
"@esbuild/openharmony-arm64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1"
|
||||
integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==
|
||||
|
||||
"@esbuild/sunos-x64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244"
|
||||
integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==
|
||||
|
||||
"@esbuild/win32-arm64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935"
|
||||
integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==
|
||||
|
||||
"@esbuild/win32-ia32@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343"
|
||||
integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==
|
||||
|
||||
"@esbuild/win32-x64@0.25.11":
|
||||
version "0.25.11"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz#5ecda6f3fe138b7e456f4e429edde33c823f392f"
|
||||
integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0":
|
||||
version "4.9.0"
|
||||
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz"
|
||||
@@ -332,7 +479,7 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@^9.25.0", "@eslint/js@9.38.0":
|
||||
"@eslint/js@9.38.0", "@eslint/js@^9.25.0":
|
||||
version "9.38.0"
|
||||
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz"
|
||||
integrity sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==
|
||||
@@ -442,7 +589,7 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
|
||||
"@mui/material@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/material@^7.1.0", "@mui/material@^7.3.4":
|
||||
"@mui/material@^7.1.0":
|
||||
version "7.3.4"
|
||||
resolved "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz"
|
||||
integrity sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==
|
||||
@@ -481,7 +628,7 @@
|
||||
csstype "^3.1.3"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/system@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system@^7.3.3":
|
||||
"@mui/system@^7.3.3":
|
||||
version "7.3.3"
|
||||
resolved "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz"
|
||||
integrity sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==
|
||||
@@ -546,6 +693,15 @@
|
||||
"@mui/utils" "^7.3.3"
|
||||
"@mui/x-internals" "8.14.0"
|
||||
|
||||
"@napi-rs/wasm-runtime@^1.0.7":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz#dcfea99a75f06209a235f3d941e3460a51e9b14c"
|
||||
integrity sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.5.0"
|
||||
"@emnapi/runtime" "^1.5.0"
|
||||
"@tybys/wasm-util" "^0.10.1"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||
@@ -554,7 +710,7 @@
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
@@ -572,7 +728,7 @@
|
||||
resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz"
|
||||
integrity sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==
|
||||
|
||||
"@photo-sphere-viewer/core@^5.13.2", "@photo-sphere-viewer/core@>=5.13.1":
|
||||
"@photo-sphere-viewer/core@^5.13.2":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.14.0.tgz"
|
||||
integrity sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==
|
||||
@@ -624,7 +780,7 @@
|
||||
utility-types "^3.11.0"
|
||||
zustand "^5.0.1"
|
||||
|
||||
"@react-three/fiber@^9.0.0", "@react-three/fiber@^9.1.2":
|
||||
"@react-three/fiber@^9.1.2":
|
||||
version "9.4.0"
|
||||
resolved "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz"
|
||||
integrity sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==
|
||||
@@ -656,11 +812,116 @@
|
||||
estree-walker "^2.0.2"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz#0f44a2f8668ed87b040b6fe659358ac9239da4db"
|
||||
integrity sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz#25b9a01deef6518a948431564c987bcb205274f5"
|
||||
integrity sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz"
|
||||
integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz#8e526417cd6f54daf1d0c04cf361160216581956"
|
||||
integrity sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz#0e7027054493f3409b1f219a3eac5efd128ef899"
|
||||
integrity sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz#72b204a920139e9ec3d331bd9cfd9a0c248ccb10"
|
||||
integrity sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz#ab1b522ebe5b7e06c99504cc38f6cd8b808ba41c"
|
||||
integrity sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz#f8cc30b638f1ee7e3d18eac24af47ea29d9beb00"
|
||||
integrity sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz#7af37a9e85f25db59dc8214172907b7e146c12cc"
|
||||
integrity sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz#a623eb0d3617c03b7a73716eb85c6e37b776f7e0"
|
||||
integrity sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz#76ea038b549c5c6c5f0d062942627c4066642ee2"
|
||||
integrity sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz#d9a4c3f0a3492bc78f6fdfe8131ac61c7359ccd5"
|
||||
integrity sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz#87ab033eebd1a9a1dd7b60509f6333ec1f82d994"
|
||||
integrity sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz#bda3eb67e1c993c1ba12bc9c2f694e7703958d9f"
|
||||
integrity sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz#f7bc10fbe096ab44694233dc42a2291ed5453d4b"
|
||||
integrity sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz#a151cb1234cc9b2cf5e8cfc02aa91436b8f9e278"
|
||||
integrity sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz#7859e196501cc3b3062d45d2776cfb4d2f3a9350"
|
||||
integrity sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==
|
||||
|
||||
"@rollup/rollup-openharmony-arm64@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz#85d0df7233734df31e547c1e647d2a5300b3bf30"
|
||||
integrity sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz#e62357d00458db17277b88adbf690bb855cac937"
|
||||
integrity sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz#fc7cd40f44834a703c1f1c3fe8bcc27ce476cd50"
|
||||
integrity sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz#1a22acfc93c64a64a48c42672e857ee51774d0d3"
|
||||
integrity sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.52.5":
|
||||
version "4.52.5"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz#1657f56326bbe0ac80eedc9f9c18fc1ddd24e107"
|
||||
integrity sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==
|
||||
|
||||
"@svgr/babel-plugin-add-jsx-attribute@8.0.0":
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz"
|
||||
@@ -715,7 +976,7 @@
|
||||
"@svgr/babel-plugin-transform-react-native-svg" "8.1.0"
|
||||
"@svgr/babel-plugin-transform-svg-component" "8.0.0"
|
||||
|
||||
"@svgr/core@*", "@svgr/core@^8.1.0":
|
||||
"@svgr/core@^8.1.0":
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz"
|
||||
integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==
|
||||
@@ -757,11 +1018,73 @@
|
||||
source-map-js "^1.2.1"
|
||||
tailwindcss "4.1.16"
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz#9bd16c0a08db20d7c93907a9bd1564e0255307eb"
|
||||
integrity sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz"
|
||||
integrity sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz#6193bafbb1a885795702f12bbef9cc5eb4cc550b"
|
||||
integrity sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz#0e2b064d71ba87a9001ac963be2752a8ddb64349"
|
||||
integrity sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz#8e80c959eeda81a08ed955e23eb6d228287b9672"
|
||||
integrity sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz#d5f54910920fc5808122515f5208c5ecc1a40545"
|
||||
integrity sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz#67cdb932230ac47bf3bf5415ccc92417b27020ee"
|
||||
integrity sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz#80ae0cfd8ebc970f239060ecdfdd07f6f6b14dce"
|
||||
integrity sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz#524e5b87e8e79a712de3d9bbb94d2fc2fa44391c"
|
||||
integrity sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz#dc31d6bc1f6c1e8119a335ae3f28deb4d7c560f2"
|
||||
integrity sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.5.0"
|
||||
"@emnapi/runtime" "^1.5.0"
|
||||
"@emnapi/wasi-threads" "^1.1.0"
|
||||
"@napi-rs/wasm-runtime" "^1.0.7"
|
||||
"@tybys/wasm-util" "^0.10.1"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz#f1f810cdb49dae8071d5edf0db5cc0da2ec6a7e8"
|
||||
integrity sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz#76dcda613578f06569c0a6015f39f12746a24dce"
|
||||
integrity sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==
|
||||
|
||||
"@tailwindcss/oxide@4.1.16":
|
||||
version "4.1.16"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz"
|
||||
@@ -801,6 +1124,13 @@
|
||||
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz"
|
||||
integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==
|
||||
|
||||
"@tybys/wasm-util@^0.10.1":
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
|
||||
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/babel__core@^7.20.5":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
||||
@@ -870,7 +1200,7 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@1.0.8":
|
||||
"@types/estree@*", "@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
|
||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||
@@ -904,7 +1234,7 @@
|
||||
resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz"
|
||||
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
||||
|
||||
"@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.15.24":
|
||||
"@types/node@^22.15.24":
|
||||
version "22.18.13"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz"
|
||||
integrity sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==
|
||||
@@ -951,7 +1281,7 @@
|
||||
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
|
||||
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
||||
|
||||
"@types/react@*", "@types/react@^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react@^18.2.25 || ^19", "@types/react@^19.1.2", "@types/react@^19.2.0", "@types/react@>=16.8", "@types/react@>=18", "@types/react@>=18.0.0":
|
||||
"@types/react@^19.1.2":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz"
|
||||
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
|
||||
@@ -970,7 +1300,7 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/three@*", "@types/three@>=0.134.0":
|
||||
"@types/three@*":
|
||||
version "0.180.0"
|
||||
resolved "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz"
|
||||
integrity sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==
|
||||
@@ -1018,7 +1348,7 @@
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@^8.46.2", "@typescript-eslint/parser@8.46.2":
|
||||
"@typescript-eslint/parser@8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz"
|
||||
integrity sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==
|
||||
@@ -1046,7 +1376,7 @@
|
||||
"@typescript-eslint/types" "8.46.2"
|
||||
"@typescript-eslint/visitor-keys" "8.46.2"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@^8.46.2", "@typescript-eslint/tsconfig-utils@8.46.2":
|
||||
"@typescript-eslint/tsconfig-utils@8.46.2", "@typescript-eslint/tsconfig-utils@^8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz"
|
||||
integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==
|
||||
@@ -1062,7 +1392,7 @@
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@^8.46.2", "@typescript-eslint/types@8.46.2":
|
||||
"@typescript-eslint/types@8.46.2", "@typescript-eslint/types@^8.46.2":
|
||||
version "8.46.2"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz"
|
||||
integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==
|
||||
@@ -1145,7 +1475,7 @@ acorn-jsx@^5.3.2:
|
||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0:
|
||||
acorn@^8.15.0:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
@@ -1249,7 +1579,7 @@ braces@^3.0.3:
|
||||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.24.0, "browserslist@>= 4.21.0":
|
||||
browserslist@^4.24.0:
|
||||
version "4.27.0"
|
||||
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz"
|
||||
integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==
|
||||
@@ -1546,7 +1876,7 @@ earcut@^3.0.0, earcut@^3.0.2:
|
||||
resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz"
|
||||
integrity sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==
|
||||
|
||||
easymde@^2.20.0, "easymde@>= 2.0.0 < 3.0.0":
|
||||
easymde@^2.20.0:
|
||||
version "2.20.0"
|
||||
resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz"
|
||||
integrity sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==
|
||||
@@ -1689,7 +2019,7 @@ eslint-visitor-keys@^4.2.1:
|
||||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.25.0, eslint@>=8.40:
|
||||
eslint@^9.25.0:
|
||||
version "9.38.0"
|
||||
resolved "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz"
|
||||
integrity sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==
|
||||
@@ -1815,12 +2145,7 @@ fastq@^1.6.0:
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
fdir@^6.4.4:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||
|
||||
fdir@^6.5.0:
|
||||
fdir@^6.4.4, fdir@^6.5.0:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||
@@ -2284,7 +2609,7 @@ its-fine@^2.0.0:
|
||||
dependencies:
|
||||
"@types/react-reconciler" "^0.28.9"
|
||||
|
||||
jiti@*, jiti@^2.6.1, jiti@>=1.21.0:
|
||||
jiti@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz"
|
||||
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
||||
@@ -2363,12 +2688,62 @@ lie@^3.0.2:
|
||||
dependencies:
|
||||
immediate "~3.0.5"
|
||||
|
||||
lightningcss-android-arm64@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz#6966b7024d39c94994008b548b71ab360eb3a307"
|
||||
integrity sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==
|
||||
|
||||
lightningcss-darwin-arm64@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz"
|
||||
integrity sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==
|
||||
|
||||
lightningcss@^1.21.0, lightningcss@1.30.2:
|
||||
lightningcss-darwin-x64@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz#5ce87e9cd7c4f2dcc1b713f5e8ee185c88d9b7cd"
|
||||
integrity sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==
|
||||
|
||||
lightningcss-freebsd-x64@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz#6ae1d5e773c97961df5cff57b851807ef33692a5"
|
||||
integrity sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz#62c489610c0424151a6121fa99d77731536cdaeb"
|
||||
integrity sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz#2a3661b56fe95a0cafae90be026fe0590d089298"
|
||||
integrity sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz#d7ddd6b26959245e026bc1ad9eb6aa983aa90e6b"
|
||||
integrity sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz#5a89814c8e63213a5965c3d166dff83c36152b1a"
|
||||
integrity sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz#808c2e91ce0bf5d0af0e867c6152e5378c049728"
|
||||
integrity sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz#ab4a8a8a2e6a82a4531e8bbb6bf0ff161ee6625a"
|
||||
integrity sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz#f01f382c8e0a27e1c018b0bee316d210eac43b6e"
|
||||
integrity sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==
|
||||
|
||||
lightningcss@1.30.2:
|
||||
version "1.30.2"
|
||||
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz"
|
||||
integrity sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==
|
||||
@@ -2812,7 +3187,7 @@ mobx-react-lite@^4.1.0:
|
||||
dependencies:
|
||||
use-sync-external-store "^1.4.0"
|
||||
|
||||
mobx@^6.13.7, mobx@^6.9.0:
|
||||
mobx@^6.13.7:
|
||||
version "6.15.0"
|
||||
resolved "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz"
|
||||
integrity sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==
|
||||
@@ -2993,12 +3368,12 @@ picomatch@^2.3.1:
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
"picomatch@^3 || ^4", picomatch@^4.0.2, picomatch@^4.0.3:
|
||||
picomatch@^4.0.2, picomatch@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||
|
||||
pixi.js@^8.10.1, pixi.js@^8.2.6:
|
||||
pixi.js@^8.10.1:
|
||||
version "8.14.0"
|
||||
resolved "https://registry.npmjs.org/pixi.js/-/pixi.js-8.14.0.tgz"
|
||||
integrity sha512-ituDiEBb1Oqx56RYwTtC6MjPUhPfF/i15fpUv5oEqmzC/ce3SaSumulJcOjKG7+y0J0Ekl9Rl4XTxaUw+MVFZw==
|
||||
@@ -3055,7 +3430,7 @@ promise-worker-transferable@^1.0.4:
|
||||
is-promise "^2.1.0"
|
||||
lie "^3.0.2"
|
||||
|
||||
prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.8.1:
|
||||
prop-types@^15.6.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@@ -3116,19 +3491,14 @@ rbush@^4.0.0:
|
||||
dependencies:
|
||||
quickselect "^3.0.0"
|
||||
|
||||
"react-dom@^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18 || ^19", "react-dom@^18.0.0 || ^19.0.0", react-dom@^19, react-dom@^19.0.0, react-dom@^19.1.0, react-dom@>=16.0.0, react-dom@>=16.13, react-dom@>=16.6.0, react-dom@>=16.8.2, react-dom@>=18:
|
||||
react-dom@^19.1.0:
|
||||
version "19.2.0"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz"
|
||||
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
|
||||
dependencies:
|
||||
scheduler "^0.27.0"
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-is@^16.7.0:
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
@@ -3162,7 +3532,7 @@ react-photo-sphere-viewer@^6.2.3:
|
||||
dependencies:
|
||||
eventemitter3 "^5.0.1"
|
||||
|
||||
react-reconciler@^0.31.0, react-reconciler@0.31.0:
|
||||
react-reconciler@0.31.0, react-reconciler@^0.31.0:
|
||||
version "0.31.0"
|
||||
resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz"
|
||||
integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==
|
||||
@@ -3189,7 +3559,7 @@ react-router-dom@^7.6.1:
|
||||
dependencies:
|
||||
react-router "7.9.4"
|
||||
|
||||
react-router@^7.9.4, react-router@7.9.4:
|
||||
react-router@7.9.4, react-router@^7.9.4:
|
||||
version "7.9.4"
|
||||
resolved "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz"
|
||||
integrity sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==
|
||||
@@ -3226,12 +3596,12 @@ react-use-measure@^2.1.7:
|
||||
resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz"
|
||||
integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
|
||||
|
||||
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18 || ^19", "react@^18.0 || ^19", "react@^18.0.0 || ^19.0.0", react@^19, react@^19.0.0, react@^19.1.0, react@^19.2.0, "react@>= 16.8.0", react@>=16.0.0, react@>=16.13, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.8.2, react@>=17.0, react@>=18, react@>=18.0.0, react@>=19.0.0:
|
||||
react@^19.1.0:
|
||||
version "19.2.0"
|
||||
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz"
|
||||
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
|
||||
|
||||
redux@^5.0.0, redux@^5.0.1:
|
||||
redux@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz"
|
||||
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
|
||||
@@ -3317,7 +3687,7 @@ rollup-plugin-visualizer@^6.0.5:
|
||||
source-map "^0.7.4"
|
||||
yargs "^17.5.1"
|
||||
|
||||
rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^4.34.9, "rollup@2.x || 3.x || 4.x":
|
||||
rollup@^4.34.9:
|
||||
version "4.52.5"
|
||||
resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz"
|
||||
integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==
|
||||
@@ -3503,7 +3873,7 @@ svg-parser@^2.0.4:
|
||||
resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz"
|
||||
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
||||
|
||||
tailwindcss@^4.1.8, "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1", tailwindcss@4.1.16:
|
||||
tailwindcss@4.1.16, tailwindcss@^4.1.8:
|
||||
version "4.1.16"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz"
|
||||
integrity sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==
|
||||
@@ -3535,7 +3905,7 @@ three@^0.170.0:
|
||||
resolved "https://registry.npmjs.org/three/-/three-0.170.0.tgz"
|
||||
integrity sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==
|
||||
|
||||
three@^0.177.0, "three@>= 0.159.0", three@>=0.125.0, three@>=0.126.1, three@>=0.128.0, three@>=0.134.0, three@>=0.137, three@>=0.156, three@>=0.159:
|
||||
three@^0.177.0:
|
||||
version "0.177.0"
|
||||
resolved "https://registry.npmjs.org/three/-/three-0.177.0.tgz"
|
||||
integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==
|
||||
@@ -3605,9 +3975,9 @@ ts-api-utils@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
|
||||
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
||||
|
||||
tslib@^2.0.3:
|
||||
tslib@^2.0.3, tslib@^2.4.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tunnel-rat@^0.1.2:
|
||||
@@ -3634,7 +4004,7 @@ typescript-eslint@^8.30.1:
|
||||
"@typescript-eslint/typescript-estree" "8.46.2"
|
||||
"@typescript-eslint/utils" "8.46.2"
|
||||
|
||||
typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@>=4.9.5, typescript@~5.8.3:
|
||||
typescript@~5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
|
||||
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
||||
@@ -3715,7 +4085,7 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0, use-sync-external-store@>=1.2.0:
|
||||
use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz"
|
||||
integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
|
||||
@@ -3737,6 +4107,11 @@ utility-types@^3.11.0:
|
||||
resolved "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz"
|
||||
integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==
|
||||
|
||||
uuid@^13.0.0:
|
||||
version "13.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8"
|
||||
integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==
|
||||
|
||||
vfile-location@^5.0.0:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz"
|
||||
@@ -3770,7 +4145,7 @@ vite-plugin-svgr@^4.5.0:
|
||||
"@svgr/core" "^8.1.0"
|
||||
"@svgr/plugin-jsx" "^8.1.0"
|
||||
|
||||
"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^6.3.5, vite@>=2.6.0:
|
||||
vite@^6.3.5:
|
||||
version "6.4.1"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz"
|
||||
integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
|
||||
|
||||
Reference in New Issue
Block a user