diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index 8b13789..0000000
--- a/src/App.tsx
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx
index 1bf8fbe..6082418 100644
--- a/src/app/router/index.tsx
+++ b/src/app/router/index.tsx
@@ -26,7 +26,7 @@ import {
CountryCreatePage,
CityCreatePage,
CarrierCreatePage,
- // VehicleCreatePage,
+ VehicleCreatePage,
CountryEditPage,
CityEditPage,
UserCreatePage,
@@ -40,6 +40,7 @@ import {
RoutePreview,
RouteEditPage,
ArticlePreviewPage,
+ CountryAddPage,
} from "@pages";
import { authStore, createSightStore, editSightStore } from "@shared";
import { Layout } from "@widgets";
@@ -57,7 +58,7 @@ import {
const PublicRoute = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated } = authStore;
if (isAuthenticated) {
- return ;
+ return ;
}
return <>{children}>;
};
@@ -69,7 +70,7 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
return ;
}
if (location.pathname === "/") {
- return ;
+ return ;
}
return <>{children}>;
};
@@ -134,6 +135,7 @@ const router = createBrowserRouter([
// Country
{ path: "country", element: },
{ path: "country/create", element: },
+ { path: "country/add", element: },
// { path: "country/:id", element: },
{ path: "country/:id/edit", element: },
// City
@@ -166,7 +168,7 @@ const router = createBrowserRouter([
{ path: "station/:id/edit", element: },
// Vehicle
// { path: "vehicle", element: },
- // { path: "vehicle/create", element: },
+ { path: "vehicle/create", element: },
// { path: "vehicle/:id", element: },
// { path: "vehicle/:id/edit", element: },
// Article
diff --git a/src/pages/Article/ArticleCreatePage/index.tsx b/src/pages/Article/ArticleCreatePage/index.tsx
index 36c54b6..ac5cfaf 100644
--- a/src/pages/Article/ArticleCreatePage/index.tsx
+++ b/src/pages/Article/ArticleCreatePage/index.tsx
@@ -2,14 +2,19 @@ import React from "react";
import { useNavigate } from "react-router-dom";
import { ArrowLeft } from "lucide-react";
import { LanguageSwitcher } from "@widgets";
+import { articlesStore } from "@shared";
const ArticleCreatePage: React.FC = () => {
const navigate = useNavigate();
+ const { articleData } = articlesStore;
+
return (
-
Создание статьи
+
+ {articleData?.ru?.heading || "Создание статьи"}
+
diff --git a/src/pages/Article/ArticleEditPage/index.tsx b/src/pages/Article/ArticleEditPage/index.tsx
index b33980d..47cd0ff 100644
--- a/src/pages/Article/ArticleEditPage/index.tsx
+++ b/src/pages/Article/ArticleEditPage/index.tsx
@@ -2,7 +2,7 @@ import React, { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft } from "lucide-react";
import { LanguageSwitcher } from "@widgets";
-import { articlesStore } from "@shared";
+import { articlesStore, languageStore } from "@shared";
import { observer } from "mobx-react-lite";
const ArticleEditPage: React.FC = observer(() => {
@@ -11,6 +11,11 @@ const ArticleEditPage: React.FC = observer(() => {
const { articleData, getArticle } = articlesStore;
+ useEffect(() => {
+ // Устанавливаем русский язык при загрузке страницы
+ languageStore.setLanguage("ru");
+ }, []);
+
useEffect(() => {
if (id) {
// Fetch data for all languages
diff --git a/src/pages/Article/ArticleListPage/index.tsx b/src/pages/Article/ArticleListPage/index.tsx
index fd410a1..b0b8aea 100644
--- a/src/pages/Article/ArticleListPage/index.tsx
+++ b/src/pages/Article/ArticleListPage/index.tsx
@@ -1,10 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
+import { ruRU } from "@mui/x-data-grid/locales";
import { articlesStore, languageStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Trash2, Eye, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { DeleteModal, LanguageSwitcher } from "@widgets";
+import { Box, CircularProgress } from "@mui/material";
export const ArticleListPage = observer(() => {
const { articleList, getArticleList, deleteArticles } = articlesStore;
@@ -14,9 +16,15 @@ export const ArticleListPage = observer(() => {
const [rowId, setRowId] = useState(null);
const { language } = languageStore;
const [ids, setIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
- getArticleList();
+ const fetchArticles = async () => {
+ setIsLoading(true);
+ await getArticleList();
+ setIsLoading(false);
+ };
+ fetchArticles();
}, [language]);
const columns: GridColDef[] = [
@@ -93,10 +101,21 @@ export const ArticleListPage = observer(() => {
columns={columns}
hideFooterPagination
checkboxSelection
+ loading={isLoading}
+ localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as number[]);
}}
hideFooter
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? : "Нет статей"}
+
+ ),
+ }}
/>
diff --git a/src/pages/Carrier/CarrierCreatePage/index.tsx b/src/pages/Carrier/CarrierCreatePage/index.tsx
index 06a8f92..f447921 100644
--- a/src/pages/Carrier/CarrierCreatePage/index.tsx
+++ b/src/pages/Carrier/CarrierCreatePage/index.tsx
@@ -38,12 +38,14 @@ export const CarrierCreatePage = observer(() => {
useEffect(() => {
cityStore.getCities("ru");
mediaStore.getMedia();
+ languageStore.setLanguage("ru");
}, []);
const handleCreate = async () => {
try {
setIsLoading(true);
await carrierStore.createCarrier();
+
toast.success("Перевозчик успешно создан");
navigate("/carrier");
} catch (error) {
@@ -229,6 +231,8 @@ export const CarrierCreatePage = observer(() => {
setIsUploadMediaOpen(false)}
+ contextObjectName={createCarrierData[language].full_name}
+ contextType="carrier"
afterUpload={handleMediaSelect}
hardcodeType={activeMenuType}
/>
diff --git a/src/pages/Carrier/CarrierEditPage/index.tsx b/src/pages/Carrier/CarrierEditPage/index.tsx
index 20bea5f..e914ba2 100644
--- a/src/pages/Carrier/CarrierEditPage/index.tsx
+++ b/src/pages/Carrier/CarrierEditPage/index.tsx
@@ -14,11 +14,11 @@ import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify";
import { carrierStore, cityStore, mediaStore, languageStore } from "@shared";
import { useState, useEffect } from "react";
-import { ImageUploadCard, LanguageSwitcher } from "@widgets";
+import { ImageUploadCard, LanguageSwitcher, DeleteModal } from "@widgets";
import {
SelectMediaDialog,
- UploadMediaDialog,
PreviewMediaDialog,
+ UploadMediaDialog,
} from "@shared";
export const CarrierEditPage = observer(() => {
@@ -32,6 +32,7 @@ export const CarrierEditPage = observer(() => {
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [mediaId, setMediaId] = useState("");
+ const [isDeleteLogoModalOpen, setIsDeleteLogoModalOpen] = useState(false);
const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
@@ -72,6 +73,9 @@ export const CarrierEditPage = observer(() => {
mediaStore.getMedia();
})();
+
+ // Устанавливаем русский язык при загрузке страницы
+ languageStore.setLanguage("ru");
}, [id]);
const handleEdit = async () => {
@@ -209,15 +213,7 @@ export const CarrierEditPage = observer(() => {
setMediaId(selectedMedia?.id ?? "");
}}
onDeleteImageClick={() => {
- setEditCarrierData(
- editCarrierData[language].full_name,
- editCarrierData[language].short_name,
- editCarrierData.city_id,
- editCarrierData[language].slogan,
- "",
- language
- );
- setActiveMenuType(null);
+ setIsDeleteLogoModalOpen(true);
}}
onSelectFileClick={() => {
setActiveMenuType("image");
@@ -244,7 +240,7 @@ export const CarrierEditPage = observer(() => {
{isLoading ? (
) : (
- "Обновить"
+ "Сохранить"
)}
@@ -259,6 +255,8 @@ export const CarrierEditPage = observer(() => {
setIsUploadMediaOpen(false)}
+ contextObjectName={editCarrierData[language].full_name}
+ contextType="carrier"
afterUpload={handleMediaSelect}
hardcodeType={activeMenuType}
/>
@@ -268,6 +266,23 @@ export const CarrierEditPage = observer(() => {
onClose={() => setIsPreviewMediaOpen(false)}
mediaId={mediaId}
/>
+
+ {
+ setEditCarrierData(
+ editCarrierData[language].full_name,
+ editCarrierData[language].short_name,
+ editCarrierData.city_id,
+ editCarrierData[language].slogan,
+ "",
+ language
+ );
+ setIsDeleteLogoModalOpen(false);
+ }}
+ onCancel={() => setIsDeleteLogoModalOpen(false)}
+ edit
+ />
);
});
diff --git a/src/pages/Carrier/CarrierListPage/index.tsx b/src/pages/Carrier/CarrierListPage/index.tsx
index 24c035b..6802a35 100644
--- a/src/pages/Carrier/CarrierListPage/index.tsx
+++ b/src/pages/Carrier/CarrierListPage/index.tsx
@@ -1,10 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
+import { ruRU } from "@mui/x-data-grid/locales";
import { carrierStore, cityStore, languageStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
+import { Box, CircularProgress } from "@mui/material";
export const CarrierListPage = observer(() => {
const { carriers, getCarriers, deleteCarrier } = carrierStore;
@@ -14,15 +16,19 @@ export const CarrierListPage = observer(() => {
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState(null);
const [ids, setIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => {
- (async () => {
+ const fetchData = async () => {
+ setIsLoading(true);
await getCities("ru");
await getCities("en");
await getCities("zh");
await getCarriers(language);
- })();
+ setIsLoading(false);
+ };
+ fetchData();
}, [language]);
const columns: GridColDef[] = [
@@ -63,11 +69,13 @@ export const CarrierListPage = observer(() => {
headerName: "Город",
flex: 1,
renderCell: (params: GridRenderCellParams) => {
+ const city = cities[language]?.data.find(
+ (city) => city.id == params.value
+ );
return (
- {params.value ? (
- cities[language].data.find((city) => city.id == params.value)
- ?.name
+ {city && city.name ? (
+ city.name
) : (
)}
@@ -136,12 +144,24 @@ export const CarrierListPage = observer(() => {
{
- setIds(Array.from(newSelection.ids) as number[]);
- }}
hideFooter
+ checkboxSelection
+ loading={isLoading}
+ localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
+ onRowSelectionModelChange={(newSelection) => {
+ setIds(Array.from(newSelection.ids as unknown as number[]));
+ }}
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? (
+
+ ) : (
+ "Нет перевозчиков"
+ )}
+
+ ),
+ }}
/>
diff --git a/src/pages/City/CityCreatePage/index.tsx b/src/pages/City/CityCreatePage/index.tsx
index ac5e908..0f26a89 100644
--- a/src/pages/City/CityCreatePage/index.tsx
+++ b/src/pages/City/CityCreatePage/index.tsx
@@ -157,15 +157,8 @@ export const CityCreatePage = observer(() => {
setIsUploadMediaOpen(true);
setActiveMenuType("image");
}}
- setHardcodeType={(type) => {
- setActiveMenuType(
- type as
- | "thumbnail"
- | "watermark_lu"
- | "watermark_rd"
- | "image"
- | null
- );
+ setHardcodeType={() => {
+ setActiveMenuType("image");
}}
/>
@@ -195,6 +188,8 @@ export const CityCreatePage = observer(() => {
setIsUploadMediaOpen(false)}
+ contextObjectName={createCityData[language]?.name}
+ contextType="city"
afterUpload={handleMediaSelect}
hardcodeType={
activeMenuType as "thumbnail" | "watermark_lu" | "watermark_rd" | null
diff --git a/src/pages/City/CityEditPage/index.tsx b/src/pages/City/CityEditPage/index.tsx
index 4fa3199..4d7b0b4 100644
--- a/src/pages/City/CityEditPage/index.tsx
+++ b/src/pages/City/CityEditPage/index.tsx
@@ -43,6 +43,11 @@ export const CityEditPage = observer(() => {
const { getCountries } = countryStore;
const { getMedia, getOneMedia } = mediaStore;
+ useEffect(() => {
+ // Устанавливаем русский язык при загрузке страницы
+ languageStore.setLanguage("ru");
+ }, []);
+
const handleEdit = async () => {
try {
setIsLoading(true);
@@ -58,6 +63,7 @@ export const CityEditPage = observer(() => {
useEffect(() => {
(async () => {
if (id) {
+ await getCountries("ru");
// Fetch data for all languages
const ruData = await getCity(id as string, "ru");
const enData = await getCity(id as string, "en");
@@ -69,7 +75,7 @@ export const CityEditPage = observer(() => {
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
await getOneMedia(ruData.arms as string);
- await getCountries("ru");
+
await getMedia();
}
})();
@@ -174,15 +180,8 @@ export const CityEditPage = observer(() => {
setIsUploadMediaOpen(true);
setActiveMenuType("image");
}}
- setHardcodeType={(type) => {
- setActiveMenuType(
- type as
- | "thumbnail"
- | "watermark_lu"
- | "watermark_rd"
- | "image"
- | null
- );
+ setHardcodeType={() => {
+ setActiveMenuType("image");
}}
/>
@@ -199,7 +198,7 @@ export const CityEditPage = observer(() => {
{isLoading ? (
) : (
- "Обновить"
+ "Сохранить"
)}
@@ -214,6 +213,8 @@ export const CityEditPage = observer(() => {
setIsUploadMediaOpen(false)}
+ contextObjectName={editCityData[language].name}
+ contextType="city"
afterUpload={handleMediaSelect}
hardcodeType={
activeMenuType as
diff --git a/src/pages/City/CityListPage/index.tsx b/src/pages/City/CityListPage/index.tsx
index 528b179..cd3c288 100644
--- a/src/pages/City/CityListPage/index.tsx
+++ b/src/pages/City/CityListPage/index.tsx
@@ -1,11 +1,13 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
-import { languageStore, cityStore } from "@shared";
+import { ruRU } from "@mui/x-data-grid/locales";
+import { languageStore, cityStore, countryStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
import { toast } from "react-toastify";
+import { Box, CircularProgress } from "@mui/material";
export const CityListPage = observer(() => {
const { cities, getCities, deleteCity } = cityStore;
@@ -14,12 +16,43 @@ export const CityListPage = observer(() => {
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState(null);
const [ids, setIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [rows, setRows] = useState([]);
const { language } = languageStore;
useEffect(() => {
- getCities(language);
+ const fetchData = async () => {
+ setIsLoading(true);
+ await countryStore.getCountries("ru");
+ await countryStore.getCountries("en");
+ await countryStore.getCountries("zh");
+ await getCities(language);
+ setIsLoading(false);
+ };
+ fetchData();
}, [language]);
+ useEffect(() => {
+ let newRows = cities[language]?.data?.map((city) => ({
+ id: city.id,
+ name: city.name,
+ country: city.country_code,
+ }));
+
+ let newRows2: any[] = [];
+ for (const city of newRows) {
+ const name = countryStore.countries[language]?.data?.find(
+ (country) => country.code === city.country
+ )?.name;
+ if (name) {
+ newRows2.push(city);
+ }
+ }
+
+ setRows(newRows2 || []);
+ console.log(newRows2);
+ }, [cities, countryStore.countries, language, isLoading]);
+
const columns: GridColDef[] = [
{
field: "country",
@@ -29,7 +62,9 @@ export const CityListPage = observer(() => {
return (
{params.value ? (
- params.value
+ countryStore.countries[language]?.data?.find(
+ (country) => country.code === params.value
+ )?.name
) : (
)}
@@ -83,12 +118,6 @@ export const CityListPage = observer(() => {
},
];
- const rows = cities[language]?.data?.map((city) => ({
- id: city.id,
- name: city.name,
- country: city.country,
- }));
-
return (
<>
@@ -115,12 +144,20 @@ export const CityListPage = observer(() => {
{
setIds(Array.from(newSelection.ids as unknown as number[]));
}}
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? : "Нет городов"}
+
+ ),
+ }}
/>
diff --git a/src/pages/Country/CountryAddPage/index.tsx b/src/pages/Country/CountryAddPage/index.tsx
new file mode 100644
index 0000000..d6ef176
--- /dev/null
+++ b/src/pages/Country/CountryAddPage/index.tsx
@@ -0,0 +1,115 @@
+import {
+ Button,
+ Paper,
+ TextField,
+ Autocomplete,
+ FormControl,
+} from "@mui/material";
+import { observer } from "mobx-react-lite";
+import { ArrowLeft, Save } from "lucide-react";
+import { Loader2 } from "lucide-react";
+import { useNavigate } from "react-router-dom";
+import { toast } from "react-toastify";
+import {
+ countryStore,
+ RU_COUNTRIES,
+ EN_COUNTRIES,
+ ZH_COUNTRIES,
+} from "@shared";
+import { useState } from "react";
+
+export const CountryAddPage = observer(() => {
+ const navigate = useNavigate();
+ const [isLoading, setIsLoading] = useState(false);
+ const { createCountryData, setCountryData, createCountry } = countryStore;
+
+ const handleCountryCodeChange = (code: string) => {
+ const ruCountry = RU_COUNTRIES.find((c) => c.code === code);
+ const enCountry = EN_COUNTRIES.find((c) => c.code === code);
+ const zhCountry = ZH_COUNTRIES.find((c) => c.code === code);
+
+ if (ruCountry && enCountry && zhCountry) {
+ setCountryData(code, ruCountry.name, "ru");
+ setCountryData(code, enCountry.name, "en");
+ setCountryData(code, zhCountry.name, "zh");
+ }
+ };
+
+ const handleCreate = async () => {
+ try {
+ setIsLoading(true);
+ await createCountry();
+ toast.success("Страна успешно создана");
+ navigate("/country");
+ } catch (error) {
+ toast.error("Ошибка при создании страны");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ c.code === createCountryData.code) ||
+ null
+ }
+ onChange={(_, newValue) => {
+ if (newValue) {
+ handleCountryCodeChange(newValue.code);
+ }
+ }}
+ options={RU_COUNTRIES}
+ getOptionLabel={(option) => `${option.code} - ${option.name}`}
+ renderInput={(params) => (
+
+ )}
+ filterOptions={(options, { inputValue }) => {
+ const searchValue = inputValue.toUpperCase();
+ return options.filter(
+ (option) =>
+ option.code.includes(searchValue) ||
+ option.name.toLowerCase().includes(inputValue.toLowerCase())
+ );
+ }}
+ />
+
+
+
}
+ onClick={handleCreate}
+ disabled={isLoading || !createCountryData.code}
+ >
+ {isLoading ? (
+
+ ) : (
+ "Создать"
+ )}
+
+
+
+ );
+});
diff --git a/src/pages/Country/CountryEditPage/index.tsx b/src/pages/Country/CountryEditPage/index.tsx
index cc280c9..e763aaa 100644
--- a/src/pages/Country/CountryEditPage/index.tsx
+++ b/src/pages/Country/CountryEditPage/index.tsx
@@ -16,6 +16,11 @@ export const CountryEditPage = observer(() => {
const { editCountryData, editCountry, getCountry, setEditCountryData } =
countryStore;
+ useEffect(() => {
+ // Устанавливаем русский язык при загрузке страницы
+ languageStore.setLanguage("ru");
+ }, []);
+
const handleEdit = async () => {
try {
setIsLoading(true);
@@ -88,7 +93,7 @@ export const CountryEditPage = observer(() => {
{isLoading ? (
) : (
- "Обновить"
+ "Сохранить"
)}
diff --git a/src/pages/Country/CountryListPage/index.tsx b/src/pages/Country/CountryListPage/index.tsx
index 0e06b30..4b3f0af 100644
--- a/src/pages/Country/CountryListPage/index.tsx
+++ b/src/pages/Country/CountryListPage/index.tsx
@@ -1,22 +1,30 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
+import { ruRU } from "@mui/x-data-grid/locales";
import { countryStore, languageStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
-import { Pencil, Trash2, Minus } from "lucide-react";
-import { useNavigate } from "react-router-dom";
+import { Trash2, Minus } from "lucide-react";
+
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
+import { Box, CircularProgress } from "@mui/material";
export const CountryListPage = observer(() => {
const { countries, getCountries, deleteCountry } = countryStore;
- const navigate = useNavigate();
+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState(null);
const [ids, setIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => {
- getCountries(language);
+ const fetchCountries = async () => {
+ setIsLoading(true);
+ await getCountries(language);
+ setIsLoading(false);
+ };
+ fetchCountries();
}, [language]);
const columns: GridColDef[] = [
@@ -45,11 +53,11 @@ export const CountryListPage = observer(() => {
renderCell: (params: GridRenderCellParams) => {
return (
-
+ */}
{/*
*/}
@@ -81,7 +89,7 @@ export const CountryListPage = observer(() => {
Страны
-
+
{
{
- console.log(newSelection);
setIds(Array.from(newSelection.ids as unknown as number[]));
}}
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? : "Нет стран"}
+
+ ),
+ }}
/>
diff --git a/src/pages/Country/index.ts b/src/pages/Country/index.ts
index f80e3eb..18c6ad6 100644
--- a/src/pages/Country/index.ts
+++ b/src/pages/Country/index.ts
@@ -2,3 +2,4 @@ export * from "./CountryListPage";
export * from "./CountryPreviewPage";
export * from "./CountryCreatePage";
export * from "./CountryEditPage";
+export * from "./CountryAddPage";
diff --git a/src/pages/EditSightPage/index.tsx b/src/pages/EditSightPage/index.tsx
index c38b68e..48c3f42 100644
--- a/src/pages/EditSightPage/index.tsx
+++ b/src/pages/EditSightPage/index.tsx
@@ -19,7 +19,7 @@ export const EditSightPage = observer(() => {
const { getArticles } = articlesStore;
const { id } = useParams();
- const { getRuCities } = cityStore;
+ const { getCities } = cityStore;
let blocker = useBlocker(
({ currentLocation, nextLocation }) =>
@@ -33,13 +33,13 @@ 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");
- await getRuCities();
}
};
fetchData();
diff --git a/src/pages/LoginPage/index.tsx b/src/pages/LoginPage/index.tsx
index 77fa0eb..4318742 100644
--- a/src/pages/LoginPage/index.tsx
+++ b/src/pages/LoginPage/index.tsx
@@ -5,9 +5,12 @@ import {
Typography,
Alert,
CircularProgress,
+ FormControlLabel,
+ Checkbox,
+ Paper,
} from "@mui/material";
-import { authStore } from "@shared";
-import { useState } from "react";
+import { authStore, userStore } from "@shared";
+import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
@@ -15,9 +18,21 @@ export const LoginPage = () => {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
+ const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState
(null);
const [isLoading, setIsLoading] = useState(false);
const { login } = authStore;
+ const { getUsers } = userStore;
+ useEffect(() => {
+ // Load saved credentials if they exist
+ const savedEmail = localStorage.getItem("rememberedEmail");
+ const savedPassword = localStorage.getItem("rememberedPassword");
+ if (savedEmail && savedPassword) {
+ setEmail(savedEmail);
+ setPassword(savedPassword);
+ setRememberMe(true);
+ }
+ }, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -26,7 +41,18 @@ export const LoginPage = () => {
try {
await login(email, password);
- navigate("/sight");
+
+ // Save or clear credentials based on remember me checkbox
+ if (rememberMe) {
+ localStorage.setItem("rememberedEmail", email);
+ localStorage.setItem("rememberedPassword", password);
+ } else {
+ localStorage.removeItem("rememberedEmail");
+ localStorage.removeItem("rememberedPassword");
+ }
+
+ navigate("/map");
+ await getUsers();
toast.success("Вход в систему выполнен успешно");
} catch (err) {
setError(
@@ -47,73 +73,102 @@ export const LoginPage = () => {
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
- minHeight: "100vh",
+ width: "100vw",
+ height: "100vh",
gap: 3,
p: 3,
+ backgroundImage: "url('/login-bg.png')",
+ backgroundSize: "cover",
+ backgroundPosition: "center",
+ backgroundRepeat: "no-repeat",
}}
>
-
- Вход в систему
-
-
- {error && (
-
- {error}
-
- )}
- setEmail(e.target.value)}
- disabled={isLoading}
- error={!!error}
- />
- setPassword(e.target.value)}
- disabled={isLoading}
- error={!!error}
- />
-
-
+ {error && (
+
+ {error}
+
+ )}
+ setEmail(e.target.value)}
+ disabled={isLoading}
+ error={!!error}
+ />
+ setPassword(e.target.value)}
+ disabled={isLoading}
+ error={!!error}
+ />
+ setRememberMe(e.target.checked)}
+ disabled={isLoading}
+ />
+ }
+ label="Запомнить пароль"
+ />
+
+
+
);
};
diff --git a/src/pages/MapPage/index.tsx b/src/pages/MapPage/index.tsx
index de06029..613d7f8 100644
--- a/src/pages/MapPage/index.tsx
+++ b/src/pages/MapPage/index.tsx
@@ -11,7 +11,12 @@ import TileLayer from "ol/layer/Tile";
import OSM from "ol/source/OSM";
import VectorLayer from "ol/layer/Vector";
import VectorSource, { VectorSourceEvent } from "ol/source/Vector";
-import { Draw, Modify, Select } from "ol/interaction";
+import {
+ Draw,
+ Modify,
+ Select,
+ defaults as defaultInteractions,
+} from "ol/interaction";
import { DrawEvent } from "ol/interaction/Draw";
import { SelectEvent } from "ol/interaction/Select";
import {
@@ -22,7 +27,7 @@ import {
RegularShape,
} from "ol/style";
import { Point, LineString, Geometry, Polygon } from "ol/geom";
-import { transform } from "ol/proj";
+import { transform, toLonLat } from "ol/proj";
import { GeoJSON } from "ol/format";
import {
Bus,
@@ -43,7 +48,26 @@ import Layer from "ol/layer/Layer";
import Source from "ol/source/Source";
import { FeatureLike } from "ol/Feature";
+// --- CUSTOM SCROLLBAR STYLES ---
+const scrollbarHideStyles = `
+ .scrollbar-hide {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+ .scrollbar-hide::-webkit-scrollbar {
+ display: none;
+ }
+`;
+
+// Inject styles into document head
+if (typeof document !== "undefined") {
+ const styleElement = document.createElement("style");
+ styleElement.textContent = scrollbarHideStyles;
+ document.head.appendChild(styleElement);
+}
+
// --- MAP STORE ---
+// @ts-ignore
import { languageInstance } from "@shared"; // Убедитесь, что этот импорт правильный
import { makeAutoObservable } from "mobx";
@@ -79,14 +103,12 @@ class MapStore {
getRoutes = async () => {
const response = await languageInstance("ru").get("/route");
- console.log(response.data);
const routesIds = response.data.map((route: any) => route.id);
- for (const id of routesIds) {
- const route = await languageInstance("ru").get(`/route/${id}`);
- this.routes.push({
- ...route.data,
- });
- }
+ const routePromises = routesIds.map((id: number) =>
+ languageInstance("ru").get(`/route/${id}`)
+ );
+ const routeResponses = await Promise.all(routePromises);
+ this.routes = routeResponses.map((res) => res.data);
this.routes = this.routes.sort((a, b) =>
a.route_number.localeCompare(b.route_number)
@@ -124,14 +146,14 @@ class MapStore {
if (featureType === "station") {
data = {
- name: properties.name || "Новая станция",
+ name: properties.name || "Новая остановка",
latitude: geometry.coordinates[1],
longitude: geometry.coordinates[0],
};
} else if (featureType === "route") {
data = {
- route_number: properties.name || "Новый маршрут",
- path: geometry.coordinates,
+ route_number: properties.name || "Маршрут 1",
+ path: geometry.coordinates.map((c: any) => [c[1], c[0]]),
center_latitude: geometry.coordinates[0][1],
center_longitude: geometry.coordinates[0][0],
};
@@ -172,7 +194,7 @@ class MapStore {
} else if (featureType === "route") {
data = {
route_number: properties.name,
- path: geometry.coordinates.map((coord: any) => [coord[1], coord[0]]), // Swap coordinates
+ path: geometry.coordinates.map((coord: any) => [coord[1], coord[0]]),
};
} else if (featureType === "sight") {
data = {
@@ -185,47 +207,50 @@ class MapStore {
throw new Error(`Unknown feature type for update: ${featureType}`);
}
+ const findOldData = (store: any[], id: number) =>
+ store.find((f: any) => f.id === id);
let oldData;
- if (featureType === "route") {
- oldData = this.routes.find((f) => f.id === numericId);
- } else if (featureType === "station") {
- oldData = this.stations.find((f) => f.id === numericId);
- } else if (featureType === "sight") {
- oldData = this.sights.find((f) => f.id === numericId);
+ if (featureType === "route") oldData = findOldData(this.routes, numericId);
+ else if (featureType === "station")
+ oldData = findOldData(this.stations, numericId);
+ else if (featureType === "sight")
+ oldData = findOldData(this.sights, numericId);
+
+ if (!oldData) {
+ throw new Error(
+ `Could not find old data for ${featureType} with id ${numericId}`
+ );
}
- let response;
- if (featureType !== "route") {
- response = await languageInstance("ru").patch(
- `/${featureType}/${numericId}`,
- {
- ...oldData,
- latitude: data.latitude,
- longitude: data.longitude,
- }
- );
+ let requestBody: any;
+ if (featureType === "route") {
+ requestBody = {
+ ...oldData,
+ ...data,
+ center_latitude:
+ data.path.length > 0 ? data.path[0][0] : oldData.center_latitude,
+ center_longitude:
+ data.path.length > 0 ? data.path[0][1] : oldData.center_longitude,
+ };
} else {
- response = await languageInstance("ru").patch(
- `/${featureType}/${numericId}`,
- {
- ...oldData,
- path: data.path,
- center_latitude: data.path[0][0], // First coordinate is latitude
- center_longitude: data.path[0][1], // Second coordinate is longitude
- }
- );
+ requestBody = { ...oldData, ...data };
}
- if (featureType === "route") {
- const index = this.routes.findIndex((f) => f.id === numericId);
- if (index !== -1) this.routes[index] = response.data;
- } else if (featureType === "station") {
- const index = this.stations.findIndex((f) => f.id === numericId);
- if (index !== -1) this.stations[index] = response.data;
- } else if (featureType === "sight") {
- const index = this.sights.findIndex((f) => f.id === numericId);
- if (index !== -1) this.sights[index] = response.data;
- }
+ const response = await languageInstance("ru").patch(
+ `/${featureType}/${numericId}`,
+ requestBody
+ );
+
+ const updateStore = (store: any[], updatedItem: any) => {
+ const index = store.findIndex((f) => f.id === updatedItem.id);
+ if (index !== -1) store[index] = updatedItem;
+ else store.push(updatedItem);
+ };
+
+ if (featureType === "route") updateStore(this.routes, response.data);
+ else if (featureType === "station")
+ updateStore(this.stations, response.data);
+ else if (featureType === "sight") updateStore(this.sights, response.data);
return response.data;
};
@@ -239,6 +264,71 @@ export const mapConfig = {
zoom: 13,
};
+// --- MAP POSITION STORAGE ---
+const MAP_POSITION_KEY = "mapPosition";
+const ACTIVE_SECTION_KEY = "mapActiveSection";
+
+interface MapPosition {
+ center: [number, number];
+ zoom: number;
+}
+
+const getStoredMapPosition = (): MapPosition | null => {
+ try {
+ const stored = localStorage.getItem(MAP_POSITION_KEY);
+ if (stored) {
+ const position = JSON.parse(stored);
+ // Validate the stored data
+ if (
+ position &&
+ Array.isArray(position.center) &&
+ position.center.length === 2 &&
+ typeof position.zoom === "number" &&
+ position.zoom >= 0 &&
+ position.zoom <= 20
+ ) {
+ return position;
+ }
+ }
+ } catch (error) {
+ console.warn("Failed to parse stored map position:", error);
+ }
+ return null;
+};
+
+const saveMapPosition = (position: MapPosition): void => {
+ try {
+ localStorage.setItem(MAP_POSITION_KEY, JSON.stringify(position));
+ } catch (error) {
+ console.warn("Failed to save map position:", error);
+ }
+};
+
+// --- ACTIVE SECTION STORAGE ---
+const getStoredActiveSection = (): string | null => {
+ try {
+ const stored = localStorage.getItem(ACTIVE_SECTION_KEY);
+ if (stored) {
+ return stored;
+ }
+ } catch (error) {
+ console.warn("Failed to get stored active section:", error);
+ }
+ return null;
+};
+
+const saveActiveSection = (section: string | null): void => {
+ try {
+ if (section) {
+ localStorage.setItem(ACTIVE_SECTION_KEY, section);
+ } else {
+ localStorage.removeItem(ACTIVE_SECTION_KEY);
+ }
+ } catch (error) {
+ console.warn("Failed to save active section:", error);
+ }
+};
+
// --- SVG ICONS ---
const EditIcon = () => (
+
-
- {filteredFeatures.length === 0 && searchQuery ? (
-
- Ничего не найдено.
-
- ) : (
- sections.map(
- (s) =>
- (s.count > 0 || !searchQuery) && (
-
+ Ничего не найдено.
+
+ ) : (
+ sections.map(
+ (s) =>
+ (s.count > 0 || !searchQuery) && (
+
+
+
+ )
+ )
+ )}
-
- {selectedIds.size > 0 && (
+
+ {selectedIds.size > 0 && (
+
= ({
Удалить выбранное ({selectedIds.size})
- )}
-
+
+ )}
);
};
-
// --- MAP PAGE COMPONENT ---
export const MapPage: React.FC = () => {
const mapRef = useRef(null);
@@ -1973,7 +2179,7 @@ export const MapPage: React.FC = () => {
const [showHelp, setShowHelp] = useState(false);
const [activeSectionFromParent, setActiveSectionFromParent] = useState<
string | null
- >("layers");
+ >(() => getStoredActiveSection() || "layers");
const handleFeaturesChange = useCallback(
(feats: Feature[]) => setMapFeatures([...feats]),
@@ -2064,6 +2270,7 @@ export const MapPage: React.FC = () => {
service?.destroy();
setMapServiceInstance(null);
};
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
@@ -2082,7 +2289,7 @@ export const MapPage: React.FC = () => {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Shift" && mapServiceInstance) {
+ if (e.key === "Shift" && mapServiceInstance && !isLassoActive) {
mapServiceInstance.activateLasso();
setIsLassoActive(true);
}
@@ -2099,7 +2306,7 @@ export const MapPage: React.FC = () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
- }, [mapServiceInstance]);
+ }, [mapServiceInstance, isLassoActive]);
useEffect(() => {
if (mapServiceInstance) {
@@ -2115,6 +2322,11 @@ export const MapPage: React.FC = () => {
}
}, [mapServiceInstance, currentMapMode]);
+ // Сохраняем активную секцию в localStorage при её изменении
+ useEffect(() => {
+ saveActiveSection(activeSectionFromParent);
+ }, [activeSectionFromParent]);
+
const showLoader = isMapLoading || isDataLoading;
const showContent = mapServiceInstance && !showLoader && !error;
const isAnythingSelected =
@@ -2208,12 +2420,6 @@ export const MapPage: React.FC = () => {
{" "}
- Повторить действие
-
-
- Ctrl+R
- {" "}
- - Отменить выделение
-
setShowHelp(false)}
diff --git a/src/pages/MapPage/mapStore.ts b/src/pages/MapPage/mapStore.ts
index 94542d3..91ad7eb 100644
--- a/src/pages/MapPage/mapStore.ts
+++ b/src/pages/MapPage/mapStore.ts
@@ -55,7 +55,7 @@ class MapStore {
getRoutes = async () => {
const response = await languageInstance("ru").get("/route");
- console.log(response.data);
+
const routesIds = response.data.map((route: any) => route.id);
for (const id of routesIds) {
const route = await languageInstance("ru").get(`/route/${id}`);
@@ -116,7 +116,6 @@ class MapStore {
const updatedStations: any[] = [];
const parsedJSON = JSON.parse(json);
- console.log("Данные для сохранения (GeoJSON):", parsedJSON);
for (const feature of parsedJSON.features) {
const { geometry, properties, id } = feature;
@@ -211,13 +210,6 @@ class MapStore {
const requests: Promise[] = [];
- console.log(
- `К созданию: ${newStations.length} станций, ${newRoutes.length} маршрутов, ${newSights.length} достопримечательностей.`
- );
- console.log(
- `К обновлению: ${updatedStations.length} станций, ${updatedRoutes.length} маршрутов, ${updatedSights.length} достопримечательностей.`
- );
-
newStations.forEach((data) =>
requests.push(languageInstance("ru").post("/station", data))
);
@@ -239,13 +231,11 @@ class MapStore {
);
if (requests.length === 0) {
- console.log("Нет изменений для сохранения.");
return;
}
try {
await Promise.all(requests);
- console.log("Все изменения успешно сохранены!");
await Promise.all([
this.getRoutes(),
diff --git a/src/pages/Media/MediaEditPage/index.tsx b/src/pages/Media/MediaEditPage/index.tsx
index 74b0949..9b82b4b 100644
--- a/src/pages/Media/MediaEditPage/index.tsx
+++ b/src/pages/Media/MediaEditPage/index.tsx
@@ -16,7 +16,12 @@ import {
Snackbar,
} from "@mui/material";
import { Save, ArrowLeft } from "lucide-react";
-import { authInstance, mediaStore, MEDIA_TYPE_LABELS } from "@shared";
+import {
+ authInstance,
+ mediaStore,
+ MEDIA_TYPE_LABELS,
+ languageStore,
+} from "@shared";
import { MediaViewer } from "@widgets";
export const MediaEditPage = observer(() => {
@@ -64,6 +69,11 @@ export const MediaEditPage = observer(() => {
}
}, [media]);
+ useEffect(() => {
+ // Устанавливаем русский язык при загрузке страницы
+ languageStore.setLanguage("ru");
+ }, []);
+
// const handleDrop = (e: DragEvent) => {
// e.preventDefault();
// e.stopPropagation();
diff --git a/src/pages/Media/MediaListPage/index.tsx b/src/pages/Media/MediaListPage/index.tsx
index cf3b3e6..f99cd17 100644
--- a/src/pages/Media/MediaListPage/index.tsx
+++ b/src/pages/Media/MediaListPage/index.tsx
@@ -1,10 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
+import { ruRU } from "@mui/x-data-grid/locales";
import { languageStore, MEDIA_TYPE_LABELS, mediaStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom";
-import { CreateButton, DeleteModal } from "@widgets";
+import { DeleteModal } from "@widgets";
+import { Box, CircularProgress } from "@mui/material";
export const MediaListPage = observer(() => {
const { media, getMedia, deleteMedia } = mediaStore;
@@ -13,10 +15,16 @@ export const MediaListPage = observer(() => {
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState(null);
const [ids, setIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => {
- getMedia();
+ const fetchMedia = async () => {
+ setIsLoading(true);
+ await getMedia();
+ setIsLoading(false);
+ };
+ fetchMedia();
}, [language]);
const columns: GridColDef[] = [
@@ -91,11 +99,6 @@ export const MediaListPage = observer(() => {
return (
<>
-
-
Медиа
-
-
-
0 ? 1 : 0 }}
@@ -114,10 +117,19 @@ export const MediaListPage = observer(() => {
columns={columns}
hideFooterPagination
checkboxSelection
+ loading={isLoading}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as string[]);
}}
hideFooter
+ localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? : "Нет медиафайлов"}
+
+ ),
+ }}
/>
diff --git a/src/pages/Media/MediaPreviewPage/index.tsx b/src/pages/Media/MediaPreviewPage/index.tsx
index 64e76a4..6a01a64 100644
--- a/src/pages/Media/MediaPreviewPage/index.tsx
+++ b/src/pages/Media/MediaPreviewPage/index.tsx
@@ -15,30 +15,32 @@ export const MediaPreviewPage = observer(() => {
}, []);
return (
-
-
-
-
-
- {oneMedia && (
-
-
- Чтобы скачать файл, нажмите на кнопку ниже
-
-
}
- component="a"
- href={`${
- import.meta.env.VITE_KRBL_MEDIA
- }${id}/download?token=${localStorage.getItem("token")}`}
- target="_blank"
- >
- Скачать
-
+
+
+
+
- )}
+
+ {oneMedia && (
+
+
+ Чтобы скачать файл, нажмите на кнопку ниже
+
+
}
+ component="a"
+ href={`${
+ import.meta.env.VITE_KRBL_MEDIA
+ }${id}/download?token=${localStorage.getItem("token")}`}
+ target="_blank"
+ >
+ Скачать
+
+
+ )}
+
);
});
diff --git a/src/pages/Route/LinekedStations.tsx b/src/pages/Route/LinekedStations.tsx
index 604d61a..6d2e537 100644
--- a/src/pages/Route/LinekedStations.tsx
+++ b/src/pages/Route/LinekedStations.tsx
@@ -18,6 +18,11 @@ import {
Paper,
TableBody,
IconButton,
+ Checkbox,
+ FormControlLabel,
+ Tabs,
+ Tab,
+ Box,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
@@ -28,7 +33,8 @@ import {
DropResult,
} from "@hello-pangea/dnd";
-import { authInstance, languageStore } from "@shared";
+import { authInstance, languageStore, routeStore } from "@shared";
+import { EditStationModal } from "../../widgets/modals/EditStationModal";
// Helper function to insert an item at a specific position (1-based index)
function insertAtPosition
(arr: T[], pos: number, value: T): T[] {
@@ -68,6 +74,7 @@ type LinkedItemsProps = {
updatedLinkedItems?: T[];
refresh?: number;
cityId?: number;
+ routeDirection?: boolean;
};
export const LinkedItems = <
@@ -118,6 +125,7 @@ export const LinkedItemsContents = <
updatedLinkedItems,
refresh,
cityId,
+ routeDirection,
}: LinkedItemsProps) => {
const { language } = languageStore;
@@ -127,6 +135,10 @@ export const LinkedItemsContents = <
const [selectedItemId, setSelectedItemId] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [selectedItems, setSelectedItems] = useState>(new Set());
+ const [activeTab, setActiveTab] = useState(0);
+ const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
console.log(error);
@@ -137,8 +149,25 @@ export const LinkedItemsContents = <
const availableItems = allItems
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
+ .filter((item) => {
+ // Если направление маршрута не указано, показываем все станции
+ if (routeDirection === undefined) return true;
+ // Фильтруем станции по направлению маршрута
+ return item.direction === routeDirection;
+ })
.sort((a, b) => a.name.localeCompare(b.name));
+ // Фильтрация по поиску для массового режима
+ const filteredAvailableItems = availableItems.filter((item) => {
+ if (!cityId || item.city_id == cityId) {
+ if (!searchQuery.trim()) return true;
+ return String(item.name)
+ .toLowerCase()
+ .includes(searchQuery.toLowerCase());
+ }
+ return false;
+ });
+
useEffect(() => {
if (updatedLinkedItems) {
setLinkedItems(updatedLinkedItems);
@@ -250,12 +279,57 @@ export const LinkedItemsContents = <
data: { [`${childResource}_id`]: itemId },
})
.then(() => {
- setLinkedItems((prev) => prev.filter((item) => item.id !== itemId));
+ setLinkedItems(linkedItems.filter((item) => item.id !== itemId));
onUpdate?.();
})
.catch((error) => {
- console.error("Error unlinking item:", error);
- setError("Failed to unlink station");
+ console.error("Error deleting item:", error);
+ setError("Failed to delete station");
+ });
+ };
+
+ const handleStationClick = (item: T) => {
+ routeStore.setSelectedStationId(item.id);
+ setIsModalOpen(true);
+ };
+
+ const handleCloseModal = () => {
+ setIsModalOpen(false);
+ };
+
+ const handleCheckboxChange = (itemId: number) => {
+ const newSelected = new Set(selectedItems);
+ if (newSelected.has(itemId)) {
+ newSelected.delete(itemId);
+ } else {
+ newSelected.add(itemId);
+ }
+ setSelectedItems(newSelected);
+ };
+
+ const handleBulkLink = () => {
+ if (selectedItems.size === 0) return;
+
+ setError(null);
+ const selectedStations = Array.from(selectedItems).map((id) => ({ id }));
+ const requestData = {
+ stations: [
+ ...linkedItems.map((item) => ({ id: item.id })),
+ ...selectedStations,
+ ],
+ };
+
+ authInstance
+ .post(`/${parentResource}/${parentId}/${childResource}`, requestData)
+ .then(() => {
+ const newItems = allItems.filter((item) => selectedItems.has(item.id));
+ setLinkedItems([...linkedItems, ...newItems]);
+ setSelectedItems(new Set());
+ onUpdate?.();
+ })
+ .catch((error) => {
+ console.error("Error linking items:", error);
+ setError("Failed to link stations");
});
};
@@ -306,6 +380,7 @@ export const LinkedItemsContents = <
ref={provided.innerRef}
{...provided.draggableProps}
hover
+ onClick={() => handleStationClick(item)}
>
{type === "edit" && dragAllowed && (
@@ -358,72 +433,169 @@ export const LinkedItemsContents = <
{type === "edit" && !disableCreation && (
- Добавить станцию
- item.id === selectedItemId) || null
- }
- onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
- options={availableItems.filter(
- (item) => !cityId || item.city_id == cityId
- )}
- getOptionLabel={(item) => String(item.name)}
- renderInput={(params) => (
-
- )}
- 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) => (
-
- {String(option.name)}
-
- )}
- />
+ Добавить остановки
+ {routeDirection !== undefined && (
+
+ Показываются только остановки для{" "}
+ {routeDirection ? "прямого" : "обратного"} направления
+
+ )}
-
- {
- const newValue = Math.max(1, Number(e.target.value));
- setPosition(
- newValue > linkedItems.length + 1
- ? linkedItems.length + 1
- : newValue
- );
- }}
- InputProps={{
- inputProps: { min: 1, max: linkedItems.length + 1 },
- }}
- fullWidth
- />
-
-
- setActiveTab(newValue)}
>
- Добавить
-
+
+
+
+
+
+ {activeTab === 0 && (
+
+ item.id === selectedItemId
+ ) || null
+ }
+ onChange={(_, newValue) =>
+ setSelectedItemId(newValue?.id || null)
+ }
+ options={availableItems.filter(
+ (item) => !cityId || item.city_id == cityId
+ )}
+ getOptionLabel={(item) => String(item.name)}
+ renderInput={(params) => (
+
+ )}
+ 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) => (
+
+
+
{String(option.name)}
+
+ {String(option.description)}
+
+
+
+ )}
+ />
+
+
+ {
+ const newValue = Math.max(1, Number(e.target.value));
+ setPosition(
+ newValue > linkedItems.length + 1
+ ? linkedItems.length + 1
+ : newValue
+ );
+ }}
+ InputProps={{
+ inputProps: { min: 1, max: linkedItems.length + 1 },
+ }}
+ fullWidth
+ />
+
+
+
+ Добавить
+
+
+ )}
+
+ {activeTab === 1 && (
+
+ {/* Поле поиска */}
+ setSearchQuery(e.target.value)}
+ placeholder="Введите название остановки..."
+ size="small"
+ sx={{ mb: 1 }}
+ />
+
+ {/* Список доступных остановок с чекбоксами */}
+
+
+ {filteredAvailableItems.map((item) => (
+ handleCheckboxChange(item.id)}
+ size="small"
+ />
+ }
+ label={String(item.name)}
+ sx={{
+ margin: 0,
+ "& .MuiFormControlLabel-label": {
+ fontSize: "0.875rem",
+ },
+ }}
+ />
+ ))}
+ {filteredAvailableItems.length === 0 && (
+
+ {searchQuery.trim()
+ ? "Остановки не найдены"
+ : "Нет доступных остановок"}
+
+ )}
+
+
+
+
+ Добавить выбранные ({selectedItems.size})
+
+
+ )}
+
)}
+
>
);
};
diff --git a/src/pages/Route/RouteCreatePage/index.tsx b/src/pages/Route/RouteCreatePage/index.tsx
index e17a76c..96cde4a 100644
--- a/src/pages/Route/RouteCreatePage/index.tsx
+++ b/src/pages/Route/RouteCreatePage/index.tsx
@@ -6,19 +6,23 @@ import {
MenuItem,
FormControl,
InputLabel,
- // Typography,
+ Typography,
Box,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
} from "@mui/material";
-import { LanguageSwitcher } from "@widgets";
+import { MediaViewer } from "@widgets";
import { observer } from "mobx-react-lite";
-import { ArrowLeft, Loader2, Save } from "lucide-react";
+import { ArrowLeft, Loader2, Save, Plus } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { carrierStore } from "../../../shared/store/CarrierStore";
import { articlesStore } from "../../../shared/store/ArticlesStore";
import { Route, routeStore } from "../../../shared/store/RouteStore";
-import { languageStore } from "@shared";
+import { languageStore, SelectArticleModal, SelectMediaDialog } from "@shared";
export const RouteCreatePage = observer(() => {
const navigate = useNavigate();
@@ -33,7 +37,12 @@ export const RouteCreatePage = observer(() => {
const [turn, setTurn] = useState("");
const [centerLat, setCenterLat] = useState("");
const [centerLng, setCenterLng] = useState("");
+ const [videoPreview, setVideoPreview] = useState("");
const [isLoading, setIsLoading] = useState(false);
+ const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
+ useState(false);
+ const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
+ const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const { language } = languageStore;
useEffect(() => {
@@ -78,6 +87,25 @@ export const RouteCreatePage = observer(() => {
}
};
+ const handleArticleSelect = (articleId: number) => {
+ setGovernorAppeal(articleId.toString());
+ setIsSelectArticleDialogOpen(false);
+ };
+
+ const handleVideoSelect = (media: {
+ id: string;
+ filename: string;
+ media_name?: string;
+ media_type: number;
+ }) => {
+ setVideoPreview(media.id);
+ setIsSelectVideoDialogOpen(false);
+ };
+
+ const handleVideoPreviewClick = () => {
+ setIsVideoPreviewOpen(true);
+ };
+
const handleCreateRoute = async () => {
try {
setIsLoading(true);
@@ -126,6 +154,8 @@ export const RouteCreatePage = observer(() => {
center_latitude,
center_longitude,
path,
+ video_preview:
+ videoPreview && videoPreview !== "" ? videoPreview : undefined,
};
await routeStore.createRoute(newRoute);
@@ -139,9 +169,13 @@ export const RouteCreatePage = observer(() => {
}
};
+ // Получаем название выбранной статьи для отображения
+ const selectedArticle = articlesStore.articleList.ru.data.find(
+ (article) => article.id === Number(governorAppeal)
+ );
+
return (
-
{
onChange={(e) => setRouteNumber(e.target.value)}
/>
{
const newValue = e.target.value;
@@ -209,10 +245,25 @@ export const RouteCreatePage = observer(() => {
helperText={
typeof validateCoordinates(routeCoords) === "string"
? validateCoordinates(routeCoords)
- : "Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)"
+ : "Формат: широта долгота"
}
- placeholder="55.7558 37.6173
-55.7539 37.6208"
+ placeholder="55.7558 37.6173
55.7539 37.6208"
+ sx={{
+ "& .MuiInputBase-root": {
+ maxHeight: "500px",
+ overflow: "auto",
+ },
+ "& .MuiInputBase-input": {
+ fontFamily: "monospace",
+ fontSize: "0.8rem",
+ lineHeight: "1.2",
+ padding: "8px 12px",
+ },
+ "& .MuiFormHelperText-root": {
+ fontSize: "0.75rem",
+ marginTop: "2px",
+ },
+ }}
/>
{
value={govRouteNumber}
onChange={(e) => setGovRouteNumber(e.target.value)}
/>
-
- Обращение губернатора
-
-
+
+ {/* Заменяем Select на кнопку для выбора статьи */}
+
+
+
+
+ setIsSelectArticleDialogOpen(true)}
+ startIcon={}
+ sx={{ minWidth: "auto", px: 2 }}
+ >
+ Выбрать
+
+
+
+
+ {/* Селектор видео превью */}
+
+
+
+
+ {videoPreview && videoPreview !== ""
+ ? "Видео выбрано"
+ : "Видео не выбрано"}
+
+ setIsSelectVideoDialogOpen(true)}
+ startIcon={}
+ sx={{ minWidth: "auto", px: 2 }}
+ >
+ Выбрать
+
+
+
+
Прямой/обратный маршрут
+
+ {/* Модальное окно выбора статьи */}
+
setIsSelectArticleDialogOpen(false)}
+ onSelectArticle={handleArticleSelect}
+ />
+
+ {/* Модальное окно выбора видео */}
+ setIsSelectVideoDialogOpen(false)}
+ onSelectMedia={handleVideoSelect}
+ mediaType={2}
+ />
+
+ {/* Модальное окно предпросмотра видео */}
+ {videoPreview && videoPreview !== "" && (
+
+ )}
);
});
diff --git a/src/pages/Route/RouteEditPage/index.tsx b/src/pages/Route/RouteEditPage/index.tsx
index dfcc3c2..9fc17ec 100644
--- a/src/pages/Route/RouteEditPage/index.tsx
+++ b/src/pages/Route/RouteEditPage/index.tsx
@@ -6,34 +6,55 @@ import {
MenuItem,
FormControl,
InputLabel,
- // Typography,
+ Typography,
Box,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
} from "@mui/material";
-import { LanguageSwitcher } from "@widgets";
+import { MediaViewer } from "@widgets";
import { observer } from "mobx-react-lite";
-import { ArrowLeft, Save } from "lucide-react";
+import { ArrowLeft, Copy, Save, Plus } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { carrierStore } from "../../../shared/store/CarrierStore";
import { articlesStore } from "../../../shared/store/ArticlesStore";
-import { routeStore } from "../../../shared/store/RouteStore";
+import {
+ routeStore,
+ languageStore,
+ SelectArticleModal,
+ SelectMediaDialog,
+} from "@shared";
import { toast } from "react-toastify";
-import { languageStore, stationsStore } from "@shared";
+import { stationsStore } from "@shared";
import { LinkedItems } from "../LinekedStations";
export const RouteEditPage = observer(() => {
const navigate = useNavigate();
const { id } = useParams();
- const { editRouteData } = routeStore;
+ const { editRouteData, copyRouteAction } = routeStore;
const [isLoading, setIsLoading] = useState(false);
+ const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
+ useState(false);
+ const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
+ const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
const { language } = languageStore;
const [coordinates, setCoordinates] = useState("");
useEffect(() => {
const fetchData = async () => {
+ // Устанавливаем русский язык при загрузке страницы
const response = await routeStore.getRoute(Number(id));
routeStore.setEditRouteData(response);
+ languageStore.setLanguage("ru");
+ };
+ fetchData();
+ }, []);
+
+ useEffect(() => {
+ const fetchData = async () => {
carrierStore.getCarriers(language);
stationsStore.getStations();
articlesStore.getArticleList();
@@ -94,9 +115,43 @@ export const RouteEditPage = observer(() => {
}
};
+ const handleCopy = async () => {
+ await copyRouteAction(Number(id));
+ toast.success("Маршрут успешно скопирован");
+ };
+
+ const handleArticleSelect = (articleId: number) => {
+ routeStore.setEditRouteData({
+ governor_appeal: articleId,
+ });
+ setIsSelectArticleDialogOpen(false);
+ };
+
+ const handleVideoSelect = (media: {
+ id: string;
+ filename: string;
+ media_name?: string;
+ media_type: number;
+ }) => {
+ routeStore.setEditRouteData({
+ video_preview: media.id,
+ });
+ setIsSelectVideoDialogOpen(false);
+ };
+
+ const handleVideoPreviewClick = () => {
+ if (editRouteData.video_preview && editRouteData.video_preview !== "") {
+ setIsVideoPreviewOpen(true);
+ }
+ };
+
+ // Получаем название выбранной статьи для отображения
+ const selectedArticle = articlesStore.articleList.ru.data.find(
+ (article) => article.id === editRouteData.governor_appeal
+ );
+
return (
-
{
}
/>
{
const newValue = e.target.value;
@@ -190,10 +247,25 @@ export const RouteEditPage = observer(() => {
helperText={
typeof validateCoordinates(coordinates) === "string"
? validateCoordinates(coordinates)
- : "Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)"
+ : "Формат: широта долгота"
}
- placeholder="55.7558 37.6173
-55.7539 37.6208"
+ placeholder="55.7558 37.6173
55.7539 37.6208"
+ sx={{
+ "& .MuiInputBase-root": {
+ maxHeight: "500px",
+ overflow: "auto",
+ },
+ "& .MuiInputBase-input": {
+ fontFamily: "monospace",
+ fontSize: "0.8rem",
+ lineHeight: "1.2",
+ padding: "8px 12px",
+ },
+ "& .MuiFormHelperText-root": {
+ fontSize: "0.75rem",
+ marginTop: "2px",
+ },
+ }}
/>
{
})
}
/>
-
- Обращение губернатора
-
-
+
+ {/* Заменяем Select на кнопку для выбора статьи */}
+
+
+
+
+ setIsSelectArticleDialogOpen(true)}
+ startIcon={}
+ sx={{ minWidth: "auto", px: 2 }}
+ >
+ Выбрать
+
+
+
+
+ {/* Селектор видео превью */}
+
+
+
+
+ {editRouteData.video_preview &&
+ editRouteData.video_preview !== ""
+ ? "Видео выбрано"
+ : "Видео не выбрано"}
+
+ setIsSelectVideoDialogOpen(true)}
+ startIcon={}
+ sx={{ minWidth: "auto", px: 2 }}
+ >
+ Выбрать
+
+
+
+
Прямой/обратный маршрут
diff --git a/src/pages/Route/route-preview/Constants.ts b/src/pages/Route/route-preview/Constants.ts
index a4b646f..8fe8b8f 100644
--- a/src/pages/Route/route-preview/Constants.ts
+++ b/src/pages/Route/route-preview/Constants.ts
@@ -2,8 +2,8 @@ export const UP_SCALE = 30000;
export const PATH_WIDTH = 15;
export const STATION_RADIUS = 20;
export const STATION_OUTLINE_WIDTH = 10;
-export const SIGHT_SIZE = 60;
+export const SIGHT_SIZE = 40;
export const SCALE_FACTOR = 50;
export const BACKGROUND_COLOR = 0x111111;
-export const PATH_COLOR = 0xff4d4d;
\ No newline at end of file
+export const PATH_COLOR = 0xff4d4d;
diff --git a/src/pages/Route/route-preview/InfiniteCanvas.tsx b/src/pages/Route/route-preview/InfiniteCanvas.tsx
index 516d307..375ce3c 100644
--- a/src/pages/Route/route-preview/InfiniteCanvas.tsx
+++ b/src/pages/Route/route-preview/InfiniteCanvas.tsx
@@ -37,7 +37,7 @@ export function InfiniteCanvas({
setScreenCenter,
screenCenter,
} = useTransform();
- const { routeData, originalRouteData } = useMapData();
+ const { routeData, originalRouteData, setSelectedSight } = useMapData();
const applicationRef = useApplication();
@@ -45,6 +45,7 @@ export function InfiniteCanvas({
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
const [startRotation, setStartRotation] = useState(0);
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
+ const [isPointerDown, setIsPointerDown] = useState(false);
// Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута
const [isUserInteracting, setIsUserInteracting] = useState(false);
@@ -65,7 +66,8 @@ export function InfiniteCanvas({
}, [applicationRef?.app.canvas, setScreenCenter]);
const handlePointerDown = (e: FederatedMouseEvent) => {
- setIsDragging(true);
+ setIsPointerDown(true);
+ setIsDragging(false);
setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя
setStartPosition({
x: position.x,
@@ -93,7 +95,18 @@ export function InfiniteCanvas({
}, [originalRouteData?.rotate, isUserInteracting, setRotation]);
const handlePointerMove = (e: FederatedMouseEvent) => {
- if (!isDragging) return;
+ if (!isPointerDown) return;
+
+ // Проверяем, началось ли перетаскивание
+ if (!isDragging) {
+ const dx = e.globalX - startMousePosition.x;
+ const dy = e.globalY - startMousePosition.y;
+ if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
+ setIsDragging(true);
+ } else {
+ return;
+ }
+ }
if (e.shiftKey) {
const center = screenCenter ?? { x: 0, y: 0 };
@@ -136,6 +149,12 @@ export function InfiniteCanvas({
};
const handlePointerUp = (e: FederatedMouseEvent) => {
+ // Если не было перетаскивания, то это простой клик - закрываем виджет
+ if (!isDragging) {
+ setSelectedSight(undefined);
+ }
+
+ setIsPointerDown(false);
setIsDragging(false);
// Сбрасываем флаг взаимодействия через небольшую задержку
// чтобы избежать немедленного срабатывания useEffect
@@ -185,7 +204,6 @@ export function InfiniteCanvas({
useEffect(() => {
applicationRef?.app.render();
- console.log(position, scale, rotation);
}, [position, scale, rotation]);
return (
diff --git a/src/pages/Route/route-preview/LeftSidebar.tsx b/src/pages/Route/route-preview/LeftSidebar.tsx
index 0e87c0e..c43bf9e 100644
--- a/src/pages/Route/route-preview/LeftSidebar.tsx
+++ b/src/pages/Route/route-preview/LeftSidebar.tsx
@@ -1,10 +1,30 @@
import { 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";
-export function LeftSidebar() {
+export const LeftSidebar = observer(() => {
const navigate = useNavigate();
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
+ const { routeData } = useMapData();
+ const [carrierThumbnail, setCarrierThumbnail] = useState(null);
+ const [carrierLogo, setCarrierLogo] = useState(null);
+ useEffect(() => {
+ async function fetchCarrierThumbnail() {
+ if (routeData?.carrier_id) {
+ const { city_id, logo } = (
+ await authInstance.get(`/carrier/${routeData.carrier_id}`)
+ ).data;
+ const { arms } = (await authInstance.get(`/city/${city_id}`)).data;
+ setCarrierThumbnail(arms);
+ setCarrierLogo(logo);
+ }
+ }
+ fetchCarrierThumbnail();
+ }, [routeData?.carrier_id]);
const handleBack = () => {
if (navigationType === "PUSH") {
@@ -27,6 +47,7 @@ export function LeftSidebar() {
color: "#fff",
backgroundColor: "#222",
borderRadius: 10,
+ height: 40,
width: "100%",
border: "none",
cursor: "pointer",
@@ -41,10 +62,30 @@ export function LeftSidebar() {
justifyContent="center"
my={10}
>
-
-
- При поддержке Правительства Санкт-Петербурга
-
+
+ {carrierThumbnail && (
+
+ )}
+
+ При поддержке Правительства
+ {" "}
+
-
+ {carrierLogo && (
+
+ )}
);
-}
+});
diff --git a/src/pages/Route/route-preview/MapDataContext.tsx b/src/pages/Route/route-preview/MapDataContext.tsx
index b0b1012..1e1f7b1 100644
--- a/src/pages/Route/route-preview/MapDataContext.tsx
+++ b/src/pages/Route/route-preview/MapDataContext.tsx
@@ -29,10 +29,13 @@ const MapDataContext = createContext<{
isRouteLoading: boolean;
isStationLoading: boolean;
isSightLoading: boolean;
+ selectedSight?: SightData;
+ setSelectedSight: (sight?: SightData) => void;
setScaleRange: (min: number, max: number) => void;
setMapRotation: (rotation: number) => void;
setMapCenter: (x: number, y: number) => void;
setStationOffset: (stationId: number, x: number, y: number) => void;
+ setStationAlign: (stationId: number, align: number) => void;
setSightCoordinates: (
sightId: number,
latitude: number,
@@ -50,10 +53,13 @@ const MapDataContext = createContext<{
isRouteLoading: true,
isStationLoading: true,
isSightLoading: true,
+ selectedSight: undefined,
+ setSelectedSight: () => {},
setScaleRange: () => {},
setMapRotation: () => {},
setMapCenter: () => {},
setStationOffset: () => {},
+ setStationAlign: () => {},
setSightCoordinates: () => {},
saveChanges: () => {},
});
@@ -87,6 +93,7 @@ export const MapDataProvider = observer(
const [isRouteLoading, setIsRouteLoading] = useState(true);
const [isStationLoading, setIsStationLoading] = useState(true);
const [isSightLoading, setIsSightLoading] = useState(true);
+ const [selectedSight, setSelectedSight] = useState();
useEffect(() => {
const fetchData = async () => {
@@ -106,17 +113,18 @@ export const MapDataProvider = observer(
languageInstance("ru").get(`/route/${routeId}/station`),
languageInstance("en").get(`/route/${routeId}/station`),
languageInstance("zh").get(`/route/${routeId}/station`),
- authInstance.get(`/route/${routeId}/sight`),
+ languageInstance("ru").get(`/route/${routeId}/sight`),
]);
- setOriginalRouteData(routeResponse.data as RouteData);
+ const routeData = routeResponse.data as RouteData;
+ setOriginalRouteData(routeData);
setOriginalStationData(ruStationResponse.data as StationData[]);
setStationData({
ru: ruStationResponse.data as StationData[],
en: enStationResponse.data as StationData[],
zh: zhStationResponse.data as StationData[],
});
- setOriginalSightData(sightResponse as unknown as SightData[]);
+ setOriginalSightData(sightResponse.data as SightData[]);
setIsRouteLoading(false);
setIsStationLoading(false);
@@ -176,43 +184,136 @@ export const MapDataProvider = observer(
}
async function saveSightChanges() {
- console.log("sightChanges", sightChanges);
for (const sight of sightChanges) {
await authInstance.patch(`/route/${routeId}/sight`, sight);
}
}
function setStationOffset(stationId: number, x: number, y: number) {
- setStationChanges((prev) => {
- let found = prev.find((station) => station.station_id === stationId);
- if (found) {
- found.offset_x = x;
- found.offset_y = y;
+ const currentStation = stationData.ru?.find(
+ (station) => station.id === stationId
+ );
+ if (
+ currentStation &&
+ Math.abs(currentStation.offset_x - x) < 0.01 &&
+ Math.abs(currentStation.offset_y - y) < 0.01
+ ) {
+ return;
+ }
- return prev.map((station) => {
- if (station.station_id === stationId) {
- return found;
+ setStationChanges((prev) => {
+ const existingIndex = prev.findIndex(
+ (station) => station.station_id === stationId
+ );
+
+ if (existingIndex !== -1) {
+ const newChanges = [...prev];
+ newChanges[existingIndex] = {
+ ...newChanges[existingIndex],
+ offset_x: x,
+ offset_y: y,
+ };
+ return newChanges;
+ } else {
+ const originalStation = originalStationData?.find(
+ (s) => s.id === stationId
+ );
+ return [
+ ...prev,
+ {
+ station_id: stationId,
+ offset_x: x,
+ offset_y: y,
+ align: originalStation?.align ?? 1,
+ transfers: originalStation?.transfers ?? {
+ bus: "",
+ metro_blue: "",
+ metro_green: "",
+ metro_orange: "",
+ metro_purple: "",
+ metro_red: "",
+ train: "",
+ tram: "",
+ trolleybus: "",
+ },
+ },
+ ];
+ }
+ });
+
+ setStationData((prev) => {
+ const updated = { ...prev };
+ Object.keys(updated).forEach((lang) => {
+ updated[lang] = updated[lang].map((station) => {
+ if (station.id === stationId) {
+ return { ...station, offset_x: x, offset_y: y };
}
return station;
});
+ });
+ return updated;
+ });
+ }
+
+ function setStationAlign(stationId: number, align: number) {
+ const currentStation = stationData.ru?.find(
+ (station) => station.id === stationId
+ );
+ if (currentStation && currentStation.align === align) {
+ return;
+ }
+
+ setStationChanges((prev) => {
+ const existingIndex = prev.findIndex(
+ (station) => station.station_id === stationId
+ );
+
+ if (existingIndex !== -1) {
+ const newChanges = [...prev];
+ newChanges[existingIndex] = {
+ ...newChanges[existingIndex],
+ align: align,
+ };
+ return newChanges;
} else {
- const foundStation = stationData.ru?.find(
- (station) => station.id === stationId
+ const originalStation = originalStationData?.find(
+ (s) => s.id === stationId
);
- if (foundStation) {
- return [
- ...prev,
- {
- station_id: stationId,
- offset_x: x,
- offset_y: y,
- transfers: foundStation.transfers,
+ return [
+ ...prev,
+ {
+ station_id: stationId,
+ align: align,
+ offset_x: originalStation?.offset_x ?? 0,
+ offset_y: originalStation?.offset_y ?? 0,
+ transfers: originalStation?.transfers ?? {
+ bus: "",
+ metro_blue: "",
+ metro_green: "",
+ metro_orange: "",
+ metro_purple: "",
+ metro_red: "",
+ train: "",
+ tram: "",
+ trolleybus: "",
},
- ];
- }
- return prev;
+ },
+ ];
}
});
+
+ setStationData((prev) => {
+ const updated = { ...prev };
+ Object.keys(updated).forEach((lang) => {
+ updated[lang] = updated[lang].map((station) => {
+ if (station.id === stationId) {
+ return { ...station, align: align };
+ }
+ return station;
+ });
+ });
+ return updated;
+ });
}
function setSightCoordinates(
@@ -221,14 +322,18 @@ export const MapDataProvider = observer(
longitude: number
) {
setSightChanges((prev) => {
- let found = prev.find((sight) => sight.sight_id === sightId);
- if (found) {
- found.latitude = latitude;
- found.longitude = longitude;
+ const existingIndex = prev.findIndex(
+ (sight) => sight.sight_id === sightId
+ );
- return prev.map((sight) => {
- if (sight.sight_id === sightId) {
- return found;
+ if (existingIndex !== -1) {
+ return prev.map((sight, index) => {
+ if (index === existingIndex) {
+ return {
+ ...sight,
+ latitude,
+ longitude,
+ };
}
return sight;
});
@@ -249,9 +354,7 @@ export const MapDataProvider = observer(
});
}
- useEffect(() => {
- console.log("sightChanges", sightChanges);
- }, [sightChanges]);
+ useEffect(() => {}, [sightChanges]);
const value = useMemo(
() => ({
@@ -264,11 +367,14 @@ export const MapDataProvider = observer(
isRouteLoading,
isStationLoading,
isSightLoading,
+ selectedSight,
+ setSelectedSight,
setScaleRange,
setMapRotation,
setMapCenter,
saveChanges,
setStationOffset,
+ setStationAlign,
setSightCoordinates,
}),
[
@@ -281,6 +387,7 @@ export const MapDataProvider = observer(
isRouteLoading,
isStationLoading,
isSightLoading,
+ selectedSight,
]
);
diff --git a/src/pages/Route/route-preview/RightSidebar.tsx b/src/pages/Route/route-preview/RightSidebar.tsx
index 2ccf2e8..212be38 100644
--- a/src/pages/Route/route-preview/RightSidebar.tsx
+++ b/src/pages/Route/route-preview/RightSidebar.tsx
@@ -1,8 +1,9 @@
-import { Button, Stack, TextField, Typography } from "@mui/material";
+import { Button, Stack, TextField, Typography, Slider } from "@mui/material";
import { useMapData } from "./MapDataContext";
import { useEffect, useState } from "react";
import { useTransform } from "./TransformContext";
-import { coordinatesToLocal } from "./utils";
+import { coordinatesToLocal, localToCoordinates } from "./utils";
+import { SCALE_FACTOR } from "./Constants";
export function RightSidebar() {
const {
@@ -15,24 +16,36 @@ export function RightSidebar() {
} = useMapData();
const {
rotation,
- // position,
- // screenToLocal,
- // screenCenter,
+ position,
+ screenToLocal,
+ screenCenter,
rotateToAngle,
setTransform,
+ scale,
+ setScaleAtCenter,
} = useTransform();
+
const [minScale, setMinScale] = useState(1);
- const [maxScale, setMaxScale] = useState(10);
+ const [maxScale, setMaxScale] = useState(5);
const [localCenter, setLocalCenter] = useState<{ x: number; y: number }>({
x: 0,
y: 0,
});
const [rotationDegrees, setRotationDegrees] = useState(0);
+ const [isUserEditing, setIsUserEditing] = useState(false);
useEffect(() => {
if (originalRouteData) {
- setMinScale(originalRouteData.scale_min ?? 1);
- setMaxScale(originalRouteData.scale_max ?? 10);
+ // Проверяем и сбрасываем минимальный масштаб если нужно
+ const originalMinScale = originalRouteData.scale_min ?? 1;
+ const resetMinScale = originalMinScale < 1 ? 1 : originalMinScale;
+
+ // Проверяем и сбрасываем максимальный масштаб если нужно
+ const originalMaxScale = originalRouteData.scale_max ?? 5;
+ const resetMaxScale = originalMaxScale < 3 ? 3 : originalMaxScale;
+
+ setMinScale(resetMinScale);
+ setMaxScale(resetMaxScale);
setRotationDegrees(originalRouteData.rotate ?? 0);
setLocalCenter({
x: originalRouteData.center_latitude ?? 0,
@@ -52,16 +65,26 @@ export function RightSidebar() {
((Math.round((rotation * 180) / Math.PI) % 360) + 360) % 360
);
}, [rotation]);
+
useEffect(() => {
setMapRotation(rotationDegrees);
}, [rotationDegrees]);
- // useEffect(() => {
- // 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 });
- // }, [position]);
+ 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 });
+ }
+ }, [
+ position,
+ screenCenter,
+ screenToLocal,
+ localToCoordinates,
+ setLocalCenter,
+ isUserEditing,
+ ]);
useEffect(() => {
setMapCenter(localCenter.x, localCenter.y);
@@ -104,7 +127,30 @@ export function RightSidebar() {
label="Минимальный масштаб"
variant="filled"
value={minScale}
- onChange={(e) => setMinScale(Number(e.target.value))}
+ onChange={(e) => {
+ let newMinScale = Number(e.target.value);
+
+ // Сбрасываем к 1 если меньше
+ if (newMinScale < 1) {
+ newMinScale = 1;
+ }
+
+ setMinScale(newMinScale);
+
+ if (maxScale - newMinScale < 2) {
+ let newMaxScale = newMinScale + 2;
+ // Сбрасываем максимальный к 3 если меньше минимального
+ if (newMaxScale < 3) {
+ newMaxScale = 3;
+ setMinScale(1); // Сбрасываем минимальный к 1
+ }
+ setMaxScale(newMaxScale);
+ }
+
+ if (newMinScale > scale * SCALE_FACTOR) {
+ setScaleAtCenter(newMinScale / SCALE_FACTOR);
+ }
+ }}
style={{ backgroundColor: "#222", borderRadius: 4 }}
sx={{
"& .MuiInputLabel-root": {
@@ -116,7 +162,8 @@ export function RightSidebar() {
}}
slotProps={{
input: {
- min: 0.1,
+ min: 1,
+ max: 10,
},
}}
/>
@@ -125,7 +172,30 @@ export function RightSidebar() {
label="Максимальный масштаб"
variant="filled"
value={maxScale}
- onChange={(e) => setMaxScale(Number(e.target.value))}
+ onChange={(e) => {
+ let newMaxScale = Number(e.target.value);
+
+ // Сбрасываем к 3 если меньше минимального
+ if (newMaxScale < 3) {
+ newMaxScale = 3;
+ }
+
+ setMaxScale(newMaxScale);
+
+ if (newMaxScale - minScale < 2) {
+ let newMinScale = newMaxScale - 2;
+ // Сбрасываем минимальный к 1 если меньше
+ if (newMinScale < 1) {
+ newMinScale = 1;
+ setMaxScale(3); // Сбрасываем максимальный к минимальному значению
+ }
+ setMinScale(newMinScale);
+ }
+
+ if (newMaxScale < scale * SCALE_FACTOR) {
+ setScaleAtCenter(newMaxScale / SCALE_FACTOR);
+ }
+ }}
style={{ backgroundColor: "#222", borderRadius: 4, color: "#fff" }}
sx={{
"& .MuiInputLabel-root": {
@@ -137,12 +207,71 @@ export function RightSidebar() {
}}
slotProps={{
input: {
- min: 0.1,
+ min: 3,
+ max: 10,
},
}}
/>
+
+ Текущий масштаб: {Math.round(scale * SCALE_FACTOR * 100) / 100}
+
+
+ {
+ if (typeof newValue === "number") {
+ setScaleAtCenter(newValue / SCALE_FACTOR);
+ }
+ }}
+ min={minScale}
+ max={maxScale}
+ step={0.1}
+ sx={{
+ color: "#fff",
+ "& .MuiSlider-thumb": {
+ backgroundColor: "#fff",
+ },
+ "& .MuiSlider-track": {
+ backgroundColor: "#fff",
+ },
+ "& .MuiSlider-rail": {
+ backgroundColor: "#666",
+ },
+ }}
+ />
+
+ {
+ const newScale = Number(e.target.value);
+ if (
+ !isNaN(newScale) &&
+ newScale >= minScale &&
+ newScale <= maxScale
+ ) {
+ setScaleAtCenter(newScale / SCALE_FACTOR);
+ }
+ }}
+ style={{ backgroundColor: "#222", borderRadius: 4 }}
+ sx={{
+ "& .MuiInputLabel-root": {
+ color: "#fff",
+ },
+ "& .MuiInputBase-input": {
+ color: "#fff",
+ },
+ }}
+ inputProps={{
+ min: minScale,
+ max: maxScale,
+ }}
+ />
+
{
+ setIsUserEditing(true);
setLocalCenter((prev) => ({ ...prev, x: Number(e.target.value) }));
pan({ x: Number(e.target.value), y: localCenter.y });
}}
+ onBlur={() => setIsUserEditing(false)}
style={{ backgroundColor: "#222", borderRadius: 4 }}
sx={{
"& .MuiInputLabel-root": {
@@ -195,16 +326,21 @@ export function RightSidebar() {
color: "#fff",
},
}}
+ inputProps={{
+ step: 0.001,
+ }}
/>
{
+ setIsUserEditing(true);
setLocalCenter((prev) => ({ ...prev, y: Number(e.target.value) }));
pan({ x: localCenter.x, y: Number(e.target.value) });
}}
+ onBlur={() => setIsUserEditing(false)}
style={{ backgroundColor: "#222", borderRadius: 4 }}
sx={{
"& .MuiInputLabel-root": {
@@ -214,6 +350,9 @@ export function RightSidebar() {
color: "#fff",
},
}}
+ inputProps={{
+ step: 0.001,
+ }}
/>
diff --git a/src/pages/Route/route-preview/Sight.tsx b/src/pages/Route/route-preview/Sight.tsx
index f857a76..d5545fb 100644
--- a/src/pages/Route/route-preview/Sight.tsx
+++ b/src/pages/Route/route-preview/Sight.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useTransform } from "./TransformContext";
import { SightData } from "./types";
-import { Assets, FederatedMouseEvent, Graphics, Texture } from "pixi.js";
+import { Assets, FederatedMouseEvent, Texture } from "pixi.js";
import { SIGHT_SIZE, UP_SCALE } from "./Constants";
import { coordinatesToLocal, localToCoordinates } from "./utils";
@@ -12,19 +12,21 @@ interface SightProps {
id: number;
}
-export function Sight({ sight, id }: Readonly) {
+export const Sight = ({ sight, id }: Readonly) => {
const { rotation, scale } = useTransform();
- const { setSightCoordinates } = useMapData();
+ const { setSightCoordinates, setSelectedSight } = useMapData();
const [position, setPosition] = useState(
coordinatesToLocal(sight.latitude, sight.longitude)
);
const [isDragging, setIsDragging] = useState(false);
+ const [isPointerDown, setIsPointerDown] = useState(false);
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
const [startMousePosition, setStartMousePosition] = useState({ x: 0, y: 0 });
const handlePointerDown = (e: FederatedMouseEvent) => {
- setIsDragging(true);
+ setIsPointerDown(true);
+ setIsDragging(false);
setStartPosition({
x: position.x,
y: position.y,
@@ -37,7 +39,18 @@ export function Sight({ sight, id }: Readonly) {
e.stopPropagation();
};
const handlePointerMove = (e: FederatedMouseEvent) => {
- if (!isDragging) return;
+ if (!isPointerDown) return;
+
+ if (!isDragging) {
+ const dx = e.globalX - startMousePosition.x;
+ const dy = e.globalY - startMousePosition.y;
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
+ setIsDragging(true);
+ } else {
+ return;
+ }
+ }
+
const dx = (e.globalX - startMousePosition.x) / scale / UP_SCALE;
const dy = (e.globalY - startMousePosition.y) / scale / UP_SCALE;
const cos = Math.cos(rotation);
@@ -53,30 +66,37 @@ export function Sight({ sight, id }: Readonly) {
};
const handlePointerUp = (e: FederatedMouseEvent) => {
+ setIsPointerDown(false);
+
+ // Если не было перетаскивания, то это клик
+ if (!isDragging) {
+ setSelectedSight(sight);
+ }
+
setIsDragging(false);
e.stopPropagation();
};
const [texture, setTexture] = useState(Texture.EMPTY);
useEffect(() => {
- if (texture === Texture.EMPTY) {
- Assets.load("/SightIcon.png").then((result) => {
- setTexture(result);
- });
- }
- }, [texture]);
+ Assets.load("/SightIcon.png").then(setTexture);
+ }, []);
- function draw(g: Graphics) {
- g.clear();
- g.circle(0, 0, 20);
- g.fill({ color: "#000" }); // Fill circle with primary color
- }
+ useEffect(() => {
+ console.log(
+ `Rendering Sight ${id + 1} at [${sight.latitude}, ${sight.longitude}]`
+ );
+ }, [id, sight.latitude, sight.longitude]);
if (!sight) {
console.error("sight is null");
return null;
}
+ // Компенсируем масштаб для сохранения постоянного размера
+ const compensatedSize = SIGHT_SIZE / scale;
+ const compensatedFontSize = 24 / scale;
+
return (
) {
onGlobalPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerUpOutside={handlePointerUp}
- x={position.x * UP_SCALE - SIGHT_SIZE / 2} // Offset by half width to center
- y={position.y * UP_SCALE - SIGHT_SIZE / 2} // Offset by half height to center
+ x={position.x * UP_SCALE - SIGHT_SIZE / 2}
+ y={position.y * UP_SCALE - SIGHT_SIZE / 2}
>
-
-
+
+ {
+ g.clear();
+ g.circle(0, 0, 20 / scale);
+ g.fill({ color: "#000" });
+ }}
+ x={compensatedSize}
+ y={0}
+ />
);
-}
+};
diff --git a/src/pages/Route/route-preview/SightInfoWidget.tsx b/src/pages/Route/route-preview/SightInfoWidget.tsx
new file mode 100644
index 0000000..2299c73
--- /dev/null
+++ b/src/pages/Route/route-preview/SightInfoWidget.tsx
@@ -0,0 +1,60 @@
+import { Box, Typography, IconButton } from "@mui/material";
+import { Close } from "@mui/icons-material";
+import { useMapData } from "./MapDataContext";
+
+export function SightInfoWidget() {
+ const { selectedSight, setSelectedSight } = useMapData();
+
+ if (!selectedSight) {
+ return null;
+ }
+
+ return (
+
+
+
+ {selectedSight.name}
+
+ setSelectedSight(undefined)}
+ sx={{ color: "#fff", p: 0, minWidth: 24, width: 24, height: 24 }}
+ >
+
+
+
+
+
+ {selectedSight.address}
+
+
+
+ Город: {selectedSight.city}
+
+
+ );
+}
diff --git a/src/pages/Route/route-preview/Station.tsx b/src/pages/Route/route-preview/Station.tsx
index b738be2..effe436 100644
--- a/src/pages/Route/route-preview/Station.tsx
+++ b/src/pages/Route/route-preview/Station.tsx
@@ -1,4 +1,8 @@
import { FederatedMouseEvent, Graphics } from "pixi.js";
+import { useCallback, useState, useEffect, useRef, FC } from "react";
+import { observer } from "mobx-react-lite";
+
+// --- Заглушки для зависимостей (замените на ваши реальные импорты) ---
import {
BACKGROUND_COLOR,
PATH_COLOR,
@@ -7,140 +11,545 @@ import {
UP_SCALE,
} from "./Constants";
import { useTransform } from "./TransformContext";
-import { useCallback, useState } from "react";
import { StationData } from "./types";
import { useMapData } from "./MapDataContext";
import { coordinatesToLocal } from "./utils";
-import { observer } from "mobx-react-lite";
+import { languageStore } from "@shared";
+// --- Конец заглушек ---
+
+// --- Декларации для react-pixi ---
+// (В реальном проекте типы должны быть предоставлены библиотекой @pixi/react-pixi)
+declare const pixiContainer: any;
+declare const pixiGraphics: any;
+declare const pixiText: any;
+
+// --- Типы ---
+type HorizontalAlign = "left" | "center" | "right";
+type VerticalAlign = "top" | "center" | "bottom";
+type TextAlign = HorizontalAlign | `${HorizontalAlign} ${VerticalAlign}`;
+type LabelAlign = "left" | "center" | "right";
+
+// --- Утилиты ---
+
+/**
+ * Преобразует текстовое позиционирование в anchor координаты.
+ */
+const getAnchorFromTextAlign = (align: TextAlign): { x: number; y: number } => {
+ const parts = align.split(" ");
+ const horizontal = parts[0] as HorizontalAlign;
+ const vertical = (parts[1] as VerticalAlign) || "center";
+ const horizontalMap: Record = {
+ left: 0,
+ center: 0.5,
+ right: 1,
+ };
+ const verticalMap: Record = {
+ top: 0,
+ center: 0.5,
+ bottom: 1,
+ };
+ return { x: horizontalMap[horizontal], y: verticalMap[vertical] };
+};
+
+/**
+ * Получает координату anchor.x из типа выравнивания.
+ */
+
+// --- Интерфейсы пропсов ---
interface StationProps {
station: StationData;
ruLabel: string | null;
+ anchorPoint?: { x: number; y: number };
+ /** Anchor для всего блока с текстом. По умолчанию: `"right center"` */
+ labelBlockAnchor?: TextAlign | { x: number; y: number };
+ /** Внутреннее выравнивание текста в блоке. По умолчанию: `"left"` */
+ labelAlign?: LabelAlign;
+ /** Callback для изменения внутреннего выравнивания */
+ onLabelAlignChange?: (align: LabelAlign) => void;
}
-export const Station = observer(
- ({ station, ruLabel }: Readonly) => {
- const draw = useCallback((g: Graphics) => {
+interface LabelAlignmentControlProps {
+ scale: number;
+ currentAlign: LabelAlign;
+ onAlignChange: (align: LabelAlign) => void;
+ onPointerOver: () => void;
+ onPointerOut: () => void;
+ onControlPointerEnter: () => void;
+ onControlPointerLeave: () => void;
+}
+
+interface StationLabelProps
+ extends Omit {}
+
+// =========================================================================
+// Компонент: Панель управления выравниванием в стиле УрФУ
+// =========================================================================
+
+const LabelAlignmentControl: FC = ({
+ scale,
+ currentAlign,
+ onAlignChange,
+
+ onControlPointerEnter,
+ onControlPointerLeave,
+}) => {
+ const controlHeight = 50 / scale;
+ const controlWidth = 200 / scale;
+ const fontSize = 18 / scale;
+ const borderRadius = 8 / scale;
+ const compensatedRuFontSize = (26 * 0.75) / scale;
+ const buttonWidth = controlWidth / 3;
+ const strokeWidth = 2 / scale;
+
+ const drawBg = useCallback(
+ (g: Graphics) => {
+ g.clear();
+
+ // Основной фон с градиентом
+ g.roundRect(
+ -controlWidth / 2,
+ 0,
+ controlWidth,
+ controlHeight,
+ borderRadius
+ );
+ g.fill({ color: "#1a1a1a" }); // Темный фон как у УрФУ
+
+ // Тонкая рамка
+ g.roundRect(
+ -controlWidth / 2,
+ 0,
+ controlWidth,
+ controlHeight,
+ borderRadius
+ );
+ g.stroke({ color: "#333333", width: strokeWidth });
+
+ // Разделители между кнопками
+ for (let i = 1; i < 3; i++) {
+ const x = -controlWidth / 2 + buttonWidth * i;
+ g.moveTo(x, strokeWidth);
+ g.lineTo(x, controlHeight - strokeWidth);
+ g.stroke({ color: "#333333", width: strokeWidth });
+ }
+ },
+ [controlWidth, controlHeight, borderRadius, buttonWidth, strokeWidth]
+ );
+
+ const drawButtonHighlight = useCallback(
+ (g: Graphics, index: number, isActive: boolean) => {
+ g.clear();
+
+ if (isActive) {
+ const x = -controlWidth / 2 + buttonWidth * index;
+ g.roundRect(
+ x + strokeWidth,
+ strokeWidth,
+ buttonWidth - strokeWidth * 2,
+ controlHeight - strokeWidth * 2,
+ borderRadius / 2
+ );
+ g.fill({ color: "#0066cc", alpha: 0.8 }); // Синий акцент УрФУ
+ }
+ },
+ [controlWidth, controlHeight, buttonWidth, strokeWidth, borderRadius]
+ );
+
+ const getTextStyle = (isActive: boolean) => ({
+ fontSize,
+ fontWeight: isActive ? ("bold" as const) : ("normal" as const),
+ fill: isActive ? "#ffffff" : "#cccccc",
+ fontFamily: "Arial, sans-serif",
+ });
+
+ const alignOptions = [
+ { key: "left" as const, label: "Left" },
+ { key: "center" as const, label: "Center" },
+ { key: "right" as const, label: "Right" },
+ ];
+
+ return (
+ {
+ e.stopPropagation();
+ onControlPointerEnter();
+ }}
+ onPointerOut={(e: FederatedMouseEvent) => {
+ e.stopPropagation();
+ onControlPointerLeave();
+ }}
+ onPointerDown={(e: FederatedMouseEvent) => {
+ e.stopPropagation();
+ }}
+ >
+ {/* Основной фон */}
+
+
+ {/* Кнопки с подсветкой */}
+ {alignOptions.map((option, index) => (
+
+ {/* Подсветка активной кнопки */}
+
+ drawButtonHighlight(g, index, option.key === currentAlign)
+ }
+ />
+
+ {/* Текст кнопки */}
+ {
+ e.stopPropagation();
+ onAlignChange(option.key);
+ }}
+ onPointerDown={(e: FederatedMouseEvent) => {
+ e.stopPropagation();
+ onAlignChange(option.key);
+ }}
+ onPointerOver={(e: FederatedMouseEvent) => {
+ e.stopPropagation();
+ onControlPointerEnter();
+ }}
+ />
+
+ ))}
+
+ );
+};
+
+// =========================================================================
+// Компонент: Метка Станции (с логикой)
+// =========================================================================
+
+const StationLabel = observer(
+ ({
+ station,
+ ruLabel,
+ anchorPoint,
+ labelBlockAnchor: labelBlockAnchorProp,
+ labelAlign: labelAlignProp = "center",
+ onLabelAlignChange,
+ }: Readonly) => {
+ const { language } = languageStore;
+ const { rotation, scale } = useTransform();
+ const { setStationOffset, setStationAlign } = useMapData();
+
+ const [position, setPosition] = useState({ x: 0, y: 0 });
+ const [isDragging, setIsDragging] = useState(false);
+ const [isPointerDown, setIsPointerDown] = useState(false);
+ const [isHovered, setIsHovered] = useState(false);
+ const [isControlHovered, setIsControlHovered] = useState(false);
+ const [currentLabelAlign, setCurrentLabelAlign] = useState(labelAlignProp);
+ const [ruLabelWidth, setRuLabelWidth] = useState(0);
+
+ const dragStartPos = useRef({ x: 0, y: 0 });
+ const mouseStartPos = useRef({ x: 0, y: 0 });
+ const hideTimer = useRef(null);
+ const ruLabelRef = useRef(null);
+
+ useEffect(() => {
+ return () => {
+ if (hideTimer.current) {
+ clearTimeout(hideTimer.current);
+ }
+ };
+ }, []);
+
+ const handlePointerEnter = () => {
+ if (hideTimer.current) {
+ clearTimeout(hideTimer.current);
+ hideTimer.current = null;
+ }
+ setIsHovered(true);
+ };
+
+ const handleControlPointerEnter = () => {
+ // Дополнительная обработка для панели управления
+ if (hideTimer.current) {
+ clearTimeout(hideTimer.current);
+ hideTimer.current = null;
+ }
+ setIsControlHovered(true);
+ setIsHovered(true);
+ };
+
+ const handleControlPointerLeave = () => {
+ setIsControlHovered(false);
+ // Если курсор не над основным контейнером, скрываем панель через некоторое время
+ if (!isHovered) {
+ hideTimer.current = setTimeout(() => {
+ setIsHovered(false);
+ }, 0);
+ }
+ };
+
+ const handlePointerLeave = () => {
+ // Увеличиваем время до скрытия панели и добавляем проверку
+ hideTimer.current = setTimeout(() => {
+ setIsHovered(false);
+ // Если курсор не над панелью управления, скрываем и её
+ if (!isControlHovered) {
+ setIsControlHovered(false);
+ }
+ }, 100); // Увеличиваем время до скрытия панели
+ };
+
+ useEffect(() => {
+ setPosition({ x: station.offset_x ?? 0, y: station.offset_y ?? 0 });
+ }, [station.offset_x, station.offset_y, station.id]);
+
+ // Функция для конвертации числового align в строковый
+ const convertNumericAlign = (align: number): LabelAlign => {
+ switch (align) {
+ case 0:
+ return "left";
+ case 1:
+ return "center";
+ case 2:
+ return "right";
+ default:
+ return "center";
+ }
+ };
+
+ // Функция для конвертации строкового align в числовой
+ const convertStringAlign = (align: LabelAlign): number => {
+ switch (align) {
+ case "left":
+ return 0;
+ case "center":
+ return 1;
+ case "right":
+ return 2;
+ default:
+ return 1;
+ }
+ };
+
+ useEffect(() => {
+ setCurrentLabelAlign(convertNumericAlign(station.align ?? 1));
+ }, [station.align]);
+
+ if (!station) return null;
+
+ const coordinates = coordinatesToLocal(station.latitude, station.longitude);
+ const compensatedRuFontSize = (26 * 0.75) / scale;
+ const compensatedNameFontSize = (16 * 0.75) / scale;
+ const minDistance = 30;
+ const compensatedOffset = Math.max(minDistance / scale, 24 / scale);
+ const textBlockPosition = anchorPoint || position;
+ const finalLabelBlockAnchor = labelBlockAnchorProp || "right center";
+ const labelBlockAnchor =
+ typeof finalLabelBlockAnchor === "string"
+ ? getAnchorFromTextAlign(finalLabelBlockAnchor)
+ : finalLabelBlockAnchor;
+
+ // Измеряем ширину верхнего лейбла
+ useEffect(() => {
+ if (ruLabelRef.current && ruLabel) {
+ setRuLabelWidth(ruLabelRef.current.width);
+ }
+ }, [ruLabel, compensatedRuFontSize]);
+
+ const handlePointerDown = (e: FederatedMouseEvent) => {
+ setIsPointerDown(true);
+ setIsDragging(false);
+ dragStartPos.current = { ...position };
+ mouseStartPos.current = { x: e.global.x, y: e.global.y };
+ e.stopPropagation();
+ };
+
+ const handlePointerMove = (e: FederatedMouseEvent) => {
+ if (!isPointerDown) return;
+ if (!isDragging) {
+ const dx = e.global.x - mouseStartPos.current.x;
+ const dy = e.global.y - mouseStartPos.current.y;
+ if (Math.hypot(dx, dy) > 3) setIsDragging(true);
+ else return;
+ }
+ const dx = (e.global.x - mouseStartPos.current.x) / scale;
+ const dy = (e.global.y - mouseStartPos.current.y) / scale;
+ const newPosition = {
+ x: dragStartPos.current.x + dx,
+ y: dragStartPos.current.y + dy,
+ };
+
+ // Проверяем, изменилась ли позиция
+ if (
+ Math.abs(newPosition.x - position.x) > 0.01 ||
+ Math.abs(newPosition.y - position.y) > 0.01
+ ) {
+ setPosition(newPosition);
+ setStationOffset(station.id, newPosition.x, newPosition.y);
+ }
+ e.stopPropagation();
+ };
+
+ const handlePointerUp = (e: FederatedMouseEvent) => {
+ setIsPointerDown(false);
+ setTimeout(() => setIsDragging(false), 50);
+ e.stopPropagation();
+ };
+
+ const handleAlignChange = async (align: LabelAlign) => {
+ setCurrentLabelAlign(align);
+ onLabelAlignChange?.(align);
+ // Сохраняем в стор
+ const numericAlign = convertStringAlign(align);
+ setStationAlign(station.id, numericAlign);
+ };
+
+ // Функция для расчета позиции нижнего лейбла относительно ширины верхнего
+ const getSecondLabelPosition = (): number => {
+ if (!ruLabelWidth) return 0;
+
+ switch (currentLabelAlign) {
+ case "left":
+ // Позиционируем относительно левого края верхнего текста
+ return -ruLabelWidth / 2;
+ case "center":
+ // Центрируем относительно центра верхнего текста
+ return 0;
+ case "right":
+ // Позиционируем относительно правого края верхнего текста
+ return ruLabelWidth / 2;
+ default:
+ return 0;
+ }
+ };
+
+ // Функция для расчета anchor нижнего лейбла
+ const getSecondLabelAnchor = (): number => {
+ switch (currentLabelAlign) {
+ case "left":
+ return 0; // anchor.x = 0 (левый край)
+ case "center":
+ return 0.5; // anchor.x = 0.5 (центр)
+ case "right":
+ return 1; // anchor.x = 1 (правый край)
+ default:
+ return 0.5;
+ }
+ };
+
+ return (
+
+
+ {ruLabel && (
+
+ )}
+ {station.name && language !== "ru" && ruLabel && (
+
+ )}
+ {(isHovered || isControlHovered) && !isDragging && (
+
+ )}
+
+
+ );
+ }
+);
+
+// =========================================================================
+// Главный экспортируемый компонент: Станция
+// =========================================================================
+
+export const Station = ({
+ station,
+ ruLabel,
+ anchorPoint,
+ labelBlockAnchor,
+ labelAlign,
+ onLabelAlignChange,
+}: Readonly) => {
+ const draw = useCallback(
+ (g: Graphics) => {
g.clear();
const coordinates = coordinatesToLocal(
station.latitude,
station.longitude
);
- g.circle(
- coordinates.x * UP_SCALE,
- coordinates.y * UP_SCALE,
- STATION_RADIUS
- );
+ const radius = STATION_RADIUS;
+ g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, radius);
g.fill({ color: PATH_COLOR });
g.stroke({ color: BACKGROUND_COLOR, width: STATION_OUTLINE_WIDTH });
- }, []);
+ },
+ [station.latitude, station.longitude]
+ );
- return (
-
-
-
-
- );
- }
-);
-
-export const StationLabel = observer(
- ({ station, ruLabel }: Readonly) => {
- const { rotation, scale } = useTransform();
- const { setStationOffset } = useMapData();
-
- const [position, setPosition] = useState({
- x: station.offset_x,
- y: station.offset_y,
- });
- const [isDragging, setIsDragging] = useState(false);
- const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
- const [startMousePosition, setStartMousePosition] = useState({
- x: 0,
- y: 0,
- });
-
- if (!station) {
- console.error("station is null");
- return null;
- }
-
- const handlePointerDown = (e: FederatedMouseEvent) => {
- setIsDragging(true);
- setStartPosition({
- x: position.x,
- y: position.y,
- });
- setStartMousePosition({
- x: e.globalX,
- y: e.globalY,
- });
-
- e.stopPropagation();
- };
- const handlePointerMove = (e: FederatedMouseEvent) => {
- if (!isDragging) return;
- const dx = e.globalX - startMousePosition.x;
- const dy = e.globalY - startMousePosition.y;
- const newPosition = {
- x: startPosition.x + dx,
- y: startPosition.y + dy,
- };
- setPosition(newPosition);
- setStationOffset(station.id, newPosition.x, newPosition.y);
- e.stopPropagation();
- };
-
- const handlePointerUp = (e: FederatedMouseEvent) => {
- setIsDragging(false);
- e.stopPropagation();
- };
- const coordinates = coordinatesToLocal(station.latitude, station.longitude);
-
- return (
-
-
-
- {ruLabel && (
-
- )}
-
- );
- }
-);
+ return (
+
+
+
+
+ );
+};
diff --git a/src/pages/Route/route-preview/TransformContext.tsx b/src/pages/Route/route-preview/TransformContext.tsx
index 9e38225..8655b18 100644
--- a/src/pages/Route/route-preview/TransformContext.tsx
+++ b/src/pages/Route/route-preview/TransformContext.tsx
@@ -26,9 +26,12 @@ const TransformContext = createContext<{
rotationDegrees?: number,
scale?: number
) => void;
+ setScaleOnly: (newScale: number) => void;
+ setScaleWithoutMovingCenter: (newScale: number) => void;
setScreenCenter: React.Dispatch<
React.SetStateAction<{ x: number; y: number } | undefined>
>;
+ setScaleAtCenter: (newScale: number) => void;
}>({
position: { x: 0, y: 0 },
scale: 1,
@@ -41,7 +44,10 @@ const TransformContext = createContext<{
localToScreen: () => ({ x: 0, y: 0 }),
rotateToAngle: () => {},
setTransform: () => {},
+ setScaleOnly: () => {},
+ setScaleWithoutMovingCenter: () => {},
setScreenCenter: () => {},
+ setScaleAtCenter: () => {},
});
// Provider component
@@ -136,8 +142,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
useScale !== undefined ? useScale / SCALE_FACTOR : scale;
const center = screenCenter ?? { x: 0, y: 0 };
- console.log("center", center.x, center.y);
-
const newPosition = {
x: -latitude * UP_SCALE * selectedScale,
y: -longitude * UP_SCALE * selectedScale,
@@ -160,6 +164,37 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
[rotation, scale, screenCenter]
);
+ const setScaleAtCenter = useCallback(
+ (newScale: number) => {
+ if (scale === newScale) return;
+
+ const center = screenCenter ?? { x: 0, y: 0 };
+
+ const actualZoomFactor = newScale / scale;
+
+ const newPosition = {
+ x: position.x + (center.x - position.x) * (1 - actualZoomFactor),
+ y: position.y + (center.y - position.y) * (1 - actualZoomFactor),
+ };
+
+ setPosition(newPosition);
+ setScale(newScale);
+ },
+ [position, scale, screenCenter]
+ );
+
+ const setScaleOnly = useCallback((newScale: number) => {
+ // Изменяем только масштаб, не трогая позицию и поворот
+ setScale(newScale);
+ }, []);
+
+ const setScaleWithoutMovingCenter = useCallback(
+ (newScale: number) => {
+ setScale(newScale);
+ },
+ [setScale]
+ );
+
const value = useMemo(
() => ({
position,
@@ -173,17 +208,25 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
screenToLocal,
localToScreen,
setTransform,
+ setScaleOnly,
+ setScaleWithoutMovingCenter,
setScreenCenter,
+ setScaleAtCenter,
}),
[
position,
scale,
rotation,
screenCenter,
+ setScale,
rotateToAngle,
screenToLocal,
localToScreen,
setTransform,
+ setScaleOnly,
+ setScaleWithoutMovingCenter,
+ setScreenCenter,
+ setScaleAtCenter,
]
);
diff --git a/src/pages/Route/route-preview/Widgets.tsx b/src/pages/Route/route-preview/Widgets.tsx
index 2c0d966..000c26d 100644
--- a/src/pages/Route/route-preview/Widgets.tsx
+++ b/src/pages/Route/route-preview/Widgets.tsx
@@ -1,6 +1,11 @@
-import { Stack, Typography } from "@mui/material";
+import { Stack, Typography, Box, IconButton } from "@mui/material";
+import { Close } from "@mui/icons-material";
+import { Landmark } from "lucide-react";
+import { useMapData } from "./MapDataContext";
export function Widgets() {
+ const { selectedSight, setSelectedSight } = useMapData();
+
return (
+
+ {/* Виджет выбранной достопримечательности (заменяет виджет погоды) */}
-
- Погода
-
+ {selectedSight ? (
+
+ {/* Заголовок с кнопкой закрытия */}
+
+
+
+
+ {selectedSight.name}
+
+
+ setSelectedSight(undefined)}
+ sx={{
+ color: "#fff",
+ p: 0,
+ minWidth: 20,
+ width: 20,
+ height: 20,
+ "&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)" },
+ }}
+ >
+
+
+
+
+ {/* Описание достопримечательности */}
+ {selectedSight.address && (
+
+ {selectedSight.address}
+
+ )}
+
+ {/* Город */}
+ {selectedSight.city && (
+
+ Город: {selectedSight.city}
+
+ )}
+
+ ) : (
+
+
+
+ Выберите достопримечательность
+
+
+ )}
);
diff --git a/src/pages/Route/route-preview/index.tsx b/src/pages/Route/route-preview/index.tsx
index 00a537f..b390fcf 100644
--- a/src/pages/Route/route-preview/index.tsx
+++ b/src/pages/Route/route-preview/index.tsx
@@ -1,5 +1,5 @@
import { useRef, useEffect, useState } from "react";
-
+import { Widgets } from "./Widgets";
import { Application, extend } from "@pixi/react";
import {
Container,
@@ -14,16 +14,18 @@ import { MapDataProvider, useMapData } from "./MapDataContext";
import { TransformProvider, useTransform } from "./TransformContext";
import { InfiniteCanvas } from "./InfiniteCanvas";
-import { UP_SCALE } from "./Constants";
-import { Station } from "./Station";
import { TravelPath } from "./TravelPath";
import { LeftSidebar } from "./LeftSidebar";
import { RightSidebar } from "./RightSidebar";
-import { Widgets } from "./Widgets";
+
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";
extend({
Container,
@@ -43,8 +45,8 @@ export const RoutePreview = () => {
-
+
@@ -55,15 +57,27 @@ export const RoutePreview = () => {
export const RouteMap = observer(() => {
const { language } = languageStore;
- const { setPosition, screenToLocal, setTransform, screenCenter } =
- useTransform();
- const { routeData, stationData, sightData, originalRouteData } = useMapData();
- console.log(stationData);
+ const { setPosition, setTransform, screenCenter } = useTransform();
+ const {
+ routeData,
+ stationData,
+ sightData,
+ originalRouteData,
+ originalSightData,
+ } = useMapData();
+
const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
const [isSetup, setIsSetup] = useState(false);
const parentRef = useRef(null);
+ useEffect(() => {
+ document.body.style.overflow = "hidden";
+ return () => {
+ document.body.style.overflow = "auto";
+ };
+ }, []);
+
useEffect(() => {
if (originalRouteData) {
const path = originalRouteData?.path;
@@ -146,20 +160,14 @@ export const RouteMap = observer(() => {
key={obj.id}
ruLabel={
language === "ru"
- ? stationData.en[index].name
+ ? stationData.ru[index].name
: stationData.ru[index].name
}
/>
))}
-
- {
- g.clear();
- const localCenter = screenToLocal(0, 0);
- g.circle(localCenter.x, localCenter.y, 10);
- g.fill("#fff");
- }}
- />
+ {originalSightData?.map((sight: SightData, index: number) => {
+ return ;
+ })}
diff --git a/src/pages/Route/route-preview/types.ts b/src/pages/Route/route-preview/types.ts
index 6ced8be..d99942f 100644
--- a/src/pages/Route/route-preview/types.ts
+++ b/src/pages/Route/route-preview/types.ts
@@ -1,69 +1,72 @@
export interface RouteData {
- carrier: string;
- carrier_id: number;
- center_latitude: number;
- center_longitude: number;
- governor_appeal: number;
- id: number;
- path: [number, number][];
- rotate: number;
- route_direction: boolean;
- route_number: string;
- route_sys_number: string;
- scale_max: number;
- scale_min: number;
+ carrier: string;
+ carrier_id: number;
+ center_latitude: number;
+ center_longitude: number;
+ governor_appeal: number;
+ id: number;
+ path: [number, number][];
+ rotate: number;
+ route_direction: boolean;
+ route_number: string;
+ route_sys_number: string;
+ scale_max: number;
+ scale_min: number;
+ thumbnail?: string; // uuid логотипа маршрута
}
export interface StationTransferData {
- bus: string;
- metro_blue: string;
- metro_green: string;
- metro_orange: string;
- metro_purple: string;
- metro_red: string;
- train: string;
- tram: string;
- trolleybus: string;
+ bus: string;
+ metro_blue: string;
+ metro_green: string;
+ metro_orange: string;
+ metro_purple: string;
+ metro_red: string;
+ train: string;
+ tram: string;
+ trolleybus: string;
}
export interface StationData {
- address: string;
- city_id?: number;
- description: string;
- id: number;
- latitude: number;
- longitude: number;
- name: string;
- offset_x: number;
- offset_y: number;
- system_name: string;
- transfers: StationTransferData;
+ address: string;
+ city_id?: number;
+ description: string;
+ id: number;
+ latitude: number;
+ longitude: number;
+ name: string;
+ offset_x: number;
+ offset_y: number;
+ system_name: string;
+ transfers: StationTransferData;
+ align: number;
}
export interface StationPatchData {
- station_id: number;
- offset_x: number;
- offset_y: number;
- transfers: StationTransferData;
+ station_id: number;
+ offset_x: number;
+ offset_y: number;
+ align: number;
+ transfers: StationTransferData;
}
export interface SightPatchData {
- sight_id: number;
- latitude: number;
- longitude: number;
+ sight_id: number;
+ latitude: number;
+ longitude: number;
}
export interface SightData {
- address: string;
- city: string;
- city_id: number;
- id: number;
- latitude: number;
- left_article: number;
- longitude: number;
- name: string;
- preview_media: number;
- thumbnail: string; // uuid
- watermark_lu: string; // uuid
- watermark_rd: string; // uuid
-}
\ No newline at end of file
+ address: string;
+ city: string;
+ city_id: number;
+ id: number;
+ latitude: number;
+ left_article: number;
+ longitude: number;
+ name: string;
+ preview_media: number;
+ thumbnail: string; // uuid
+ watermark_lu: string; // uuid
+ watermark_rd: string; // uuid
+}
diff --git a/src/pages/Sight/SightListPage/index.tsx b/src/pages/Sight/SightListPage/index.tsx
index 486bd13..b96a8ba 100644
--- a/src/pages/Sight/SightListPage/index.tsx
+++ b/src/pages/Sight/SightListPage/index.tsx
@@ -1,10 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
+import { ruRU } from "@mui/x-data-grid/locales";
import { languageStore, sightsStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
+import { Box, CircularProgress } from "@mui/material";
export const SightListPage = observer(() => {
const { sights, getSights, deleteListSight } = sightsStore;
@@ -13,10 +15,16 @@ export const SightListPage = observer(() => {
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState
(null);
const [ids, setIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => {
- getSights();
+ const fetchSights = async () => {
+ setIsLoading(true);
+ await getSights();
+ setIsLoading(false);
+ };
+ fetchSights();
}, [language]);
const columns: GridColDef[] = [
@@ -116,12 +124,25 @@ export const SightListPage = observer(() => {
{
- setIds(Array.from(newSelection.ids) as number[]);
- }}
hideFooter
+ checkboxSelection
+ loading={isLoading}
+ localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
+ onRowSelectionModelChange={(newSelection) => {
+ console.log(newSelection);
+ setIds(Array.from(newSelection.ids as unknown as number[]));
+ }}
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? (
+
+ ) : (
+ "Нет достопримечательностей"
+ )}
+
+ ),
+ }}
/>
diff --git a/src/pages/Snapshot/SnapshotListPage/index.tsx b/src/pages/Snapshot/SnapshotListPage/index.tsx
index a1ca189..20c048c 100644
--- a/src/pages/Snapshot/SnapshotListPage/index.tsx
+++ b/src/pages/Snapshot/SnapshotListPage/index.tsx
@@ -1,10 +1,11 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
+import { ruRU } from "@mui/x-data-grid/locales";
import { languageStore, snapshotStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { DatabaseBackup, Trash2 } from "lucide-react";
-
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
+import { Box, CircularProgress } from "@mui/material";
export const SnapshotListPage = observer(() => {
const { snapshots, getSnapshots, deleteSnapshot, restoreSnapshot } =
@@ -14,9 +15,15 @@ export const SnapshotListPage = observer(() => {
const [rowId, setRowId] = useState(null); // Lifted state
const { language } = languageStore;
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
- getSnapshots();
+ const fetchSnapshots = async () => {
+ setIsLoading(true);
+ await getSnapshots();
+ setIsLoading(false);
+ };
+ fetchSnapshots();
}, [language]);
const columns: GridColDef[] = [
@@ -81,6 +88,15 @@ export const SnapshotListPage = observer(() => {
columns={columns}
hideFooterPagination
hideFooter
+ loading={isLoading}
+ localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? : "Нет снапшотов"}
+
+ ),
+ }}
/>
diff --git a/src/pages/Station/LinkedSights.tsx b/src/pages/Station/LinkedSights.tsx
new file mode 100644
index 0000000..0a59cab
--- /dev/null
+++ b/src/pages/Station/LinkedSights.tsx
@@ -0,0 +1,317 @@
+import { useState, useEffect } from "react";
+import {
+ Stack,
+ Typography,
+ Button,
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ useTheme,
+ TextField,
+ Autocomplete,
+ TableCell,
+ TableContainer,
+ Table,
+ TableHead,
+ TableRow,
+ Paper,
+ TableBody,
+} from "@mui/material";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+
+import { authInstance, languageStore } from "@shared";
+
+type Field = {
+ label: string;
+ data: keyof T;
+ render?: (value: any) => React.ReactNode;
+};
+
+type LinkedSightsProps = {
+ parentId: string | number;
+ fields: Field[];
+ setItemsParent?: (items: T[]) => void;
+ type: "show" | "edit";
+ onUpdate?: () => void;
+ disableCreation?: boolean;
+ updatedLinkedItems?: T[];
+ refresh?: number;
+};
+
+export const LinkedSights = <
+ T extends { id: number; name: string; [key: string]: any }
+>(
+ props: LinkedSightsProps
+) => {
+ const theme = useTheme();
+
+ return (
+ <>
+
+ }
+ sx={{
+ background: theme.palette.background.paper,
+ borderBottom: `1px solid ${theme.palette.divider}`,
+ width: "100%",
+ }}
+ >
+
+ Привязанные достопримечательности
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export const LinkedSightsContents = <
+ T extends { id: number; name: string; [key: string]: any }
+>({
+ parentId,
+ setItemsParent,
+ fields,
+ type,
+ onUpdate,
+ disableCreation = false,
+ updatedLinkedItems,
+ refresh,
+}: LinkedSightsProps) => {
+ const { language } = languageStore;
+
+ const [allItems, setAllItems] = useState([]);
+ const [linkedItems, setLinkedItems] = useState([]);
+ const [selectedItemId, setSelectedItemId] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ console.log(error);
+ }, [error]);
+
+ const parentResource = "station";
+ const childResource = "sight";
+
+ const availableItems = allItems
+ .filter((item) => !linkedItems.some((linked) => linked.id === item.id))
+ .sort((a, b) => a.name.localeCompare(b.name));
+
+ useEffect(() => {
+ if (updatedLinkedItems) {
+ setLinkedItems(updatedLinkedItems);
+ }
+ }, [updatedLinkedItems]);
+
+ useEffect(() => {
+ setItemsParent?.(linkedItems);
+ }, [linkedItems, setItemsParent]);
+
+ const linkItem = () => {
+ if (selectedItemId !== null) {
+ setError(null);
+ const requestData = {
+ sight_id: selectedItemId,
+ };
+
+ authInstance
+ .post(`/${parentResource}/${parentId}/${childResource}`, requestData)
+ .then(() => {
+ const newItem = allItems.find((item) => item.id === selectedItemId);
+ if (newItem) {
+ setLinkedItems([...linkedItems, newItem]);
+ }
+ setSelectedItemId(null);
+ onUpdate?.();
+ })
+ .catch((error) => {
+ console.error("Error linking sight:", error);
+ setError("Failed to link sight");
+ });
+ }
+ };
+
+ const deleteItem = (itemId: number) => {
+ setError(null);
+ authInstance
+ .delete(`/${parentResource}/${parentId}/${childResource}`, {
+ data: { [`${childResource}_id`]: itemId },
+ })
+ .then(() => {
+ setLinkedItems(linkedItems.filter((item) => item.id !== itemId));
+ onUpdate?.();
+ })
+ .catch((error) => {
+ console.error("Error deleting sight:", error);
+ setError("Failed to delete sight");
+ });
+ };
+
+ useEffect(() => {
+ if (parentId) {
+ setIsLoading(true);
+ setError(null);
+ authInstance
+ .get(`/${parentResource}/${parentId}/${childResource}`)
+ .then((response) => {
+ setLinkedItems(response?.data || []);
+ })
+ .catch((error) => {
+ console.error("Error fetching linked sights:", error);
+ setError("Failed to load linked sights");
+ setLinkedItems([]);
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ }
+ }, [parentId, language, refresh]);
+
+ useEffect(() => {
+ if (type === "edit") {
+ setError(null);
+ authInstance
+ .get(`/${childResource}/`)
+ .then((response) => {
+ setAllItems(response?.data || []);
+ })
+ .catch((error) => {
+ console.error("Error fetching all sights:", error);
+ setError("Failed to load available sights");
+ setAllItems([]);
+ });
+ }
+ }, [type]);
+
+ return (
+ <>
+ {linkedItems?.length > 0 && (
+
+
+
+
+
+ №
+
+ {fields.map((field) => (
+ {field.label}
+ ))}
+ {type === "edit" && (
+ Действие
+ )}
+
+
+
+
+ {linkedItems.map((item, index) => (
+
+ {index + 1}
+ {fields.map((field, idx) => (
+
+ {field.render
+ ? field.render(item[field.data])
+ : item[field.data]}
+
+ ))}
+ {type === "edit" && (
+
+ {
+ e.stopPropagation();
+ deleteItem(item.id);
+ }}
+ >
+ Отвязать
+
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+ {linkedItems.length === 0 && !isLoading && (
+
+ Достопримечательности не найдены
+
+ )}
+
+ {type === "edit" && !disableCreation && (
+
+
+ Добавить достопримечательность
+
+ item.id === selectedItemId) || null
+ }
+ onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
+ options={availableItems}
+ getOptionLabel={(item) => String(item.name)}
+ renderInput={(params) => (
+
+ )}
+ 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) => (
+
+ {String(option.name)}
+
+ )}
+ />
+
+
+ Добавить
+
+
+ )}
+
+ {isLoading && (
+
+ Загрузка...
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+ >
+ );
+};
diff --git a/src/pages/Station/StationCreatePage/index.tsx b/src/pages/Station/StationCreatePage/index.tsx
index c5be96d..8307067 100644
--- a/src/pages/Station/StationCreatePage/index.tsx
+++ b/src/pages/Station/StationCreatePage/index.tsx
@@ -44,7 +44,7 @@ export const StationCreatePage = observer(() => {
try {
setIsLoading(true);
await createStation();
- toast.success("Станция успешно создана");
+ toast.success("Остановка успешно создана");
navigate("/station");
} catch (error) {
console.error("Error creating station:", error);
@@ -79,7 +79,7 @@ export const StationCreatePage = observer(() => {
-
Создание станции
+ Создание остановки
{
- setLanguageCreateStationData(language, {
+ setCreateCommonData({
description: e.target.value,
})
}
/>
- {
address: e.target.value,
})
}
- />
+ /> */}
{
const navigate = useNavigate();
@@ -31,6 +32,11 @@ export const StationEditPage = observer(() => {
const { cities, getCities } = cityStore;
const [coordinates, setCoordinates] = useState("");
+ useEffect(() => {
+ // Устанавливаем русский язык при загрузке страницы
+ languageStore.setLanguage("ru");
+ }, []);
+
useEffect(() => {
if (
editStationData.common.latitude !== 0 ||
@@ -46,7 +52,7 @@ export const StationEditPage = observer(() => {
try {
setIsLoading(true);
await editStation(Number(id));
- toast.success("Станция успешно обновлена");
+ toast.success("Остановка успешно обновлена");
} catch (error) {
console.error("Error updating station:", error);
toast.error("Ошибка при обновлении станции");
@@ -118,15 +124,15 @@ export const StationEditPage = observer(() => {
- setLanguageEditStationData(language, {
+ setEditCommonData({
description: e.target.value,
})
}
/>
- {
address: e.target.value,
})
}
- />
+ /> */}
{
+ {id && (
+
+ )}
+
{
{isLoading ? (
) : (
- "Обновить"
+ "Сохранить"
)}
diff --git a/src/pages/Station/StationListPage/index.tsx b/src/pages/Station/StationListPage/index.tsx
index 149209f..953e11e 100644
--- a/src/pages/Station/StationListPage/index.tsx
+++ b/src/pages/Station/StationListPage/index.tsx
@@ -1,10 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
+import { ruRU } from "@mui/x-data-grid/locales";
import { languageStore, stationsStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
+import { Box, CircularProgress } from "@mui/material";
export const StationListPage = observer(() => {
const { stationLists, getStationList, deleteStation } = stationsStore;
@@ -13,10 +15,16 @@ export const StationListPage = observer(() => {
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState(null);
const [ids, setIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => {
- getStationList();
+ const fetchStations = async () => {
+ setIsLoading(true);
+ await getStationList();
+ setIsLoading(false);
+ };
+ fetchStations();
}, [language]);
const columns: GridColDef[] = [
@@ -115,7 +123,7 @@ export const StationListPage = observer(() => {
Станции
-
+
{
columns={columns}
hideFooterPagination
checkboxSelection
+ loading={isLoading}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as number[]);
}}
hideFooter
+ localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? : "Нет станций"}
+
+ ),
+ }}
/>
diff --git a/src/pages/Station/StationPreviewPage/index.tsx b/src/pages/Station/StationPreviewPage/index.tsx
index cdedf7e..d45dc72 100644
--- a/src/pages/Station/StationPreviewPage/index.tsx
+++ b/src/pages/Station/StationPreviewPage/index.tsx
@@ -5,6 +5,7 @@ import { observer } from "mobx-react-lite";
import { ArrowLeft } from "lucide-react";
import { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
+import { LinkedSights } from "../LinkedSights";
export const StationPreviewPage = observer(() => {
const { id } = useParams();
@@ -71,6 +72,17 @@ export const StationPreviewPage = observer(() => {
{stationPreview[id!]?.[language]?.data.description}
)}
+
+ {id && (
+
+ )}
);
diff --git a/src/pages/Station/index.ts b/src/pages/Station/index.ts
index 610bcc7..98394af 100644
--- a/src/pages/Station/index.ts
+++ b/src/pages/Station/index.ts
@@ -2,3 +2,4 @@ export * from "./StationListPage";
export * from "./StationCreatePage";
export * from "./StationPreviewPage";
export * from "./StationEditPage";
+export * from "./LinkedSights";
diff --git a/src/pages/User/UserEditPage/index.tsx b/src/pages/User/UserEditPage/index.tsx
index a7c8f3c..b6def48 100644
--- a/src/pages/User/UserEditPage/index.tsx
+++ b/src/pages/User/UserEditPage/index.tsx
@@ -10,15 +10,21 @@ 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 } from "@shared";
+import { userStore, languageStore } from "@shared";
import { useEffect, useState } from "react";
export const UserEditPage = observer(() => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
+
const { id } = useParams();
const { editUserData, editUser, getUser, setEditUserData } = userStore;
+ useEffect(() => {
+ // Устанавливаем русский язык при загрузке страницы
+ languageStore.setLanguage("ru");
+ }, []);
+
const handleEdit = async () => {
try {
setIsLoading(true);
@@ -130,7 +136,7 @@ export const UserEditPage = observer(() => {
{isLoading ? (
) : (
- "Обновить"
+ "Сохранить"
)}
diff --git a/src/pages/User/UserListPage/index.tsx b/src/pages/User/UserListPage/index.tsx
index 307cc48..a4ef2e8 100644
--- a/src/pages/User/UserListPage/index.tsx
+++ b/src/pages/User/UserListPage/index.tsx
@@ -1,11 +1,12 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
+import { ruRU } from "@mui/x-data-grid/locales";
import { userStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom";
-
import { CreateButton, DeleteModal } from "@widgets";
+import { Box, CircularProgress } from "@mui/material";
export const UserListPage = observer(() => {
const { users, getUsers, deleteUser } = userStore;
@@ -14,9 +15,15 @@ export const UserListPage = observer(() => {
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState(null);
const [ids, setIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
- getUsers();
+ const fetchUsers = async () => {
+ setIsLoading(true);
+ await getUsers();
+ setIsLoading(false);
+ };
+ fetchUsers();
}, []);
const columns: GridColDef[] = [
@@ -136,10 +143,23 @@ export const UserListPage = observer(() => {
columns={columns}
hideFooterPagination
checkboxSelection
+ loading={isLoading}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as number[]);
}}
hideFooter
+ localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? (
+
+ ) : (
+ "Нет пользователей"
+ )}
+
+ ),
+ }}
/>
diff --git a/src/pages/Vehicle/VehicleEditPage/index.tsx b/src/pages/Vehicle/VehicleEditPage/index.tsx
index 5e3858f..55a5bd4 100644
--- a/src/pages/Vehicle/VehicleEditPage/index.tsx
+++ b/src/pages/Vehicle/VehicleEditPage/index.tsx
@@ -31,6 +31,12 @@ export const VehicleEditPage = observer(() => {
} = vehicleStore;
const { getCarriers } = carrierStore;
const { language } = languageStore;
+
+ useEffect(() => {
+ // Устанавливаем русский язык при загрузке страницы
+ languageStore.setLanguage("ru");
+ }, []);
+
useEffect(() => {
(async () => {
await getVehicle(Number(id));
diff --git a/src/pages/Vehicle/VehicleListPage/index.tsx b/src/pages/Vehicle/VehicleListPage/index.tsx
index ecfd37c..d6333bf 100644
--- a/src/pages/Vehicle/VehicleListPage/index.tsx
+++ b/src/pages/Vehicle/VehicleListPage/index.tsx
@@ -1,4 +1,5 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
+import { ruRU } from "@mui/x-data-grid/locales";
import { carrierStore, languageStore, vehicleStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
@@ -6,6 +7,7 @@ import { Eye, Pencil, Trash2, Minus } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal } from "@widgets";
import { VEHICLE_TYPES } from "@shared";
+import { Box, CircularProgress } from "@mui/material";
export const VehicleListPage = observer(() => {
const { vehicles, getVehicles, deleteVehicle } = vehicleStore;
@@ -15,11 +17,17 @@ export const VehicleListPage = observer(() => {
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState(null);
const [ids, setIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => {
- getVehicles();
- getCarriers(language);
+ const fetchData = async () => {
+ setIsLoading(true);
+ await getVehicles();
+ await getCarriers(language);
+ setIsLoading(false);
+ };
+ fetchData();
}, [language]);
const columns: GridColDef[] = [
@@ -157,10 +165,23 @@ export const VehicleListPage = observer(() => {
columns={columns}
hideFooterPagination
checkboxSelection
+ loading={isLoading}
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids) as number[]);
}}
hideFooter
+ localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
+ slots={{
+ noRowsOverlay: () => (
+
+ {isLoading ? (
+
+ ) : (
+ "Нет транспортных средств"
+ )}
+
+ ),
+ }}
/>
diff --git a/src/shared/config/constants.tsx b/src/shared/config/constants.tsx
index 246ba59..218d32d 100644
--- a/src/shared/config/constants.tsx
+++ b/src/shared/config/constants.tsx
@@ -11,10 +11,10 @@ import {
// Car,
Table,
Split,
- Newspaper,
+ // Newspaper,
PersonStanding,
Cpu,
- BookImage,
+ // BookImage,
} from "lucide-react";
import { CarrierSvg } from "./CarrierSvg";
@@ -70,18 +70,18 @@ export const NAVIGATION_ITEMS: {
label: "Справочник",
icon: Table,
nestedItems: [
- {
- id: "media",
- label: "Медиа",
- icon: BookImage,
- path: "/media",
- },
- {
- id: "articles",
- label: "Статьи",
- icon: Newspaper,
- path: "/article",
- },
+ // {
+ // id: "media",
+ // label: "Медиа",
+ // icon: BookImage,
+ // path: "/media",
+ // },
+ // {
+ // id: "articles",
+ // label: "Статьи",
+ // icon: Newspaper,
+ // path: "/article",
+ // },
{
id: "attractions",
label: "Достопримечательности",
diff --git a/src/shared/const/index.ts b/src/shared/const/index.ts
index f1c1672..be30fa8 100644
--- a/src/shared/const/index.ts
+++ b/src/shared/const/index.ts
@@ -18,3 +18,752 @@ export const MEDIA_TYPE_VALUES = {
panorama: 5,
model: 6,
};
+
+export const RU_COUNTRIES = [
+ { code: "AF", name: "Афганистан" },
+ { code: "AX", name: "Аландские острова" },
+ { code: "AL", name: "Албания" },
+ { code: "DZ", name: "Алжир" },
+ { code: "AS", name: "Американское Самоа" },
+ { code: "AD", name: "Андорра" },
+ { code: "AO", name: "Ангола" },
+ { code: "AI", name: "Ангилья" },
+ { code: "AQ", name: "Антарктида" },
+ { code: "AG", name: "Антигуа и Барбуда" },
+ { code: "AR", name: "Аргентина" },
+ { code: "AM", name: "Армения" },
+ { code: "AW", name: "Аруба" },
+ { code: "AU", name: "Австралия" },
+ { code: "AT", name: "Австрия" },
+ { code: "AZ", name: "Азербайджан" },
+ { code: "BS", name: "Багамы" },
+ { code: "BH", name: "Бахрейн" },
+ { code: "BD", name: "Бангладеш" },
+ { code: "BB", name: "Барбадос" },
+ { code: "BY", name: "Беларусь" },
+ { code: "BE", name: "Бельгия" },
+ { code: "BZ", name: "Белиз" },
+ { code: "BJ", name: "Бенин" },
+ { code: "BM", name: "Бермуды" },
+ { code: "BT", name: "Бутан" },
+ { code: "BO", name: "Боливия" },
+ { code: "BA", name: "Босния и Герцеговина" },
+ { code: "BW", name: "Ботсвана" },
+ { code: "BV", name: "Остров Буве" },
+ { code: "BR", name: "Бразилия" },
+ { code: "IO", name: "Британская территория в Индийском океане" },
+ { code: "BN", name: "Бруней-Даруссалам" },
+ { code: "BG", name: "Болгария" },
+ { code: "BF", name: "Буркина-Фасо" },
+ { code: "BI", name: "Бурунди" },
+ { code: "KH", name: "Камбоджа" },
+ { code: "CM", name: "Камерун" },
+ { code: "CA", name: "Канада" },
+ { code: "CV", name: "Кабо-Верде" },
+ { code: "KY", name: "Каймановы острова" },
+ { code: "CF", name: "Центральноафриканская Республика" },
+ { code: "TD", name: "Чад" },
+ { code: "CL", name: "Чили" },
+ { code: "CN", name: "Китай" },
+ { code: "CX", name: "Остров Рождества" },
+ { code: "CC", name: "Кокосовые (Килинг) острова" },
+ { code: "CO", name: "Колумбия" },
+ { code: "KM", name: "Коморы" },
+ { code: "CG", name: "Конго" },
+ { code: "CD", name: "Демократическая Республика Конго" },
+ { code: "CK", name: "Острова Кука" },
+ { code: "CR", name: "Коста-Рика" },
+ { code: "CI", name: "Кот-д'Ивуар" },
+ { code: "HR", name: "Хорватия" },
+ { code: "CU", name: "Куба" },
+ { code: "CY", name: "Кипр" },
+ { code: "CZ", name: "Чехия" },
+ { code: "DK", name: "Дания" },
+ { code: "DJ", name: "Джибути" },
+ { code: "DM", name: "Доминика" },
+ { code: "DO", name: "Доминиканская Республика" },
+ { code: "EC", name: "Эквадор" },
+ { code: "EG", name: "Египет" },
+ { code: "SV", name: "Сальвадор" },
+ { code: "GQ", name: "Экваториальная Гвинея" },
+ { code: "ER", name: "Эритрея" },
+ { code: "EE", name: "Эстония" },
+ { code: "ET", name: "Эфиопия" },
+ { code: "FK", name: "Фолклендские острова (Мальвинские)" },
+ { code: "FO", name: "Фарерские острова" },
+ { code: "FJ", name: "Фиджи" },
+ { code: "FI", name: "Финляндия" },
+ { code: "FR", name: "Франция" },
+ { code: "GF", name: "Французская Гвиана" },
+ { code: "PF", name: "Французская Полинезия" },
+ { code: "TF", name: "Французские Южные территории" },
+ { code: "GA", name: "Габон" },
+ { code: "GM", name: "Гамбия" },
+ { code: "GE", name: "Грузия" },
+ { code: "DE", name: "Германия" },
+ { code: "GH", name: "Гана" },
+ { code: "GI", name: "Гибралтар" },
+ { code: "GR", name: "Греция" },
+ { code: "GL", name: "Гренландия" },
+ { code: "GD", name: "Гренада" },
+ { code: "GP", name: "Гваделупа" },
+ { code: "GU", name: "Гуам" },
+ { code: "GT", name: "Гватемала" },
+ { code: "GG", name: "Гернси" },
+ { code: "GN", name: "Гвинея" },
+ { code: "GW", name: "Гвинея-Бисау" },
+ { code: "GY", name: "Гайана" },
+ { code: "HT", name: "Гаити" },
+ { code: "HM", name: "Остров Херд и острова Макдональд" },
+ { code: "VA", name: "Ватикан" },
+ { code: "HN", name: "Гондурас" },
+ { code: "HK", name: "Гонконг" },
+ { code: "HU", name: "Венгрия" },
+ { code: "IS", name: "Исландия" },
+ { code: "IN", name: "Индия" },
+ { code: "ID", name: "Индонезия" },
+ { code: "IR", name: "Иран" },
+ { code: "IQ", name: "Ирак" },
+ { code: "IE", name: "Ирландия" },
+ { code: "IM", name: "Остров Мэн" },
+ { code: "IL", name: "Израиль" },
+ { code: "IT", name: "Италия" },
+ { code: "JM", name: "Ямайка" },
+ { code: "JP", name: "Япония" },
+ { code: "JE", name: "Джерси" },
+ { code: "JO", name: "Иордания" },
+ { code: "KZ", name: "Казахстан" },
+ { code: "KE", name: "Кения" },
+ { code: "KI", name: "Кирибати" },
+ { code: "KR", name: "Корея" },
+ { code: "KP", name: "Северная Корея" },
+ { code: "KW", name: "Кувейт" },
+ { code: "KG", name: "Киргизия" },
+ { code: "LA", name: "Лаос" },
+ { code: "LV", name: "Латвия" },
+ { code: "LB", name: "Ливан" },
+ { code: "LS", name: "Лесото" },
+ { code: "LR", name: "Либерия" },
+ { code: "LY", name: "Ливия" },
+ { code: "LI", name: "Лихтенштейн" },
+ { code: "LT", name: "Литва" },
+ { code: "LU", name: "Люксембург" },
+ { code: "MO", name: "Макао" },
+ { code: "MK", name: "Северная Македония" },
+ { code: "MG", name: "Мадагаскар" },
+ { code: "MW", name: "Малави" },
+ { code: "MY", name: "Малайзия" },
+ { code: "MV", name: "Мальдивы" },
+ { code: "ML", name: "Мали" },
+ { code: "MT", name: "Мальта" },
+ { code: "MH", name: "Маршалловы Острова" },
+ { code: "MQ", name: "Мартиника" },
+ { code: "MR", name: "Мавритания" },
+ { code: "MU", name: "Маврикий" },
+ { code: "YT", name: "Майотта" },
+ { code: "MX", name: "Мексика" },
+ { code: "FM", name: "Микронезия" },
+ { code: "MD", name: "Молдова" },
+ { code: "MC", name: "Монако" },
+ { code: "MN", name: "Монголия" },
+ { code: "ME", name: "Черногория" },
+ { code: "MS", name: "Монтсеррат" },
+ { code: "MA", name: "Марокко" },
+ { code: "MZ", name: "Мозамбик" },
+ { code: "MM", name: "Мьянма" },
+ { code: "NA", name: "Намибия" },
+ { code: "NR", name: "Науру" },
+ { code: "NP", name: "Непал" },
+ { code: "NL", name: "Нидерланды" },
+ { code: "AN", name: "Нидерландские Антильские острова" },
+ { code: "NC", name: "Новая Каледония" },
+ { code: "NZ", name: "Новая Зеландия" },
+ { code: "NI", name: "Никарагуа" },
+ { code: "NE", name: "Нигер" },
+ { code: "NG", name: "Нигерия" },
+ { code: "NU", name: "Ниуэ" },
+ { code: "NF", name: "Остров Норфолк" },
+ { code: "MP", name: "Северные Марианские острова" },
+ { code: "NO", name: "Норвегия" },
+ { code: "OM", name: "Оман" },
+ { code: "PK", name: "Пакистан" },
+ { code: "PW", name: "Палау" },
+ { code: "PS", name: "Палестинская территория" },
+ { code: "PA", name: "Панама" },
+ { code: "PG", name: "Папуа — Новая Гвинея" },
+ { code: "PY", name: "Парагвай" },
+ { code: "PE", name: "Перу" },
+ { code: "PH", name: "Филиппины" },
+ { code: "PN", name: "Питкэрн" },
+ { code: "PL", name: "Польша" },
+ { code: "PT", name: "Португалия" },
+ { code: "PR", name: "Пуэрто-Рико" },
+ { code: "QA", name: "Катар" },
+ { code: "RE", name: "Реюньон" },
+ { code: "RO", name: "Румыния" },
+ { code: "RU", name: "Россия" },
+ { code: "RW", name: "Руанда" },
+ { code: "BL", name: "Сен-Бартелеми" },
+ { code: "SH", name: "Остров Святой Елены" },
+ { code: "KN", name: "Сент-Китс и Невис" },
+ { code: "LC", name: "Сент-Люсия" },
+ { code: "MF", name: "Сен-Мартен" },
+ { code: "PM", name: "Сен-Пьер и Микелон" },
+ { code: "VC", name: "Сент-Винсент и Гренадины" },
+ { code: "WS", name: "Самоа" },
+ { code: "SM", name: "Сан-Марино" },
+ { code: "ST", name: "Сан-Томе и Принсипи" },
+ { code: "SA", name: "Саудовская Аравия" },
+ { code: "SN", name: "Сенегал" },
+ { code: "RS", name: "Сербия" },
+ { code: "SC", name: "Сейшельские Острова" },
+ { code: "SL", name: "Сьерра-Леоне" },
+ { code: "SG", name: "Сингапур" },
+ { code: "SK", name: "Словакия" },
+ { code: "SI", name: "Словения" },
+ { code: "SB", name: "Соломоновы Острова" },
+ { code: "SO", name: "Сомали" },
+ { code: "ZA", name: "Южная Африка" },
+ { code: "GS", name: "Южная Георгия и Южные Сандвичевы острова" },
+ { code: "ES", name: "Испания" },
+ { code: "LK", name: "Шри-Ланка" },
+ { code: "SD", name: "Судан" },
+ { code: "SR", name: "Суринам" },
+ { code: "SJ", name: "Шпицберген и Ян-Майен" },
+ { code: "SZ", name: "Свазиленд" },
+ { code: "SE", name: "Швеция" },
+ { code: "CH", name: "Швейцария" },
+ { code: "SY", name: "Сирия" },
+ { code: "TW", name: "Тайвань" },
+ { code: "TJ", name: "Таджикистан" },
+ { code: "TZ", name: "Танзания" },
+ { code: "TH", name: "Таиланд" },
+ { code: "TL", name: "Восточный Тимор" },
+ { code: "TG", name: "Того" },
+ { code: "TK", name: "Токелау" },
+ { code: "TO", name: "Тонга" },
+ { code: "TT", name: "Тринидад и Тобаго" },
+ { code: "TN", name: "Тунис" },
+ { code: "TR", name: "Турция" },
+ { code: "TM", name: "Туркмения" },
+ { code: "TC", name: "Теркс и Кайкос" },
+ { code: "TV", name: "Тувалу" },
+ { code: "UG", name: "Уганда" },
+ { code: "UA", name: "Украина" },
+ { code: "AE", name: "Объединённые Арабские Эмираты" },
+ { code: "GB", name: "Великобритания" },
+ { code: "US", name: "США" },
+ { code: "UM", name: "Внешние малые острова США" },
+ { code: "UY", name: "Уругвай" },
+ { code: "UZ", name: "Узбекистан" },
+ { code: "VU", name: "Вануату" },
+ { code: "VE", name: "Венесуэла" },
+ { code: "VN", name: "Вьетнам" },
+ { code: "VG", name: "Британские Виргинские острова" },
+ { code: "VI", name: "Виргинские острова (США)" },
+ { code: "WF", name: "Уоллис и Футуна" },
+ { code: "EH", name: "Западная Сахара" },
+ { code: "YE", name: "Йемен" },
+ { code: "ZM", name: "Замбия" },
+ { code: "ZW", name: "Зимбабве" },
+];
+
+// countries-en.js
+export const EN_COUNTRIES = [
+ { code: "AF", name: "Afghanistan" },
+ { code: "AX", name: "Aland Islands" },
+ { code: "AL", name: "Albania" },
+ { code: "DZ", name: "Algeria" },
+ { code: "AS", name: "American Samoa" },
+ { code: "AD", name: "Andorra" },
+ { code: "AO", name: "Angola" },
+ { code: "AI", name: "Anguilla" },
+ { code: "AQ", name: "Antarctica" },
+ { code: "AG", name: "Antigua And Barbuda" },
+ { code: "AR", name: "Argentina" },
+ { code: "AM", name: "Armenia" },
+ { code: "AW", name: "Aruba" },
+ { code: "AU", name: "Australia" },
+ { code: "AT", name: "Austria" },
+ { code: "AZ", name: "Azerbaijan" },
+ { code: "BS", name: "Bahamas" },
+ { code: "BH", name: "Bahrain" },
+ { code: "BD", name: "Bangladesh" },
+ { code: "BB", name: "Barbados" },
+ { code: "BY", name: "Belarus" },
+ { code: "BE", name: "Belgium" },
+ { code: "BZ", name: "Belize" },
+ { code: "BJ", name: "Benin" },
+ { code: "BM", name: "Bermuda" },
+ { code: "BT", name: "Bhutan" },
+ { code: "BO", name: "Bolivia" },
+ { code: "BA", name: "Bosnia And Herzegovina" },
+ { code: "BW", name: "Botswana" },
+ { code: "BV", name: "Bouvet Island" },
+ { code: "BR", name: "Brazil" },
+ { code: "IO", name: "British Indian Ocean Territory" },
+ { code: "BN", name: "Brunei Darussalam" },
+ { code: "BG", name: "Bulgaria" },
+ { code: "BF", name: "Burkina Faso" },
+ { code: "BI", name: "Burundi" },
+ { code: "KH", name: "Cambodia" },
+ { code: "CM", name: "Cameroon" },
+ { code: "CA", name: "Canada" },
+ { code: "CV", name: "Cape Verde" },
+ { code: "KY", name: "Cayman Islands" },
+ { code: "CF", name: "Central African Republic" },
+ { code: "TD", name: "Chad" },
+ { code: "CL", name: "Chile" },
+ { code: "CN", name: "China" },
+ { code: "CX", name: "Christmas Island" },
+ { code: "CC", name: "Cocos (Keeling) Islands" },
+ { code: "CO", name: "Colombia" },
+ { code: "KM", name: "Comoros" },
+ { code: "CG", name: "Congo" },
+ { code: "CD", name: "Congo, Democratic Republic" },
+ { code: "CK", name: "Cook Islands" },
+ { code: "CR", name: "Costa Rica" },
+ { code: "CI", name: "Cote D'Ivoire" },
+ { code: "HR", name: "Croatia" },
+ { code: "CU", name: "Cuba" },
+ { code: "CY", name: "Cyprus" },
+ { code: "CZ", name: "Czech Republic" },
+ { code: "DK", name: "Denmark" },
+ { code: "DJ", name: "Djibouti" },
+ { code: "DM", name: "Dominica" },
+ { code: "DO", name: "Dominican Republic" },
+ { code: "EC", name: "Ecuador" },
+ { code: "EG", name: "Egypt" },
+ { code: "SV", name: "El Salvador" },
+ { code: "GQ", name: "Equatorial Guinea" },
+ { code: "ER", name: "Eritrea" },
+ { code: "EE", name: "Estonia" },
+ { code: "ET", name: "Ethiopia" },
+ { code: "FK", name: "Falkland Islands (Malvinas)" },
+ { code: "FO", name: "Faroe Islands" },
+ { code: "FJ", name: "Fiji" },
+ { code: "FI", name: "Finland" },
+ { code: "FR", name: "France" },
+ { code: "GF", name: "French Guiana" },
+ { code: "PF", name: "French Polynesia" },
+ { code: "TF", name: "French Southern Territories" },
+ { code: "GA", name: "Gabon" },
+ { code: "GM", name: "Gambia" },
+ { code: "GE", name: "Georgia" },
+ { code: "DE", name: "Germany" },
+ { code: "GH", name: "Ghana" },
+ { code: "GI", name: "Gibraltar" },
+ { code: "GR", name: "Greece" },
+ { code: "GL", name: "Greenland" },
+ { code: "GD", name: "Grenada" },
+ { code: "GP", name: "Guadeloupe" },
+ { code: "GU", name: "Guam" },
+ { code: "GT", name: "Guatemala" },
+ { code: "GG", name: "Guernsey" },
+ { code: "GN", name: "Guinea" },
+ { code: "GW", name: "Guinea-Bissau" },
+ { code: "GY", name: "Guyana" },
+ { code: "HT", name: "Haiti" },
+ { code: "HM", name: "Heard Island & Mcdonald Islands" },
+ { code: "VA", name: "Holy See (Vatican City State)" },
+ { code: "HN", name: "Honduras" },
+ { code: "HK", name: "Hong Kong" },
+ { code: "HU", name: "Hungary" },
+ { code: "IS", name: "Iceland" },
+ { code: "IN", name: "India" },
+ { code: "ID", name: "Indonesia" },
+ { code: "IR", name: "Iran, Islamic Republic Of" },
+ { code: "IQ", name: "Iraq" },
+ { code: "IE", name: "Ireland" },
+ { code: "IM", name: "Isle Of Man" },
+ { code: "IL", name: "Israel" },
+ { code: "IT", name: "Italy" },
+ { code: "JM", name: "Jamaica" },
+ { code: "JP", name: "Japan" },
+ { code: "JE", name: "Jersey" },
+ { code: "JO", name: "Jordan" },
+ { code: "KZ", name: "Kazakhstan" },
+ { code: "KE", name: "Kenya" },
+ { code: "KI", name: "Kiribati" },
+ { code: "KR", name: "Korea" },
+ { code: "KP", name: "North Korea" },
+ { code: "KW", name: "Kuwait" },
+ { code: "KG", name: "Kyrgyzstan" },
+ { code: "LA", name: "Lao People's Democratic Republic" },
+ { code: "LV", name: "Latvia" },
+ { code: "LB", name: "Lebanon" },
+ { code: "LS", name: "Lesotho" },
+ { code: "LR", name: "Liberia" },
+ { code: "LY", name: "Libyan Arab Jamahiriya" },
+ { code: "LI", name: "Liechtenstein" },
+ { code: "LT", name: "Lithuania" },
+ { code: "LU", name: "Luxembourg" },
+ { code: "MO", name: "Macao" },
+ { code: "MK", name: "Macedonia" },
+ { code: "MG", name: "Madagascar" },
+ { code: "MW", name: "Malawi" },
+ { code: "MY", name: "Malaysia" },
+ { code: "MV", name: "Maldives" },
+ { code: "ML", name: "Mali" },
+ { code: "MT", name: "Malta" },
+ { code: "MH", name: "Marshall Islands" },
+ { code: "MQ", name: "Martinique" },
+ { code: "MR", name: "Mauritania" },
+ { code: "MU", name: "Mauritius" },
+ { code: "YT", name: "Mayotte" },
+ { code: "MX", name: "Mexico" },
+ { code: "FM", name: "Micronesia, Federated States Of" },
+ { code: "MD", name: "Moldova" },
+ { code: "MC", name: "Monaco" },
+ { code: "MN", name: "Mongolia" },
+ { code: "ME", name: "Montenegro" },
+ { code: "MS", name: "Montserrat" },
+ { code: "MA", name: "Morocco" },
+ { code: "MZ", name: "Mozambique" },
+ { code: "MM", name: "Myanmar" },
+ { code: "NA", name: "Namibia" },
+ { code: "NR", name: "Nauru" },
+ { code: "NP", name: "Nepal" },
+ { code: "NL", name: "Netherlands" },
+ { code: "AN", name: "Netherlands Antilles" },
+ { code: "NC", name: "New Caledonia" },
+ { code: "NZ", name: "New Zealand" },
+ { code: "NI", name: "Nicaragua" },
+ { code: "NE", name: "Niger" },
+ { code: "NG", name: "Nigeria" },
+ { code: "NU", name: "Niue" },
+ { code: "NF", name: "Norfolk Island" },
+ { code: "MP", name: "Northern Mariana Islands" },
+ { code: "NO", name: "Norway" },
+ { code: "OM", name: "Oman" },
+ { code: "PK", name: "Pakistan" },
+ { code: "PW", name: "Palau" },
+ { code: "PS", name: "Palestinian Territory, Occupied" },
+ { code: "PA", name: "Panama" },
+ { code: "PG", name: "Papua New Guinea" },
+ { code: "PY", name: "Paraguay" },
+ { code: "PE", name: "Peru" },
+ { code: "PH", name: "Philippines" },
+ { code: "PN", name: "Pitcairn" },
+ { code: "PL", name: "Poland" },
+ { code: "PT", name: "Portugal" },
+ { code: "PR", name: "Puerto Rico" },
+ { code: "QA", name: "Qatar" },
+ { code: "RE", name: "Reunion" },
+ { code: "RO", name: "Romania" },
+ { code: "RU", name: "Russian Federation" },
+ { code: "RW", name: "Rwanda" },
+ { code: "BL", name: "Saint Barthelemy" },
+ { code: "SH", name: "Saint Helena" },
+ { code: "KN", name: "Saint Kitts And Nevis" },
+ { code: "LC", name: "Saint Lucia" },
+ { code: "MF", name: "Saint Martin" },
+ { code: "PM", name: "Saint Pierre And Miquelon" },
+ { code: "VC", name: "Saint Vincent And Grenadines" },
+ { code: "WS", name: "Samoa" },
+ { code: "SM", name: "San Marino" },
+ { code: "ST", name: "Sao Tome And Principe" },
+ { code: "SA", name: "Saudi Arabia" },
+ { code: "SN", name: "Senegal" },
+ { code: "RS", name: "Serbia" },
+ { code: "SC", name: "Seychelles" },
+ { code: "SL", name: "Sierra Leone" },
+ { code: "SG", name: "Singapore" },
+ { code: "SK", name: "Slovakia" },
+ { code: "SI", name: "Slovenia" },
+ { code: "SB", name: "Solomon Islands" },
+ { code: "SO", name: "Somalia" },
+ { code: "ZA", name: "South Africa" },
+ { code: "GS", name: "South Georgia And Sandwich Isl." },
+ { code: "ES", name: "Spain" },
+ { code: "LK", name: "Sri Lanka" },
+ { code: "SD", name: "Sudan" },
+ { code: "SR", name: "Suriname" },
+ { code: "SJ", name: "Svalbard And Jan Mayen" },
+ { code: "SZ", name: "Swaziland" },
+ { code: "SE", name: "Sweden" },
+ { code: "CH", name: "Switzerland" },
+ { code: "SY", name: "Syrian Arab Republic" },
+ { code: "TW", name: "Taiwan" },
+ { code: "TJ", name: "Tajikistan" },
+ { code: "TZ", name: "Tanzania" },
+ { code: "TH", name: "Thailand" },
+ { code: "TL", name: "Timor-Leste" },
+ { code: "TG", name: "Togo" },
+ { code: "TK", name: "Tokelau" },
+ { code: "TO", name: "Tonga" },
+ { code: "TT", name: "Trinidad And Tobago" },
+ { code: "TN", name: "Tunisia" },
+ { code: "TR", name: "Turkey" },
+ { code: "TM", name: "Turkmenistan" },
+ { code: "TC", name: "Turks And Caicos Islands" },
+ { code: "TV", name: "Tuvalu" },
+ { code: "UG", name: "Uganda" },
+ { code: "UA", name: "Ukraine" },
+ { code: "AE", name: "United Arab Emirates" },
+ { code: "GB", name: "United Kingdom" },
+ { code: "US", name: "United States" },
+ { code: "UM", name: "United States Outlying Islands" },
+ { code: "UY", name: "Uruguay" },
+ { code: "UZ", name: "Uzbekistan" },
+ { code: "VU", name: "Vanuatu" },
+ { code: "VE", name: "Venezuela" },
+ { code: "VN", name: "Vietnam" },
+ { code: "VG", name: "Virgin Islands, British" },
+ { code: "VI", name: "Virgin Islands, U.S." },
+ { code: "WF", name: "Wallis And Futuna" },
+ { code: "EH", name: "Western Sahara" },
+ { code: "YE", name: "Yemen" },
+ { code: "ZM", name: "Zambia" },
+ { code: "ZW", name: "Zimbabwe" },
+];
+
+// countries-zh.js
+export const ZH_COUNTRIES = [
+ { code: "AF", name: "阿富汗" },
+ { code: "AX", name: "奥兰群岛" },
+ { code: "AL", name: "阿尔巴尼亚" },
+ { code: "DZ", name: "阿尔及利亚" },
+ { code: "AS", name: "美属萨摩亚" },
+ { code: "AD", name: "安道尔" },
+ { code: "AO", name: "安哥拉" },
+ { code: "AI", name: "安圭拉" },
+ { code: "AQ", name: "南极洲" },
+ { code: "AG", name: "安提瓜和巴布达" },
+ { code: "AR", name: "阿根廷" },
+ { code: "AM", name: "亚美尼亚" },
+ { code: "AW", name: "阿鲁巴" },
+ { code: "AU", name: "澳大利亚" },
+ { code: "AT", name: "奥地利" },
+ { code: "AZ", name: "阿塞拜疆" },
+ { code: "BS", name: "巴哈马" },
+ { code: "BH", name: "巴林" },
+ { code: "BD", name: "孟加拉国" },
+ { code: "BB", name: "巴巴多斯" },
+ { code: "BY", name: "白俄罗斯" },
+ { code: "BE", name: "比利时" },
+ { code: "BZ", name: "伯利兹" },
+ { code: "BJ", name: "贝宁" },
+ { code: "BM", name: "百慕大" },
+ { code: "BT", name: "不丹" },
+ { code: "BO", name: "玻利维亚" },
+ { code: "BA", name: "波斯尼亚和黑塞哥维那" },
+ { code: "BW", name: "博茨瓦纳" },
+ { code: "BV", name: "布韦岛" },
+ { code: "BR", name: "巴西" },
+ { code: "IO", name: "英属印度洋领地" },
+ { code: "BN", name: "文莱" },
+ { code: "BG", name: "保加利亚" },
+ { code: "BF", name: "布基纳法索" },
+ { code: "BI", name: "布隆迪" },
+ { code: "KH", name: "柬埔寨" },
+ { code: "CM", name: "喀麦隆" },
+ { code: "CA", name: "加拿大" },
+ { code: "CV", name: "佛得角" },
+ { code: "KY", name: "开曼群岛" },
+ { code: "CF", name: "中非共和国" },
+ { code: "TD", name: "乍得" },
+ { code: "CL", name: "智利" },
+ { code: "CN", name: "中国" },
+ { code: "CX", name: "圣诞岛" },
+ { code: "CC", name: "科科斯(基林)群岛" },
+ { code: "CO", name: "哥伦比亚" },
+ { code: "KM", name: "科摩罗" },
+ { code: "CG", name: "刚果" },
+ { code: "CD", name: "刚果(金)" },
+ { code: "CK", name: "库克群岛" },
+ { code: "CR", name: "哥斯达黎加" },
+ { code: "CI", name: "科特迪瓦" },
+ { code: "HR", name: "克罗地亚" },
+ { code: "CU", name: "古巴" },
+ { code: "CY", name: "塞浦路斯" },
+ { code: "CZ", name: "捷克" },
+ { code: "DK", name: "丹麦" },
+ { code: "DJ", name: "吉布提" },
+ { code: "DM", name: "多米尼克" },
+ { code: "DO", name: "多米尼加共和国" },
+ { code: "EC", name: "厄瓜多尔" },
+ { code: "EG", name: "埃及" },
+ { code: "SV", name: "萨尔瓦多" },
+ { code: "GQ", name: "赤道几内亚" },
+ { code: "ER", name: "厄立特里亚" },
+ { code: "EE", name: "爱沙尼亚" },
+ { code: "ET", name: "埃塞俄比亚" },
+ { code: "FK", name: "福克兰群岛" },
+ { code: "FO", name: "法罗群岛" },
+ { code: "FJ", name: "斐济" },
+ { code: "FI", name: "芬兰" },
+ { code: "FR", name: "法国" },
+ { code: "GF", name: "法属圭亚那" },
+ { code: "PF", name: "法属波利尼西亚" },
+ { code: "TF", name: "法属南部领地" },
+ { code: "GA", name: "加蓬" },
+ { code: "GM", name: "冈比亚" },
+ { code: "GE", name: "格鲁吉亚" },
+ { code: "DE", name: "德国" },
+ { code: "GH", name: "加纳" },
+ { code: "GI", name: "直布罗陀" },
+ { code: "GR", name: "希腊" },
+ { code: "GL", name: "格陵兰" },
+ { code: "GD", name: "格林纳达" },
+ { code: "GP", name: "瓜德罗普" },
+ { code: "GU", name: "关岛" },
+ { code: "GT", name: "危地马拉" },
+ { code: "GG", name: "根西岛" },
+ { code: "GN", name: "几内亚" },
+ { code: "GW", name: "几内亚比绍" },
+ { code: "GY", name: "圭亚那" },
+ { code: "HT", name: "海地" },
+ { code: "HM", name: "赫德岛和麦克唐纳群岛" },
+ { code: "VA", name: "梵蒂冈" },
+ { code: "HN", name: "洪都拉斯" },
+ { code: "HK", name: "中国香港" },
+ { code: "HU", name: "匈牙利" },
+ { code: "IS", name: "冰岛" },
+ { code: "IN", name: "印度" },
+ { code: "ID", name: "印度尼西亚" },
+ { code: "IR", name: "伊朗" },
+ { code: "IQ", name: "伊拉克" },
+ { code: "IE", name: "爱尔兰" },
+ { code: "IM", name: "马恩岛" },
+ { code: "IL", name: "以色列" },
+ { code: "IT", name: "意大利" },
+ { code: "JM", name: "牙买加" },
+ { code: "JP", name: "日本" },
+ { code: "JE", name: "泽西岛" },
+ { code: "JO", name: "约旦" },
+ { code: "KZ", name: "哈萨克斯坦" },
+ { code: "KE", name: "肯尼亚" },
+ { code: "KI", name: "基里巴斯" },
+ { code: "KR", name: "韩国" },
+ { code: "KP", name: "朝鲜" },
+ { code: "KW", name: "科威特" },
+ { code: "KG", name: "吉尔吉斯斯坦" },
+ { code: "LA", name: "老挝" },
+ { code: "LV", name: "拉脱维亚" },
+ { code: "LB", name: "黎巴嫩" },
+ { code: "LS", name: "莱索托" },
+ { code: "LR", name: "利比里亚" },
+ { code: "LY", name: "利比亚" },
+ { code: "LI", name: "列支敦士登" },
+ { code: "LT", name: "立陶宛" },
+ { code: "LU", name: "卢森堡" },
+ { code: "MO", name: "中国澳门" },
+ { code: "MK", name: "北马其顿" },
+ { code: "MG", name: "马达加斯加" },
+ { code: "MW", name: "马拉维" },
+ { code: "MY", name: "马来西亚" },
+ { code: "MV", name: "马尔代夫" },
+ { code: "ML", name: "马里" },
+ { code: "MT", name: "马耳他" },
+ { code: "MH", name: "马绍尔群岛" },
+ { code: "MQ", name: "马提尼克" },
+ { code: "MR", name: "毛里塔尼亚" },
+ { code: "MU", name: "毛里求斯" },
+ { code: "YT", name: "马约特" },
+ { code: "MX", name: "墨西哥" },
+ { code: "FM", name: "密克罗尼西亚" },
+ { code: "MD", name: "摩尔多瓦" },
+ { code: "MC", name: "摩纳哥" },
+ { code: "MN", name: "蒙古" },
+ { code: "ME", name: "黑山" },
+ { code: "MS", name: "蒙特塞拉特" },
+ { code: "MA", name: "摩洛哥" },
+ { code: "MZ", name: "莫桑比克" },
+ { code: "MM", name: "缅甸" },
+ { code: "NA", name: "纳米比亚" },
+ { code: "NR", name: "瑙鲁" },
+ { code: "NP", name: "尼泊尔" },
+ { code: "NL", name: "荷兰" },
+ { code: "AN", name: "荷属安的列斯" },
+ { code: "NC", name: "新喀里多尼亚" },
+ { code: "NZ", name: "新西兰" },
+ { code: "NI", name: "尼加拉瓜" },
+ { code: "NE", name: "尼日尔" },
+ { code: "NG", name: "尼日利亚" },
+ { code: "NU", name: "纽埃" },
+ { code: "NF", name: "诺福克岛" },
+ { code: "MP", name: "北马里亚纳群岛" },
+ { code: "NO", name: "挪威" },
+ { code: "OM", name: "阿曼" },
+ { code: "PK", name: "巴基斯坦" },
+ { code: "PW", name: "帕劳" },
+ { code: "PS", name: "巴勒斯坦" },
+ { code: "PA", name: "巴拿马" },
+ { code: "PG", name: "巴布亚新几内亚" },
+ { code: "PY", name: "巴拉圭" },
+ { code: "PE", name: "秘鲁" },
+ { code: "PH", name: "菲律宾" },
+ { code: "PN", name: "皮特凯恩群岛" },
+ { code: "PL", name: "波兰" },
+ { code: "PT", name: "葡萄牙" },
+ { code: "PR", name: "波多黎各" },
+ { code: "QA", name: "卡塔尔" },
+ { code: "RE", name: "留尼汪" },
+ { code: "RO", name: "罗马尼亚" },
+ { code: "RU", name: "俄罗斯" },
+ { code: "RW", name: "卢旺达" },
+ { code: "BL", name: "圣巴泰勒米" },
+ { code: "SH", name: "圣赫勒拿" },
+ { code: "KN", name: "圣基茨和尼维斯" },
+ { code: "LC", name: "圣卢西亚" },
+ { code: "MF", name: "法属圣马丁" },
+ { code: "PM", name: "圣皮埃尔和密克隆" },
+ { code: "VC", name: "圣文森特和格林纳丁斯" },
+ { code: "WS", name: "萨摩亚" },
+ { code: "SM", name: "圣马力诺" },
+ { code: "ST", name: "圣多美和普林西比" },
+ { code: "SA", name: "沙特阿拉伯" },
+ { code: "SN", name: "塞内加尔" },
+ { code: "RS", name: "塞尔维亚" },
+ { code: "SC", name: "塞舌尔" },
+ { code: "SL", name: "塞拉利昂" },
+ { code: "SG", name: "新加坡" },
+ { code: "SK", name: "斯洛伐克" },
+ { code: "SI", name: "斯洛文尼亚" },
+ { code: "SB", name: "所罗门群岛" },
+ { code: "SO", name: "索马里" },
+ { code: "ZA", name: "南非" },
+ { code: "GS", name: "南乔治亚和南桑威奇群岛" },
+ { code: "ES", name: "西班牙" },
+ { code: "LK", name: "斯里兰卡" },
+ { code: "SD", name: "苏丹" },
+ { code: "SR", name: "苏里南" },
+ { code: "SJ", name: "斯瓦尔巴和扬马延" },
+ { code: "SZ", name: "斯威士兰" },
+ { code: "SE", name: "瑞典" },
+ { code: "CH", name: "瑞士" },
+ { code: "SY", name: "叙利亚" },
+ { code: "TW", name: "中国台湾" },
+ { code: "TJ", name: "塔吉克斯坦" },
+ { code: "TZ", name: "坦桑尼亚" },
+ { code: "TH", name: "泰国" },
+ { code: "TL", name: "东帝汶" },
+ { code: "TG", name: "多哥" },
+ { code: "TK", name: "托克劳" },
+ { code: "TO", name: "汤加" },
+ { code: "TT", name: "特立尼达和多巴哥" },
+ { code: "TN", name: "突尼斯" },
+ { code: "TR", name: "土耳其" },
+ { code: "TM", name: "土库曼斯坦" },
+ { code: "TC", name: "特克斯和凯科斯群岛" },
+ { code: "TV", name: "图瓦卢" },
+ { code: "UG", name: "乌干达" },
+ { code: "UA", name: "乌克兰" },
+ { code: "AE", name: "阿联酋" },
+ { code: "GB", name: "英国" },
+ { code: "US", name: "美国" },
+ { code: "UM", name: "美国本土外小岛屿" },
+ { code: "UY", name: "乌拉圭" },
+ { code: "UZ", name: "乌兹别克斯坦" },
+ { code: "VU", name: "瓦努阿图" },
+ { code: "VE", name: "委内瑞拉" },
+ { code: "VN", name: "越南" },
+ { code: "VG", name: "英属维尔京群岛" },
+ { code: "VI", name: "美属维尔京群岛" },
+ { code: "WF", name: "瓦利斯和富图纳" },
+ { code: "EH", name: "西撒哈拉" },
+ { code: "YE", name: "也门" },
+ { code: "ZM", name: "赞比亚" },
+ { code: "ZW", name: "津巴布韦" },
+];
diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts
index 6219148..b633647 100644
--- a/src/shared/lib/index.ts
+++ b/src/shared/lib/index.ts
@@ -1,2 +1,54 @@
export * from "./mui/theme";
export * from "./DecodeJWT";
+
+/**
+ * Генерирует название медиа по умолчанию в разных форматах
+ *
+ * Примеры использования:
+ * - Для достопримечательности с названием: "Михайловский замок_mikhail-zamok_Фото"
+ * - Для достопримечательности без названия: "Название_mikhail-zamok_Фото"
+ * - Для статьи: "Михайловский замок_mikhail-zamok_Факты" (где "Факты" - название статьи)
+ *
+ * @param objectName - Название объекта (достопримечательности, города и т.д.)
+ * @param fileName - Название файла
+ * @param mediaType - Тип медиа (число) или название статьи
+ * @param isArticle - Флаг, указывающий что медиа добавляется к статье
+ * @returns Строка в нужном формате
+ */
+export const generateDefaultMediaName = (
+ objectName: string,
+ fileName: string,
+ mediaType: number | string,
+ isArticle: boolean = false
+): string => {
+ // Убираем расширение из названия файла
+ const fileNameWithoutExtension = fileName.split(".").slice(0, -1).join(".");
+
+ if (isArticle && typeof mediaType === "string") {
+ // Для статей: "Название достопримечательности_название файла_название статьи"
+ return `${objectName}_${fileNameWithoutExtension}_${mediaType}`;
+ } else if (typeof mediaType === "number") {
+ // Получаем название типа медиа
+ const mediaTypeLabels: Record = {
+ 1: "Фото",
+ 2: "Видео",
+ 3: "Иконка",
+ 4: "Водяной знак",
+ 5: "Панорама",
+ 6: "3Д-модель",
+ };
+
+ const mediaTypeLabel = mediaTypeLabels[mediaType] || "Медиа";
+
+ if (objectName && objectName.trim() !== "") {
+ // Если есть название объекта: "Название объекта_название файла_тип медиа"
+ return `${objectName}_${fileNameWithoutExtension}_${mediaTypeLabel}`;
+ } else {
+ // Если нет названия объекта: "Название_название файла_тип медиа"
+ return `Название_${fileNameWithoutExtension}_${mediaTypeLabel}`;
+ }
+ }
+
+ // Fallback
+ return `${objectName || "Название"}_${fileNameWithoutExtension}_Медиа`;
+};
diff --git a/src/shared/modals/PreviewMediaDialog/index.tsx b/src/shared/modals/PreviewMediaDialog/index.tsx
index 2d4c80a..82fe7c2 100644
--- a/src/shared/modals/PreviewMediaDialog/index.tsx
+++ b/src/shared/modals/PreviewMediaDialog/index.tsx
@@ -75,6 +75,7 @@ export const PreviewMediaDialog = observer(
setError(err instanceof Error ? err.message : "Failed to save media");
} finally {
setIsLoading(false);
+ onClose();
}
};
@@ -96,7 +97,6 @@ export const PreviewMediaDialog = observer(
className="flex gap-4"
dividers
sx={{
- height: "600px",
display: "flex",
flexDirection: "column",
gap: 2,
@@ -149,6 +149,8 @@ export const PreviewMediaDialog = observer(
media_type: media.media_type,
filename: media.filename,
}}
+ className="h-full w-full object-contain"
+ fullHeight
/>
diff --git a/src/shared/modals/SelectArticleDialog/index.tsx b/src/shared/modals/SelectArticleDialog/index.tsx
index ba70cff..c4d7f5c 100644
--- a/src/shared/modals/SelectArticleDialog/index.tsx
+++ b/src/shared/modals/SelectArticleDialog/index.tsx
@@ -38,7 +38,9 @@ export const SelectArticleModal = observer(
onSelectArticle,
linkedArticleIds = [],
}: SelectArticleModalProps) => {
- const { articles, getArticle, getArticleMedia } = articlesStore;
+ const { language } = languageStore;
+ const { articles, getArticle, getArticleMedia, getArticles } =
+ articlesStore;
const [searchQuery, setSearchQuery] = useState("");
const [selectedArticleId, setSelectedArticleId] = useState(
null
@@ -54,6 +56,21 @@ export const SelectArticleModal = observer(
}
}, [open]);
+ useEffect(() => {
+ const fetchData = async () => {
+ await getArticles("ru");
+ await getArticles("en");
+ await getArticles("zh");
+ };
+ fetchData();
+ }, []);
+
+ useEffect(() => {
+ if (selectedArticleId) {
+ handleArticleClick(selectedArticleId);
+ }
+ }, [language]);
+
useEffect(() => {
const handleKeyPress = async (event: KeyboardEvent) => {
if (event.key.toLowerCase() === "enter") {
@@ -273,6 +290,25 @@ export const SelectArticleModal = observer(
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
+ cursor: "pointer",
+ "&:hover": {
+ textDecoration: "underline",
+ },
+ }}
+ onDoubleClick={async () => {
+ if (selectedArticleId) {
+ const media = await authInstance.get(
+ `/article/${selectedArticleId}/media`
+ );
+ onSelectArticle(
+ selectedArticleId,
+ articlesStore.articleData?.heading || "",
+ articlesStore.articleData?.body || "",
+ media.data || []
+ );
+ onClose();
+ setSelectedArticleId(null);
+ }
}}
>
{articlesStore.articleData?.heading || "Название cтатьи"}
diff --git a/src/shared/modals/UploadMediaDialog/index.tsx b/src/shared/modals/UploadMediaDialog/index.tsx
index 5d598ae..4dc4d47 100644
--- a/src/shared/modals/UploadMediaDialog/index.tsx
+++ b/src/shared/modals/UploadMediaDialog/index.tsx
@@ -1,4 +1,9 @@
-import { MEDIA_TYPE_LABELS, MEDIA_TYPE_VALUES, editSightStore } from "@shared";
+import {
+ MEDIA_TYPE_LABELS,
+ MEDIA_TYPE_VALUES,
+ editSightStore,
+ generateDefaultMediaName,
+} from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import {
@@ -32,6 +37,16 @@ interface UploadMediaDialogProps {
}) => void;
afterUploadSight?: (id: string) => void;
hardcodeType?: "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null;
+ contextObjectName?: string;
+ contextType?:
+ | "sight"
+ | "city"
+ | "carrier"
+ | "country"
+ | "vehicle"
+ | "station";
+ isArticle?: boolean;
+ articleName?: string;
}
export const UploadMediaDialog = observer(
@@ -41,6 +56,10 @@ export const UploadMediaDialog = observer(
afterUpload,
afterUploadSight,
hardcodeType,
+ contextObjectName,
+
+ isArticle,
+ articleName,
}: UploadMediaDialogProps) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
@@ -66,7 +85,7 @@ export const UploadMediaDialog = observer(
setAvailableMediaTypes([6]);
setMediaType(6);
}
- if (["jpg", "jpeg", "png", "gif"].includes(extension)) {
+ if (["jpg", "jpeg", "png", "gif", "svg"].includes(extension)) {
// Для изображений доступны все типы кроме видео
setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель
setMediaType(1); // По умолчанию Фото
@@ -76,8 +95,95 @@ export const UploadMediaDialog = observer(
setMediaType(2);
}
}
+
+ // Генерируем название по умолчанию если есть контекст
+ if (fileToUpload.name) {
+ let defaultName = "";
+
+ if (isArticle && articleName && contextObjectName) {
+ // Для статей: "Название достопримечательности_название файла_название статьи"
+ defaultName = generateDefaultMediaName(
+ contextObjectName,
+ fileToUpload.name,
+ articleName,
+ true
+ );
+ } else if (contextObjectName && contextObjectName.trim() !== "") {
+ // Для обычных медиа с названием объекта
+ const currentMediaType = hardcodeType
+ ? MEDIA_TYPE_VALUES[hardcodeType]
+ : 1; // По умолчанию фото
+ defaultName = generateDefaultMediaName(
+ contextObjectName,
+ fileToUpload.name,
+ currentMediaType,
+ false
+ );
+ } else {
+ // Для медиа без названия объекта
+ const currentMediaType = hardcodeType
+ ? MEDIA_TYPE_VALUES[hardcodeType]
+ : 1; // По умолчанию фото
+ defaultName = generateDefaultMediaName(
+ "",
+ fileToUpload.name,
+ currentMediaType,
+ false
+ );
+ }
+
+ setMediaName(defaultName);
+ }
}
- }, [fileToUpload]);
+ }, [fileToUpload, contextObjectName, hardcodeType, isArticle, articleName]);
+
+ // Обновляем название при изменении типа медиа
+ useEffect(() => {
+ if (mediaFilename && mediaType > 0) {
+ let defaultName = "";
+
+ if (isArticle && articleName && contextObjectName) {
+ // Для статей: "Название достопримечательности_название файла_название статьи"
+ defaultName = generateDefaultMediaName(
+ contextObjectName,
+ mediaFilename,
+ articleName,
+ true
+ );
+ } else if (contextObjectName && contextObjectName.trim() !== "") {
+ // Для обычных медиа с названием объекта
+ const currentMediaType = hardcodeType
+ ? MEDIA_TYPE_VALUES[hardcodeType]
+ : mediaType;
+ defaultName = generateDefaultMediaName(
+ contextObjectName,
+ mediaFilename,
+ currentMediaType,
+ false
+ );
+ } else {
+ // Для медиа без названия объекта
+ const currentMediaType = hardcodeType
+ ? MEDIA_TYPE_VALUES[hardcodeType]
+ : mediaType;
+ defaultName = generateDefaultMediaName(
+ "",
+ mediaFilename,
+ currentMediaType,
+ false
+ );
+ }
+
+ setMediaName(defaultName);
+ }
+ }, [
+ mediaType,
+ contextObjectName,
+ mediaFilename,
+ hardcodeType,
+ isArticle,
+ articleName,
+ ]);
useEffect(() => {
if (mediaFile) {
@@ -141,7 +247,6 @@ export const UploadMediaDialog = observer(
className="flex gap-4"
dividers
sx={{
- height: "600px",
display: "flex",
flexDirection: "column",
gap: 2,
@@ -200,13 +305,16 @@ export const UploadMediaDialog = observer(
height: "100%",
}}
>
- {/* */}
+ {mediaType == 2 && mediaUrl && (
+
+ )}
{mediaType === 6 && mediaUrl && (
)}
@@ -215,8 +323,7 @@ export const UploadMediaDialog = observer(
src={mediaUrl ?? ""}
alt="Uploaded media"
style={{
- maxWidth: "100%",
- maxHeight: "100%",
+ height: "100%",
objectFit: "contain",
}}
/>
diff --git a/src/shared/store/ArticlesStore/index.tsx b/src/shared/store/ArticlesStore/index.tsx
index 5fb66eb..c5a69d3 100644
--- a/src/shared/store/ArticlesStore/index.tsx
+++ b/src/shared/store/ArticlesStore/index.tsx
@@ -109,7 +109,8 @@ class ArticlesStore {
getArticles = async (language: Language) => {
this.articleLoading = true;
- const response = await authInstance.get("/article");
+
+ const response = await languageInstance(language).get("/article");
runInAction(() => {
this.articles[language] = response.data;
@@ -119,8 +120,9 @@ class ArticlesStore {
getArticle = async (id: number, language?: Language) => {
this.articleLoading = true;
+ let response: any;
if (language) {
- const response = await languageInstance(language).get(`/article/${id}`);
+ response = await languageInstance(language).get(`/article/${id}`);
runInAction(() => {
if (!this.articleData) {
this.articleData = { id, heading: "", body: "", service_name: "" };
@@ -131,11 +133,12 @@ class ArticlesStore {
};
});
} else {
- const response = await authInstance.get(`/article/${id}`);
+ response = await authInstance.get(`/article/${id}`);
runInAction(() => {
this.articleData = response.data;
});
}
+ return response;
this.articleLoading = false;
};
diff --git a/src/shared/store/AuthStore/index.tsx b/src/shared/store/AuthStore/index.tsx
index e317155..21cbd0f 100644
--- a/src/shared/store/AuthStore/index.tsx
+++ b/src/shared/store/AuthStore/index.tsx
@@ -55,7 +55,11 @@ class AuthStore {
runInAction(() => {
this.setAuthToken(data.token);
- this.payload = response.data;
+ this.payload = {
+ ...response.data.user,
+ // @ts-ignore
+ user_id: response.data.user.id,
+ };
this.error = null;
});
} catch (error) {
diff --git a/src/shared/store/CarrierStore/index.tsx b/src/shared/store/CarrierStore/index.tsx
index a3615ed..273a3c4 100644
--- a/src/shared/store/CarrierStore/index.tsx
+++ b/src/shared/store/CarrierStore/index.tsx
@@ -187,7 +187,7 @@ class CarrierStore {
: {}),
};
- await languageInstance(lang as Language).patch(
+ const response = await languageInstance(lang as Language).patch(
`/carrier/${carrierId}`,
patchPayload
);
@@ -196,6 +196,26 @@ class CarrierStore {
this.carriers[lang as keyof Carriers].data.push(response.data);
});
}
+
+ this.createCarrierData = {
+ city_id: 0,
+ logo: "",
+ ru: {
+ full_name: "",
+ short_name: "",
+ slogan: "",
+ },
+ en: {
+ full_name: "",
+ short_name: "",
+ slogan: "",
+ },
+ zh: {
+ full_name: "",
+ short_name: "",
+ slogan: "",
+ },
+ };
};
editCarrierData = {
@@ -265,7 +285,9 @@ class CarrierStore {
...this.editCarrierData[lang],
city: cityName,
city_id: this.editCarrierData.city_id,
- logo: this.editCarrierData.logo,
+ ...(this.editCarrierData.logo
+ ? { logo: this.editCarrierData.logo }
+ : {}),
});
runInAction(() => {
diff --git a/src/shared/store/CityStore/index.ts b/src/shared/store/CityStore/index.ts
index f12db9f..6cbcce6 100644
--- a/src/shared/store/CityStore/index.ts
+++ b/src/shared/store/CityStore/index.ts
@@ -284,41 +284,39 @@ class CityStore {
(country) => country.code === country_code
);
- if (name) {
- await languageInstance(language as Language).patch(`/city/${code}`, {
- name,
- country: country?.name || "",
- country_code: country_code,
- arms,
- });
+ await languageInstance(language as Language).patch(`/city/${code}`, {
+ name,
+ country: country?.name || "",
+ country_code: country_code,
+ arms,
+ });
- runInAction(() => {
- if (this.city[code]) {
- this.city[code][language as keyof CashedCities] = {
- name,
- country: country?.name || "",
- country_code: country_code,
- arms,
- };
- }
+ runInAction(() => {
+ if (this.city[code]) {
+ this.city[code][language as keyof CashedCities] = {
+ name,
+ country: country?.name || "",
+ country_code: country_code,
+ arms,
+ };
+ }
- if (this.cities[language as keyof CashedCities]) {
- this.cities[language as keyof CashedCities].data = this.cities[
- language as keyof CashedCities
- ].data.map((city) =>
- city.id === Number(code)
- ? {
- id: city.id,
- name,
- country: country?.name || "",
- country_code: country_code,
- arms,
- }
- : city
- );
- }
- });
- }
+ if (this.cities[language as keyof CashedCities]) {
+ this.cities[language as keyof CashedCities].data = this.cities[
+ language as keyof CashedCities
+ ].data.map((city) =>
+ city.id === Number(code)
+ ? {
+ id: city.id,
+ name,
+ country: country?.name || "",
+ country_code: country_code,
+ arms,
+ }
+ : city
+ );
+ }
+ });
}
};
}
diff --git a/src/shared/store/CountryStore/index.ts b/src/shared/store/CountryStore/index.ts
index df04a0e..f130949 100644
--- a/src/shared/store/CountryStore/index.ts
+++ b/src/shared/store/CountryStore/index.ts
@@ -68,14 +68,7 @@ class CountryStore {
};
getCountry = async (code: string, language: keyof CashedCountries) => {
- if (
- this.country[code]?.[language] &&
- this.country[code][language] !== null
- ) {
- return;
- }
-
- const response = await authInstance.get(`/country/${code}`);
+ const response = await languageInstance(language).get(`/country/${code}`);
runInAction(() => {
if (!this.country[code]) {
diff --git a/src/shared/store/CreateSightStore/index.tsx b/src/shared/store/CreateSightStore/index.tsx
index 42cdd7d..c7d46f3 100644
--- a/src/shared/store/CreateSightStore/index.tsx
+++ b/src/shared/store/CreateSightStore/index.tsx
@@ -1,5 +1,11 @@
// @shared/stores/createSightStore.ts
-import { Language, authInstance, languageInstance, mediaStore } from "@shared";
+import {
+ articlesStore,
+ Language,
+ authInstance,
+ languageInstance,
+ mediaStore,
+} from "@shared";
import { makeAutoObservable, runInAction } from "mobx";
type MediaItem = {
@@ -162,6 +168,8 @@ class CreateSightStore {
media: mediaData,
});
});
+
+ return articleId; // Return the linked article ID
} catch (error) {
console.error("Error linking existing right article:", error);
throw error;
@@ -315,7 +323,18 @@ class CreateSightStore {
deleteLeftArticle = async (articleId: number) => {
/* ... your existing logic ... */
await authInstance.delete(`/article/${articleId}`);
- // articlesStore.getArticles(languageStore.language); // If still needed
+ // articlesStore.getArticles(languageStore.language); // If still neede
+ runInAction(() => {
+ articlesStore.articles.ru = articlesStore.articles.ru.filter(
+ (article) => article.id !== articleId
+ );
+ articlesStore.articles.en = articlesStore.articles.en.filter(
+ (article) => article.id !== articleId
+ );
+ articlesStore.articles.zh = articlesStore.articles.zh.filter(
+ (article) => article.id !== articleId
+ );
+ });
this.unlinkLeftArticle();
};
@@ -352,6 +371,25 @@ class CreateSightStore {
body: "填写内容",
media: [],
};
+
+ articlesStore.articles.ru.push({
+ id: newLeftArticleId,
+ heading: "Новая левая статья",
+ body: "Заполните контентом",
+ service_name: "Новая левая статья",
+ });
+ articlesStore.articles.en.push({
+ id: newLeftArticleId,
+ heading: "New Left Article",
+ body: "Fill with content",
+ service_name: "New Left Article",
+ });
+ articlesStore.articles.zh.push({
+ id: newLeftArticleId,
+ heading: "新的左侧文章",
+ body: "填写内容",
+ service_name: "新的左侧文章",
+ });
});
return newLeftArticleId;
};
diff --git a/src/shared/store/EditSightStore/index.tsx b/src/shared/store/EditSightStore/index.tsx
index 263b907..cf3a677 100644
--- a/src/shared/store/EditSightStore/index.tsx
+++ b/src/shared/store/EditSightStore/index.tsx
@@ -1,5 +1,11 @@
// @shared/stores/editSightStore.ts
-import { authInstance, Language, languageInstance, mediaStore } from "@shared";
+import {
+ articlesStore,
+ authInstance,
+ Language,
+ languageInstance,
+ mediaStore,
+} from "@shared";
import { makeAutoObservable, runInAction } from "mobx";
export type SightLanguageInfo = {
@@ -82,11 +88,7 @@ class EditSightStore {
hasLoadedCommon = false;
getSightInfo = async (id: number, language: Language) => {
- if (this.sight[language].id === id) {
- return;
- }
-
- const response = await authInstance.get(`/sight/${id}`);
+ const response = await languageInstance(language).get(`/sight/${id}`);
const data = response.data;
if (data.left_article != 0 && data.left_article != null) {
@@ -376,6 +378,18 @@ class EditSightStore {
deleteLeftArticle = async (id: number) => {
await authInstance.delete(`/article/${id}`);
+
+ runInAction(() => {
+ articlesStore.articles.ru = articlesStore.articles.ru.filter(
+ (article) => article.id !== id
+ );
+ articlesStore.articles.en = articlesStore.articles.en.filter(
+ (article) => article.id !== id
+ );
+ articlesStore.articles.zh = articlesStore.articles.zh.filter(
+ (article) => article.id !== id
+ );
+ });
this.sight.common.left_article = 0;
this.sight.ru.left.heading = "";
this.sight.en.left.heading = "";
@@ -556,6 +570,8 @@ class EditSightStore {
media: mediaIds.data,
});
});
+
+ return article_id; // Return the linked article ID
};
deleteRightArticleMedia = async (article_id: number, media_id: string) => {
@@ -639,6 +655,29 @@ class EditSightStore {
body: articleZhData.body,
media: [],
});
+
+ runInAction(() => {
+ articlesStore.articles.ru.push({
+ id: id,
+ heading: articleRuData.heading,
+ body: articleRuData.body,
+ service_name: articleRuData.heading,
+ });
+ articlesStore.articles.en.push({
+ id: id,
+ heading: articleEnData.heading,
+ body: articleEnData.body,
+ service_name: articleEnData.heading,
+ });
+ articlesStore.articles.zh.push({
+ id: id,
+ heading: articleZhData.heading,
+ body: articleZhData.body,
+ service_name: articleZhData.heading,
+ });
+ });
+
+ return id; // Return the ID of the newly created article
};
createLinkWithRightArticle = async (
diff --git a/src/shared/store/RouteStore/index.ts b/src/shared/store/RouteStore/index.ts
index 8688d04..c00d305 100644
--- a/src/shared/store/RouteStore/index.ts
+++ b/src/shared/store/RouteStore/index.ts
@@ -66,17 +66,41 @@ class RouteStore {
});
};
+ routeStations: Record = {};
+
getRoute = async (id: number) => {
if (this.route[id]) return this.route[id];
const response = await authInstance.get(`/route/${id}`);
+ const stations = await authInstance.get(`/route/${id}/station`);
runInAction(() => {
this.route[id] = response.data;
+ this.routeStations[id] = stations.data;
});
return response.data;
};
+ setRouteStations = (routeId: number, stationId: number, data: any) => {
+ console.log(
+ this.routeStations[routeId],
+ stationId,
+ this.routeStations[routeId].find((station) => station.id === stationId)
+ );
+ this.routeStations[routeId] = this.routeStations[routeId]?.map((station) =>
+ station.id === stationId ? { ...station, ...data } : station
+ );
+ };
+
+ saveRouteStations = async (routeId: number, stationId: number) => {
+ await authInstance.patch(`/route/${routeId}/station`, {
+ ...this.routeStations[routeId]?.find(
+ (station) => station.id === stationId
+ ),
+ station_id: stationId,
+ });
+ };
+
editRouteData = {
carrier: "",
carrier_id: 0,
@@ -112,6 +136,21 @@ class RouteStore {
);
});
};
+
+ copyRouteAction = async (id: number) => {
+ const response = await authInstance.post(`/route/${id}/copy`);
+ console.log(response);
+
+ runInAction(() => {
+ this.routes.data = [...this.routes.data, response.data];
+ });
+ };
+
+ selectedStationId = 0;
+
+ setSelectedStationId = (id: number) => {
+ this.selectedStationId = id;
+ };
}
export const routeStore = new RouteStore();
diff --git a/src/shared/store/StationsStore/index.ts b/src/shared/store/StationsStore/index.ts
index f01d63f..8ad2778 100644
--- a/src/shared/store/StationsStore/index.ts
+++ b/src/shared/store/StationsStore/index.ts
@@ -6,7 +6,6 @@ type Language = "ru" | "en" | "zh";
type StationLanguageData = {
name: string;
system_name: string;
- description: string;
address: string;
loaded: boolean; // Indicates if this language's data has been loaded/modified
};
@@ -14,6 +13,7 @@ type StationLanguageData = {
type StationCommonData = {
city_id: number;
direction: boolean;
+ description: string;
icon: string;
latitude: number;
longitude: number;
@@ -102,21 +102,21 @@ class StationsStore {
ru: {
name: "",
system_name: "",
- description: "",
+
address: "",
loaded: false,
},
en: {
name: "",
system_name: "",
- description: "",
+
address: "",
loaded: false,
},
zh: {
name: "",
system_name: "",
- description: "",
+
address: "",
loaded: false,
},
@@ -124,6 +124,7 @@ class StationsStore {
city: "",
city_id: 0,
direction: false,
+ description: "",
icon: "",
latitude: 0,
longitude: 0,
@@ -147,21 +148,21 @@ class StationsStore {
ru: {
name: "",
system_name: "",
- description: "",
+
address: "",
loaded: false,
},
en: {
name: "",
system_name: "",
- description: "",
+
address: "",
loaded: false,
},
zh: {
name: "",
system_name: "",
- description: "",
+
address: "",
loaded: false,
},
@@ -169,6 +170,7 @@ class StationsStore {
city: "",
city_id: 0,
direction: false,
+ description: "",
icon: "",
latitude: 0,
longitude: 0,
@@ -229,21 +231,21 @@ class StationsStore {
ru: {
name: ruResponse.data.name,
system_name: ruResponse.data.system_name,
- description: ruResponse.data.description,
+
address: ruResponse.data.address,
loaded: true,
},
en: {
name: enResponse.data.name,
system_name: enResponse.data.system_name,
- description: enResponse.data.description,
+
address: enResponse.data.address,
loaded: true,
},
zh: {
name: zhResponse.data.name,
system_name: zhResponse.data.system_name,
- description: zhResponse.data.description,
+
address: zhResponse.data.address,
loaded: true,
},
@@ -251,6 +253,7 @@ class StationsStore {
city: ruResponse.data.city,
city_id: ruResponse.data.city_id,
direction: ruResponse.data.direction,
+ description: ruResponse.data.description,
icon: ruResponse.data.icon,
latitude: ruResponse.data.latitude,
longitude: ruResponse.data.longitude,
@@ -286,7 +289,8 @@ class StationsStore {
};
for (const language of ["ru", "en", "zh"] as const) {
- const { name, description, address } = this.editStationData[language];
+ const { name, address } = this.editStationData[language];
+ const description = this.editStationData.common.description;
const response = await languageInstance(language).patch(
`/station/${id}`,
{
@@ -323,7 +327,7 @@ class StationsStore {
...station,
name: response.data.name,
system_name: response.data.system_name,
- description: response.data.description,
+ description: description || "",
address: response.data.address,
...commonDataPayload,
} as Station)
@@ -418,7 +422,8 @@ class StationsStore {
}
// First create station in Russian
- const { name, description, address } = this.createStationData[language];
+ const { name, address } = this.createStationData[language];
+ const description = this.createStationData.common.description;
const response = await languageInstance(language).post("/station", {
name: name || "",
system_name: name || "", // system_name is often derived from name
@@ -437,7 +442,8 @@ class StationsStore {
for (const lang of ["ru", "en", "zh"].filter(
(lang) => lang !== language
) as Language[]) {
- const { name, description, address } = this.createStationData[lang];
+ const { name, address } = this.createStationData[lang];
+ const description = this.createStationData.common.description;
const response = await languageInstance(lang).patch(
`/station/${stationId}`,
{
@@ -459,21 +465,18 @@ class StationsStore {
ru: {
name: "",
system_name: "",
- description: "",
address: "",
loaded: false,
},
en: {
name: "",
system_name: "",
- description: "",
address: "",
loaded: false,
},
zh: {
name: "",
system_name: "",
- description: "",
address: "",
loaded: false,
},
@@ -483,6 +486,7 @@ class StationsStore {
direction: false,
icon: "",
latitude: 0,
+ description: "",
longitude: 0,
offset_x: 0,
offset_y: 0,
@@ -509,21 +513,18 @@ class StationsStore {
ru: {
name: "",
system_name: "",
- description: "",
address: "",
loaded: false,
},
en: {
name: "",
system_name: "",
- description: "",
address: "",
loaded: false,
},
zh: {
name: "",
system_name: "",
- description: "",
address: "",
loaded: false,
},
@@ -531,6 +532,7 @@ class StationsStore {
city: "",
city_id: 0,
direction: false,
+ description: "",
icon: "",
latitude: 0,
longitude: 0,
diff --git a/src/shared/store/UserStore/index.ts b/src/shared/store/UserStore/index.ts
index 25bd278..8a2da6d 100644
--- a/src/shared/store/UserStore/index.ts
+++ b/src/shared/store/UserStore/index.ts
@@ -24,8 +24,6 @@ class UserStore {
}
getUsers = async () => {
- if (this.users.loaded) return;
-
const response = await authInstance.get("/user");
runInAction(() => {
@@ -35,7 +33,7 @@ class UserStore {
};
getUser = async (id: number) => {
- if (this.user[id]) return;
+ if (this.user[id]) return this.user[id];
const response = await authInstance.get(`/user/${id}`);
runInAction(() => {
diff --git a/src/shared/store/VehicleStore/index.ts b/src/shared/store/VehicleStore/index.ts
index 014c2dd..3c9993e 100644
--- a/src/shared/store/VehicleStore/index.ts
+++ b/src/shared/store/VehicleStore/index.ts
@@ -35,8 +35,6 @@ class VehicleStore {
}
getVehicles = async () => {
- if (this.vehicles.loaded) return;
-
const response = await languageInstance("ru").get(`/vehicle`);
runInAction(() => {
diff --git a/src/widgets/DeleteModal/index.tsx b/src/widgets/DeleteModal/index.tsx
index ef335f8..c7c1d4c 100644
--- a/src/widgets/DeleteModal/index.tsx
+++ b/src/widgets/DeleteModal/index.tsx
@@ -3,21 +3,25 @@ import { Button } from "@mui/material";
export const DeleteModal = ({
onDelete,
onCancel,
+ edit = false,
open,
}: {
onDelete: () => void;
onCancel: () => void;
+ edit?: boolean;
open: boolean;
}) => {
return (
- Вы уверены, что хотите удалить этот элемент?
+ {`Вы уверены, что хотите ${
+ edit ? "убрать" : "удалить"
+ } этот элемент?`}
diff --git a/src/widgets/DevicesTable/index.tsx b/src/widgets/DevicesTable/index.tsx
index 8388dcd..fb7c445 100644
--- a/src/widgets/DevicesTable/index.tsx
+++ b/src/widgets/DevicesTable/index.tsx
@@ -18,6 +18,7 @@ import { observer } from "mobx-react-lite";
import { Button, Checkbox, Typography } from "@mui/material";
import { Vehicle } from "@shared";
import { toast } from "react-toastify";
+import { useNavigate } from "react-router-dom";
export type ConnectedDevice = string;
@@ -116,6 +117,7 @@ export const DevicesTable = observer(() => {
const { snapshots, getSnapshots } = snapshotStore;
const { getVehicles, vehicles } = vehicleStore; // Removed as devicesStore.devices should be the source of truth
const { devices } = devicesStore;
+ const navigate = useNavigate();
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState([]);
// Transform the raw devices data into rows suitable for the table
@@ -182,13 +184,25 @@ export const DevicesTable = observer(() => {
const handleSendSnapshotAction = async (snapshotId: string) => {
if (selectedDeviceUuids.length === 0) return;
+ const send = async (deviceUuid: string) => {
+ try {
+ await authInstance.post(
+ `/devices/${deviceUuid}/force-snapshot-update`,
+ {
+ snapshot_id: snapshotId,
+ }
+ );
+ toast.success(`Снапшот отправлен на устройство `);
+ } catch (error) {
+ console.error(`Error sending snapshot to device ${deviceUuid}:`, error);
+ toast.error(`Не удалось отправить снапшот на устройство`);
+ }
+ };
try {
// Create an array of promises for all snapshot requests
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`);
- return authInstance.post(`/devices/${deviceUuid}/force-snapshot`, {
- snapshot_id: snapshotId,
- });
+ return send(deviceUuid);
});
// Wait for all promises to settle (either resolve or reject)
@@ -209,6 +223,16 @@ export const DevicesTable = observer(() => {
return (
<>
+
+ navigate("/vehicle/create")}
+ >
+ Добавить устройство
+
+
{
'input[type="checkbox"]'
) === null
) {
- handleSelectDevice(
- {
- target: {
- checked: !selectedDeviceUuids.includes(
- row.device_uuid ?? ""
- ),
- },
- } as React.ChangeEvent, // Simulate event
- row.device_uuid ?? ""
- );
+ if (event.shiftKey) {
+ if (row.device_uuid) {
+ navigator.clipboard
+ .writeText(row.device_uuid)
+ .then(() => {
+ toast.success(`UUID скопирован`);
+ })
+ .catch(() => {
+ toast.error("Не удалось скопировать UUID");
+ });
+ } else {
+ toast.warning("Устройство не имеет UUID");
+ }
+ }
+
+ // Only toggle checkbox if Shift key is not pressed
+ if (!event.shiftKey) {
+ handleSelectDevice(
+ {
+ target: {
+ checked: !selectedDeviceUuids.includes(
+ row.device_uuid ?? ""
+ ),
+ },
+ } as React.ChangeEvent, // Simulate event
+ row.device_uuid ?? ""
+ );
+ }
}
}}
sx={{
diff --git a/src/widgets/ImageUploadCard/index.tsx b/src/widgets/ImageUploadCard/index.tsx
index d75d8ee..087d265 100644
--- a/src/widgets/ImageUploadCard/index.tsx
+++ b/src/widgets/ImageUploadCard/index.tsx
@@ -46,10 +46,16 @@ export const ImageUploadCard: React.FC = ({
) => {
const file = event.target.files?.[0];
if (file) {
- setFileToUpload(file);
- setUploadMediaOpen(true);
- if (imageKey && setHardcodeType) {
- setHardcodeType(imageKey);
+ if (file.type.startsWith("image/") && file.type !== "image/gif") {
+ setFileToUpload(file);
+ setUploadMediaOpen(true);
+ if (imageKey && setHardcodeType) {
+ setHardcodeType(imageKey);
+ }
+ } else if (file.type === "image/gif") {
+ toast.error("GIF файлы не поддерживаются");
+ } else {
+ toast.error("Пожалуйста, выберите изображение");
}
}
// Reset the input value so selecting the same file again triggers change
@@ -78,9 +84,11 @@ export const ImageUploadCard: React.FC = ({
const files = event.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
- if (file.type.startsWith("image/")) {
+ if (file.type.startsWith("image/") && file.type !== "image/gif") {
setFileToUpload(file);
setUploadMediaOpen(true);
+ } else if (file.type === "image/gif") {
+ toast.error("GIF файлы не поддерживаются");
} else {
toast.error("Пожалуйста, выберите изображение");
}
diff --git a/src/widgets/LanguageSwitcher/index.tsx b/src/widgets/LanguageSwitcher/index.tsx
index ecce76d..d0c2f98 100644
--- a/src/widgets/LanguageSwitcher/index.tsx
+++ b/src/widgets/LanguageSwitcher/index.tsx
@@ -44,14 +44,14 @@ export const LanguageSwitcher = observer(() => {
};
return (
-
+
{/* Added some styling for better visualization */}
{LANGUAGES.map((lang) => (
handleLanguageChange(lang)}
- variant={language === lang ? "contained" : "outlined"} // Highlight the active language
- color="primary"
+ variant={"contained"} // Highlight the active language
+ color={language === lang ? "primary" : "secondary"}
sx={{ minWidth: "60px" }} // Give buttons a consistent width
>
{getLanguageLabel(lang)}
diff --git a/src/widgets/Layout/index.tsx b/src/widgets/Layout/index.tsx
index eae9c68..0f218f3 100644
--- a/src/widgets/Layout/index.tsx
+++ b/src/widgets/Layout/index.tsx
@@ -2,20 +2,32 @@ import * as React from "react";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
-import { Menu, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+import { Menu, ChevronLeftIcon, ChevronRightIcon, User } from "lucide-react";
import { useTheme } from "@mui/material/styles";
import { AppBar } from "./ui/AppBar";
import { Drawer } from "./ui/Drawer";
import { DrawerHeader } from "./ui/DrawerHeader";
import { NavigationList } from "@features";
+import { authStore, userStore } from "@shared";
+import { observer } from "mobx-react-lite";
+import { useEffect } from "react";
+import { Typography } from "@mui/material";
interface LayoutProps {
children: React.ReactNode;
}
-export const Layout: React.FC = ({ children }) => {
+export const Layout: React.FC = observer(({ children }) => {
const theme = useTheme();
- const [open, setOpen] = React.useState(false);
+ const [open, setOpen] = React.useState(true);
+ const { getUsers, users } = userStore;
+
+ useEffect(() => {
+ const fetchUsers = async () => {
+ await getUsers();
+ };
+ fetchUsers();
+ }, []);
const handleDrawerOpen = () => {
setOpen(true);
@@ -28,7 +40,7 @@ export const Layout: React.FC = ({ children }) => {
return (
-
+
= ({ children }) => {
>
+
+
+
+ {(() => {
+ console.log(authStore.payload);
+ return (
+ <>
+
+ {
+ users?.data?.find(
+ // @ts-ignore
+ (user) => user.id === authStore.payload?.user_id
+ )?.name
+ }
+
+
+ {/* @ts-ignore */}
+ {authStore.payload?.is_admin
+ ? "Администратор"
+ : "Режим пользователя"}
+
+ >
+ );
+ })()}
+
+
+
+
+
-
- {theme.direction === "rtl" ? (
-
- ) : (
-
- )}
-
+ {
+ setOpen(!open);
+ }}
+ >
+
+
+ Белые ночи
+
+
+ {open && (
+
+ {theme.direction === "rtl" ? (
+
+ ) : (
+
+ )}
+
+ )}
@@ -67,10 +140,9 @@ export const Layout: React.FC = ({ children }) => {
maxWidth: "100vw",
}}
>
-
-
+
{children}
);
-};
+});
diff --git a/src/widgets/Layout/ui/AppBar.tsx b/src/widgets/Layout/ui/AppBar.tsx
index b653a5e..441daea 100644
--- a/src/widgets/Layout/ui/AppBar.tsx
+++ b/src/widgets/Layout/ui/AppBar.tsx
@@ -24,4 +24,12 @@ export const AppBar = styled(MuiAppBar, {
duration: theme.transitions.duration.enteringScreen,
}),
}),
+ ...(!open && {
+ marginLeft: theme.spacing(7),
+ width: `calc(100% - ${theme.spacing(7)})`,
+ [theme.breakpoints.up("sm")]: {
+ marginLeft: theme.spacing(8),
+ width: `calc(100% - ${theme.spacing(8)})`,
+ },
+ }),
}));
diff --git a/src/widgets/Layout/ui/DrawerHeader.tsx b/src/widgets/Layout/ui/DrawerHeader.tsx
index 11151dc..b3cc221 100644
--- a/src/widgets/Layout/ui/DrawerHeader.tsx
+++ b/src/widgets/Layout/ui/DrawerHeader.tsx
@@ -1,10 +1,12 @@
import { styled } from "@mui/material/styles";
import type { Theme } from "@mui/material/styles";
+import Box from "@mui/material/Box";
-export const DrawerHeader = styled("div")(({ theme }: { theme: Theme }) => ({
+export const DrawerHeader = styled(Box)(({ theme }: { theme: Theme }) => ({
display: "flex",
alignItems: "center",
- justifyContent: "flex-end",
- padding: theme.spacing(0, 1),
+ justifyContent: "space-between",
+ padding: theme.spacing(2),
...theme.mixins.toolbar,
+ borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
}));
diff --git a/src/widgets/MediaAreaForSight/index.tsx b/src/widgets/MediaAreaForSight/index.tsx
index 8449fb3..0c60376 100644
--- a/src/widgets/MediaAreaForSight/index.tsx
+++ b/src/widgets/MediaAreaForSight/index.tsx
@@ -8,9 +8,23 @@ export const MediaAreaForSight = observer(
({
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);
@@ -94,6 +108,10 @@ export const MediaAreaForSight = observer(
setUploadMediaDialogOpen(false)}
+ contextObjectName={contextObjectName}
+ contextType={contextType}
+ isArticle={isArticle}
+ articleName={articleName}
afterUploadSight={onFinishUpload}
/>
)}
diff --git a/src/widgets/ReactMarkdown/index.tsx b/src/widgets/ReactMarkdown/index.tsx
index 3ec4b84..76e7d1b 100644
--- a/src/widgets/ReactMarkdown/index.tsx
+++ b/src/widgets/ReactMarkdown/index.tsx
@@ -5,7 +5,7 @@ import rehypeRaw from "rehype-raw";
export const ReactMarkdownComponent = ({ value }: { value: string }) => {
return (
({
maxHeight: "500px",
overflowY: "auto",
overflowX: "hidden",
+ "&::-webkit-scrollbar": {
+ display: "none",
+ },
+ "&": {
+ scrollbarWidth: "none",
+ },
wordBreak: "break-word", // ✅ добавлено
},
"& .CodeMirror-selected": {
diff --git a/src/widgets/SightTabs/CreateInformationTab/index.tsx b/src/widgets/SightTabs/CreateInformationTab/index.tsx
index 950ec02..96bcf1e 100644
--- a/src/widgets/SightTabs/CreateInformationTab/index.tsx
+++ b/src/widgets/SightTabs/CreateInformationTab/index.tsx
@@ -31,7 +31,7 @@ import { toast } from "react-toastify";
export const CreateInformationTab = observer(
({ value, index }: { value: number; index: number }) => {
- const { ruCities } = cityStore;
+ const { cities } = cityStore;
const [mediaId, setMediaId] = useState("");
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
@@ -175,10 +175,11 @@ export const CreateInformationTab = observer(
/>
city.id === sight.city_id) ??
- null
+ cities["ru"]?.data?.find(
+ (city) => city.id === sight.city_id
+ ) ?? null
}
getOptionLabel={(option) => option.name}
onChange={(_, value) => {
@@ -246,7 +247,7 @@ export const CreateInformationTab = observer(
}}
>
{
@@ -266,11 +267,10 @@ export const CreateInformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
+ setHardcodeType("thumbnail");
}}
- setHardcodeType={(type) => {
- setHardcodeType(
- type as "thumbnail" | "watermark_lu" | "watermark_rd"
- );
+ setHardcodeType={() => {
+ setHardcodeType("thumbnail");
}}
/>
@@ -295,16 +295,15 @@ export const CreateInformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_lu");
+ setHardcodeType("watermark_lu");
}}
- setHardcodeType={(type) => {
- setHardcodeType(
- type as "thumbnail" | "watermark_lu" | "watermark_rd"
- );
+ setHardcodeType={() => {
+ setHardcodeType("watermark_lu");
}}
/>
{
@@ -324,11 +323,10 @@ export const CreateInformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_rd");
+ setHardcodeType("watermark_rd");
}}
- setHardcodeType={(type) => {
- setHardcodeType(
- type as "thumbnail" | "watermark_lu" | "watermark_rd"
- );
+ setHardcodeType={() => {
+ setHardcodeType("watermark_rd");
}}
/>
@@ -412,6 +410,8 @@ export const CreateInformationTab = observer(
setIsUploadMediaOpen(false)}
+ contextObjectName={sight[language].name}
+ contextType="sight"
afterUpload={(media) => {
handleChange({
[activeMenuType ?? "thumbnail"]: media.id,
diff --git a/src/widgets/SightTabs/CreateLeftTab/index.tsx b/src/widgets/SightTabs/CreateLeftTab/index.tsx
index ebf7b57..f18d1b6 100644
--- a/src/widgets/SightTabs/CreateLeftTab/index.tsx
+++ b/src/widgets/SightTabs/CreateLeftTab/index.tsx
@@ -44,7 +44,7 @@ export const CreateLeftTab = observer(
} = editSightStore;
const { language } = languageStore;
-
+ const token = localStorage.getItem("token");
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
useState(false);
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
@@ -326,6 +326,7 @@ export const CreateLeftTab = observer(
{sight[language].left.media.length > 0 ? (
-
+ <>
+
+ {sight.watermark_lu && (
+
+ )}
+ {sight.watermark_rd && (
+
+ )}
+ >
) : (
)}
@@ -400,7 +431,13 @@ export const CreateLeftTab = observer(
sx={{
padding: 1,
maxHeight: "300px",
- overflowY: "scroll",
+ overflowY: "auto",
+ "&::-webkit-scrollbar": {
+ display: "none",
+ },
+ "&": {
+ scrollbarWidth: "none",
+ },
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
@@ -451,6 +488,10 @@ export const CreateLeftTab = observer(
setUploadMediaOpen(false)}
+ contextObjectName={sight[language].name}
+ contextType="sight"
+ isArticle={true}
+ articleName={sight[language].left.heading || "Левая статья"}
afterUpload={async (media) => {
setUploadMediaOpen(false);
setFileToUpload(null);
@@ -466,6 +507,7 @@ export const CreateLeftTab = observer(
open={isDeleteModalOpen}
onDelete={() => {
deleteLeftArticle(sight.left_article);
+ setIsDeleteModalOpen(false);
toast.success("Статья откреплена");
}}
onCancel={() => setIsDeleteModalOpen(false)}
diff --git a/src/widgets/SightTabs/CreateRightTab/index.tsx b/src/widgets/SightTabs/CreateRightTab/index.tsx
index f32f666..4d63998 100644
--- a/src/widgets/SightTabs/CreateRightTab/index.tsx
+++ b/src/widgets/SightTabs/CreateRightTab/index.tsx
@@ -153,10 +153,12 @@ export const CreateRightTab = observer(
selectedArticleId: number
) => {
try {
- await linkExistingRightArticle(selectedArticleId);
+ const linkedArticleId = await linkExistingRightArticle(
+ selectedArticleId
+ );
setSelectArticleDialogOpen(false); // Close dialog
const newIndex = sight[language].right.findIndex(
- (a) => a.id === selectedArticleId
+ (a) => a.id === linkedArticleId
);
if (newIndex > -1) {
setActiveArticleIndex(newIndex);
@@ -483,6 +485,9 @@ export const CreateRightTab = observer(
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
+ contextObjectName={sight[language].name}
+ contextType="sight"
+ isArticle={false}
/>
)}
@@ -495,6 +500,9 @@ export const CreateRightTab = observer(
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
+ contextObjectName={sight[language].name}
+ contextType="sight"
+ isArticle={false}
/>
)}
@@ -597,7 +605,13 @@ export const CreateRightTab = observer(
padding: 1,
minHeight: "200px",
maxHeight: "300px",
- overflowY: "scroll",
+ overflowY: "auto",
+ "&::-webkit-scrollbar": {
+ display: "none",
+ },
+ "&": {
+ scrollbarWidth: "none",
+ },
background:
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
@@ -698,6 +712,14 @@ export const CreateRightTab = observer(
setFileToUpload(null); // Clear file if dialog is closed without upload
setMediaTarget(null);
}}
+ contextObjectName={sight[language].name}
+ contextType="sight"
+ isArticle={mediaTarget === "rightArticle"}
+ articleName={
+ mediaTarget === "rightArticle" && activeArticleIndex !== null
+ ? sight[language].right[activeArticleIndex].heading
+ : undefined
+ }
afterUpload={handleMediaUploaded} // This will use the mediaTarget
/>
{
- const { ruCities } = cityStore;
-
const [mediaId, setMediaId] = useState("");
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
@@ -53,6 +51,7 @@ export const InformationTab = observer(
const [hardcodeType, setHardcodeType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | null
>(null);
+ const { cities } = cityStore;
useEffect(() => {
// Показывать только при инициализации (не менять при ошибках пользователя)
if (sight.common.latitude !== 0 || sight.common.longitude !== 0) {
@@ -89,7 +88,7 @@ export const InformationTab = observer(
},
true
);
- setActiveMenuType(null);
+
setIsUploadMediaOpen(false);
};
@@ -163,9 +162,9 @@ export const InformationTab = observer(
/>
city.id === sight.common.city_id
) ?? null
}
@@ -246,7 +245,7 @@ export const InformationTab = observer(
}}
>
{
@@ -270,11 +269,10 @@ export const InformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
+ setHardcodeType("thumbnail");
}}
- setHardcodeType={(type) => {
- setHardcodeType(
- type as "thumbnail" | "watermark_lu" | "watermark_rd"
- );
+ setHardcodeType={() => {
+ setHardcodeType("thumbnail");
}}
/>
{
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_lu");
+ setHardcodeType("watermark_lu");
}}
- setHardcodeType={(type) => {
- setHardcodeType(
- type as "thumbnail" | "watermark_lu" | "watermark_rd"
- );
+ setHardcodeType={() => {
+ setHardcodeType("watermark_lu");
}}
/>
{
@@ -334,11 +331,10 @@ export const InformationTab = observer(
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("watermark_rd");
+ setHardcodeType("watermark_rd");
}}
- setHardcodeType={(type) => {
- setHardcodeType(
- type as "thumbnail" | "watermark_lu" | "watermark_rd"
- );
+ setHardcodeType={() => {
+ setHardcodeType("watermark_rd");
}}
/>
@@ -399,7 +395,6 @@ export const InformationTab = observer(
open={isAddMediaOpen}
onClose={() => {
setIsAddMediaOpen(false);
- setActiveMenuType(null);
}}
onSelectMedia={handleMediaSelect}
mediaType={
@@ -414,6 +409,8 @@ export const InformationTab = observer(
setIsUploadMediaOpen(false)}
+ contextObjectName={sight[language].name}
+ contextType="sight"
afterUpload={(media) => {
handleChange(
language as Language,
diff --git a/src/widgets/SightTabs/LeftWidgetTab/index.tsx b/src/widgets/SightTabs/LeftWidgetTab/index.tsx
index e336a6c..d0d97c6 100644
--- a/src/widgets/SightTabs/LeftWidgetTab/index.tsx
+++ b/src/widgets/SightTabs/LeftWidgetTab/index.tsx
@@ -9,6 +9,7 @@ import {
SelectArticleModal,
UploadMediaDialog,
Language,
+ articlesStore,
} from "@shared";
import {
LanguageSwitcher,
@@ -43,7 +44,7 @@ export const LeftWidgetTab = observer(
const { language } = languageStore;
const data = sight[language];
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
-
+ const token = localStorage.getItem("token");
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
useState(false);
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
@@ -76,20 +77,40 @@ export const LeftWidgetTab = observer(
}, []);
const handleSelectArticle = useCallback(
- (
- articleId: number,
- heading: string,
- body: string,
- media: { id: string; media_type: number; filename: string }[]
+ async (
+ articleId: number
+ // heading: string,
+ // body: string,
+ // media: { id: string; media_type: number; filename: string }[]
) => {
setIsSelectArticleDialogOpen(false);
- updateSightInfo(languageStore.language, {
+
+ const ruArticle = await articlesStore.getArticle(articleId, "ru");
+ const enArticle = await articlesStore.getArticle(articleId, "en");
+ const zhArticle = await articlesStore.getArticle(articleId, "zh");
+
+ updateSightInfo("ru", {
left: {
- heading,
- body,
- media,
+ heading: ruArticle.data.heading,
+ body: ruArticle.data.body,
+ media: ruArticle.data.media || [],
},
});
+ updateSightInfo("en", {
+ left: {
+ heading: enArticle.data.heading,
+ body: enArticle.data.body,
+ media: enArticle.data.media || [],
+ },
+ });
+ updateSightInfo("zh", {
+ left: {
+ heading: zhArticle.data.heading,
+ body: zhArticle.data.body,
+ media: zhArticle.data.media || [],
+ },
+ });
+
updateSightInfo(
languageStore.language,
{
@@ -271,6 +292,7 @@ export const LeftWidgetTab = observer(
width: "100%",
minHeight: 100,
padding: "3px",
+ position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
@@ -285,14 +307,41 @@ export const LeftWidgetTab = observer(
}}
>
{data.left.media.length > 0 ? (
-
+ <>
+
+
+
+
+ >
) : (
)}
@@ -341,7 +390,14 @@ export const LeftWidgetTab = observer(
sx={{
padding: 1,
maxHeight: "300px",
- overflowY: "scroll",
+ overflowY: "auto",
+ width: "100%",
+ "&::-webkit-scrollbar": {
+ display: "none",
+ },
+ "&": {
+ scrollbarWidth: "none",
+ },
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
@@ -373,6 +429,12 @@ export const LeftWidgetTab = observer(
setUploadMediaOpen(false)}
+ contextObjectName={sight[languageStore.language].name}
+ contextType="sight"
+ isArticle={true}
+ articleName={
+ sight[languageStore.language].left.heading || "Левая статья"
+ }
afterUpload={async (media) => {
setUploadMediaOpen(false);
setFileToUpload(null);
diff --git a/src/widgets/SightTabs/RightWidgetTab/index.tsx b/src/widgets/SightTabs/RightWidgetTab/index.tsx
index 2a360fd..6d51c12 100644
--- a/src/widgets/SightTabs/RightWidgetTab/index.tsx
+++ b/src/widgets/SightTabs/RightWidgetTab/index.tsx
@@ -115,9 +115,21 @@ export const RightWidgetTab = observer(
setActiveArticleIndex(index);
};
- const handleCreateNew = () => {
- createNewRightArticle();
- handleClose();
+ const handleCreateNew = async () => {
+ try {
+ const newArticleId = await createNewRightArticle();
+ handleClose();
+ // Automatically select the newly created article
+ const newIndex = sight[language].right.findIndex(
+ (article) => article.id === newArticleId
+ );
+ if (newIndex > -1) {
+ setActiveArticleIndex(newIndex);
+ setType("article");
+ }
+ } catch (error) {
+ console.error("Error creating new article:", error);
+ }
};
const handleSelectExisting = () => {
@@ -129,9 +141,21 @@ export const RightWidgetTab = observer(
setIsSelectModalOpen(false);
};
- const handleArticleSelect = (id: number) => {
- linkArticle(id);
- handleCloseSelectModal();
+ const handleArticleSelect = async (id: number) => {
+ try {
+ const linkedArticleId = await linkArticle(id);
+ handleCloseSelectModal();
+ // Automatically select the newly linked article
+ const newIndex = sight[language].right.findIndex(
+ (article) => article.id === linkedArticleId
+ );
+ if (newIndex > -1) {
+ setActiveArticleIndex(newIndex);
+ setType("article");
+ }
+ } catch (error) {
+ console.error("Error linking article:", error);
+ }
};
const handleMediaSelected = async (media: {
@@ -417,6 +441,9 @@ export const RightWidgetTab = observer(
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
+ contextObjectName={sight[language].name}
+ contextType="sight"
+ isArticle={false}
/>
)}
@@ -441,6 +468,9 @@ export const RightWidgetTab = observer(
linkPreviewMedia(mediaId);
}}
onFilesDrop={() => {}}
+ contextObjectName={sight[language].name}
+ contextType="sight"
+ isArticle={false}
/>
@@ -539,7 +569,15 @@ export const RightWidgetTab = observer(
padding: 1,
minHeight: "200px",
maxHeight: "300px",
- overflowY: "scroll",
+ overflowY: "auto",
+
+ width: "100%",
+ "&::-webkit-scrollbar": {
+ display: "none",
+ },
+ "&": {
+ scrollbarWidth: "none",
+ },
background:
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
@@ -565,13 +603,13 @@ export const RightWidgetTab = observer(
sx={{
p: 2,
display: "flex",
- justifyContent: "space-between",
+ justifyContent: "center",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
flexWrap: "wrap",
- gap: 1,
+ gap: "34px",
backdropFilter: "blur(12px)",
boxShadow:
"inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
@@ -627,6 +665,14 @@ export const RightWidgetTab = observer(
setUploadMediaOpen(false)}
+ contextObjectName={sight[language].name}
+ contextType="sight"
+ isArticle={true}
+ articleName={
+ activeArticleIndex !== null
+ ? sight[language].right[activeArticleIndex].heading
+ : "Правая статья"
+ }
afterUpload={async (media) => {
setUploadMediaOpen(false);
setFileToUpload(null);
diff --git a/src/widgets/index.ts b/src/widgets/index.ts
index 98752ad..d054c18 100644
--- a/src/widgets/index.ts
+++ b/src/widgets/index.ts
@@ -16,3 +16,4 @@ export * from "./LeaveAgree";
export * from "./DeleteModal";
export * from "./SnapshotRestore";
export * from "./CreateButton";
+export * from "./modals";
diff --git a/src/widgets/modals/EditStationModal.tsx b/src/widgets/modals/EditStationModal.tsx
new file mode 100644
index 0000000..96ced02
--- /dev/null
+++ b/src/widgets/modals/EditStationModal.tsx
@@ -0,0 +1,140 @@
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ TextField,
+ Typography,
+ IconButton,
+ Box,
+} from "@mui/material";
+import { routeStore } from "@shared";
+import { observer } from "mobx-react-lite";
+import { ArrowLeft } from "lucide-react";
+import { useParams } from "react-router-dom";
+import { toast } from "react-toastify";
+
+interface EditStationModalProps {
+ open: boolean;
+ 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 }>();
+ const {
+ selectedStationId,
+ setRouteStations,
+ saveRouteStations,
+ routeStations,
+ } = routeStore;
+
+ const handleSave = async () => {
+ console.log(routeId, selectedStationId);
+
+ await saveRouteStations(Number(routeId), selectedStationId);
+ toast.success("Успешно сохранено");
+ onClose();
+ };
+
+ const station = routeStations[Number(routeId)]?.find(
+ (station: any) => station.id === selectedStationId
+ );
+
+ return (
+
+ );
+ }
+);
diff --git a/src/widgets/modals/index.ts b/src/widgets/modals/index.ts
index e714367..65ce130 100644
--- a/src/widgets/modals/index.ts
+++ b/src/widgets/modals/index.ts
@@ -1 +1,2 @@
export * from "./SelectArticleDialog";
+export * from "./EditStationModal";