Compare commits
5 Commits
442160ba38
...
4b02c6e9d3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b02c6e9d3 | |||
| a58f438dce | |||
| 5e0b56c7dc | |||
| a182a52111 | |||
| dd5aee58e6 |
14
.env
14
.env
@@ -1,8 +1,8 @@
|
|||||||
VITE_API_URL='https://wn.st.unprism.ru'
|
# # VITE_API_URL='https://wn.st.unprism.ru'
|
||||||
VITE_REACT_APP ='https://wn.st.unprism.ru/'
|
# # VITE_REACT_APP ='https://wn.st.unprism.ru/'
|
||||||
VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/'
|
# # VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/'
|
||||||
VITE_NEED_AUTH='true'
|
|
||||||
# VITE_API_URL='https://wn.krbl.ru'
|
|
||||||
# VITE_REACT_APP ='https://wn.krbl.ru/'
|
|
||||||
# VITE_KRBL_MEDIA='https://wn.krbl.ru/media/'
|
|
||||||
# VITE_NEED_AUTH='true'
|
# VITE_NEED_AUTH='true'
|
||||||
|
VITE_API_URL='https://wn.krbl.ru'
|
||||||
|
VITE_REACT_APP ='https://wn.krbl.ru/'
|
||||||
|
VITE_KRBL_MEDIA='https://wn.krbl.ru/media/'
|
||||||
|
VITE_NEED_AUTH='true'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, articlesStore, languageStore } from "@shared";
|
import { authStore, articlesStore, languageStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Trash2, Eye, Minus } from "lucide-react";
|
import { Trash2, Eye, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -22,6 +22,7 @@ export const ArticleListPage = observer(() => {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
});
|
});
|
||||||
const canWriteArticles = authStore.canWrite("sights");
|
const canWriteArticles = authStore.canWrite("sights");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchArticles = async () => {
|
const fetchArticles = async () => {
|
||||||
@@ -72,11 +73,16 @@ export const ArticleListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = articleList[language].data.map((article) => ({
|
const rows = useMemo(() => {
|
||||||
id: article.id,
|
const query = searchQuery.trim().toLowerCase();
|
||||||
heading: article.heading,
|
return articleList[language].data
|
||||||
body: article.body,
|
.filter((article) => !query || (article.heading ?? "").toLowerCase().includes(query))
|
||||||
}));
|
.map((article) => ({
|
||||||
|
id: article.id,
|
||||||
|
heading: article.heading,
|
||||||
|
body: article.body,
|
||||||
|
}));
|
||||||
|
}, [articleList[language].data, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -99,6 +105,8 @@ export const ArticleListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, carrierStore, cityStore, languageStore } from "@shared";
|
import { authStore, carrierStore, cityStore, languageStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -22,6 +22,7 @@ export const CarrierListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canReadCities = authStore.canRead("cities");
|
const canReadCities = authStore.canRead("cities");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -119,16 +120,23 @@ export const CarrierListPage = observer(() => {
|
|||||||
|
|
||||||
const canWriteCarriers = authStore.canWrite("carriers");
|
const canWriteCarriers = authStore.canWrite("carriers");
|
||||||
|
|
||||||
const rows = carriers[language].data
|
const rows = useMemo(() => {
|
||||||
?.filter((carrier) =>
|
const query = searchQuery.trim().toLowerCase();
|
||||||
!allowedCityIds || allowedCityIds.includes(carrier.city_id),
|
return (carriers[language].data ?? [])
|
||||||
)
|
.filter((carrier) => !allowedCityIds || allowedCityIds.includes(carrier.city_id))
|
||||||
.map((carrier) => ({
|
.filter(
|
||||||
id: carrier.id,
|
(carrier) =>
|
||||||
full_name: carrier.full_name,
|
!query ||
|
||||||
short_name: carrier.short_name,
|
(carrier.full_name ?? "").toLowerCase().includes(query) ||
|
||||||
city_id: carrier.city_id,
|
(carrier.short_name ?? "").toLowerCase().includes(query)
|
||||||
}));
|
)
|
||||||
|
.map((carrier) => ({
|
||||||
|
id: carrier.id,
|
||||||
|
full_name: carrier.full_name,
|
||||||
|
short_name: carrier.short_name,
|
||||||
|
city_id: carrier.city_id,
|
||||||
|
}));
|
||||||
|
}, [carriers[language].data, searchQuery, allowedCityIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -153,6 +161,8 @@ export const CarrierListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, languageStore, cityStore, countryStore } from "@shared";
|
import { authStore, languageStore, cityStore, countryStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -24,6 +24,7 @@ export const CityListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteCities = authStore.canWrite("cities");
|
const canWriteCities = authStore.canWrite("cities");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -57,6 +58,18 @@ export const CityListPage = observer(() => {
|
|||||||
setRows(newRows2 || []);
|
setRows(newRows2 || []);
|
||||||
}, [cities, countryStore.countries, language, isLoading]);
|
}, [cities, countryStore.countries, language, isLoading]);
|
||||||
|
|
||||||
|
const filteredRows = useMemo(() => {
|
||||||
|
const query = searchQuery.trim().toLowerCase();
|
||||||
|
if (!query) return rows;
|
||||||
|
return rows.filter((row) => {
|
||||||
|
const cityName = (row.name ?? "").toLowerCase();
|
||||||
|
const countryName = (
|
||||||
|
countryStore.countries[language]?.data?.find((c) => c.code === row.country)?.name ?? ""
|
||||||
|
).toLowerCase();
|
||||||
|
return cityName.includes(query) || countryName.includes(query);
|
||||||
|
});
|
||||||
|
}, [rows, searchQuery, countryStore.countries, language]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
field: "country",
|
field: "country",
|
||||||
@@ -142,8 +155,10 @@ export const CityListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={filteredRows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
checkboxSelection={canWriteCities}
|
checkboxSelection={canWriteCities}
|
||||||
disableRowSelectionExcludeModel
|
disableRowSelectionExcludeModel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, countryStore, languageStore } from "@shared";
|
import { authStore, countryStore, languageStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Trash2, Minus } from "lucide-react";
|
import { Trash2, Minus } from "lucide-react";
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ export const CountryListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteCountries = authStore.canWrite("countries");
|
const canWriteCountries = authStore.canWrite("countries");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCountries = async () => {
|
const fetchCountries = async () => {
|
||||||
@@ -72,11 +73,16 @@ export const CountryListPage = observer(() => {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = countries[language]?.data.map((country) => ({
|
const rows = useMemo(() => {
|
||||||
id: country.code,
|
const query = searchQuery.trim().toLowerCase();
|
||||||
code: country.code,
|
return (countries[language]?.data ?? [])
|
||||||
name: country.name,
|
.filter((country) => !query || (country.name ?? "").toLowerCase().includes(query))
|
||||||
}));
|
.map((country) => ({
|
||||||
|
id: country.code,
|
||||||
|
code: country.code,
|
||||||
|
name: country.name,
|
||||||
|
}));
|
||||||
|
}, [countries[language]?.data, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -102,8 +108,10 @@ export const CountryListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows || []}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
checkboxSelection={canWriteCountries}
|
checkboxSelection={canWriteCountries}
|
||||||
disableRowSelectionExcludeModel
|
disableRowSelectionExcludeModel
|
||||||
|
|||||||
@@ -244,9 +244,9 @@ class MapStore {
|
|||||||
const sorted = [...features];
|
const sorted = [...features];
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case "name_asc":
|
case "name_asc":
|
||||||
return sorted.sort((a, b) => a.name.localeCompare(b.name));
|
return sorted.sort((a, b) => a.name.trim().localeCompare(b.name.trim()));
|
||||||
case "name_desc":
|
case "name_desc":
|
||||||
return sorted.sort((a, b) => b.name.localeCompare(a.name));
|
return sorted.sort((a, b) => b.name.trim().localeCompare(a.name.trim()));
|
||||||
case "created_asc":
|
case "created_asc":
|
||||||
return sorted.sort((a, b) => {
|
return sorted.sort((a, b) => {
|
||||||
if (
|
if (
|
||||||
@@ -379,7 +379,7 @@ class MapStore {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
this.routes = this.routes.sort((a, b) =>
|
this.routes = this.routes.sort((a, b) =>
|
||||||
a.route_number.localeCompare(b.route_number),
|
a.route_number.trim().localeCompare(b.route_number.trim()),
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.preloadRouteStations(routesIds);
|
await this.preloadRouteStations(routesIds);
|
||||||
@@ -2545,14 +2545,14 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
|||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case "name_asc":
|
case "name_asc":
|
||||||
return sorted.sort((a, b) =>
|
return sorted.sort((a, b) =>
|
||||||
((a.get("name") as string) || "").localeCompare(
|
((a.get("name") as string) || "").trim().localeCompare(
|
||||||
(b.get("name") as string) || "",
|
((b.get("name") as string) || "").trim(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case "name_desc":
|
case "name_desc":
|
||||||
return sorted.sort((a, b) =>
|
return sorted.sort((a, b) =>
|
||||||
((b.get("name") as string) || "").localeCompare(
|
((b.get("name") as string) || "").trim().localeCompare(
|
||||||
(a.get("name") as string) || "",
|
((a.get("name") as string) || "").trim(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case "created_asc":
|
case "created_asc":
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class MapStore {
|
|||||||
path: route.path,
|
path: route.path,
|
||||||
}));
|
}));
|
||||||
this.routes = mappedRoutes.sort((a, b) =>
|
this.routes = mappedRoutes.sort((a, b) =>
|
||||||
a.route_number.localeCompare(b.route_number)
|
a.route_number.trim().localeCompare(b.route_number.trim())
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore } from "@shared";
|
import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2, Minus } from "lucide-react";
|
import { Eye, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -22,6 +22,7 @@ export const MediaListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteMedia = authStore.canWrite("sights");
|
const canWriteMedia = authStore.canWrite("sights");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMedia = async () => {
|
const fetchMedia = async () => {
|
||||||
@@ -95,15 +96,22 @@ export const MediaListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = media.map((media) => ({
|
const rows = useMemo(() => {
|
||||||
id: media.id,
|
const query = searchQuery.trim().toLowerCase();
|
||||||
media_name: media.media_name,
|
return media
|
||||||
media_type: media.media_type,
|
.filter((item) => !query || (item.media_name ?? "").toLowerCase().includes(query))
|
||||||
}));
|
.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
media_name: item.media_name,
|
||||||
|
media_type: item.media_type,
|
||||||
|
}));
|
||||||
|
}, [media, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
{canWriteMedia && ids.length > 0 && (
|
{canWriteMedia && ids.length > 0 && (
|
||||||
<div className="flex justify-end mb-5 duration-300">
|
<div className="flex justify-end mb-5 duration-300">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -267,9 +267,9 @@ export const RouteCreatePage = observer(() => {
|
|||||||
language as keyof typeof carrierStore.carriers
|
language as keyof typeof carrierStore.carriers
|
||||||
].data?.find((c: any) => c.id === carrier_id)?.full_name || "",
|
].data?.find((c: any) => c.id === carrier_id)?.full_name || "",
|
||||||
carrier_id,
|
carrier_id,
|
||||||
route_number: routeNumber,
|
route_number: routeNumber.trim(),
|
||||||
route_sys_number: govRouteNumber,
|
route_sys_number: govRouteNumber.trim(),
|
||||||
route_name: routeName,
|
route_name: routeName.trim(),
|
||||||
route_direction,
|
route_direction,
|
||||||
scale_min: scale_min !== null ? scale_min : 0,
|
scale_min: scale_min !== null ? scale_min : 0,
|
||||||
scale_max: scale_max !== null ? scale_max : 0,
|
scale_max: scale_max !== null ? scale_max : 0,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, carrierStore, languageStore, routeStore } from "@shared";
|
import { authStore, carrierStore, languageStore, routeStore, selectedCityStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Map, Pencil, Trash2, Minus } from "lucide-react";
|
import { Map, Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -28,6 +28,7 @@ export const RouteListPage = observer(() => {
|
|||||||
authStore.canWrite("sights") &&
|
authStore.canWrite("sights") &&
|
||||||
authStore.canWrite("routes");
|
authStore.canWrite("routes");
|
||||||
const canShowActionsColumn = canWriteRoutes || canShowRoutePreview;
|
const canShowActionsColumn = canWriteRoutes || canShowRoutePreview;
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -146,13 +147,33 @@ export const RouteListPage = observer(() => {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = routes.data.map((route) => ({
|
const rows = useMemo(() => {
|
||||||
id: route.id,
|
const { selectedCityId } = selectedCityStore;
|
||||||
carrier_id: route.carrier_id,
|
const query = searchQuery.trim().toLowerCase();
|
||||||
route_number: route.route_number,
|
let filtered = routes.data;
|
||||||
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
if (selectedCityId) {
|
||||||
route_name: route.route_name,
|
const cityCarrierIds = new Set(
|
||||||
}));
|
carriers["ru"].data
|
||||||
|
.filter((c) => c.city_id === selectedCityId)
|
||||||
|
.map((c) => c.id)
|
||||||
|
);
|
||||||
|
filtered = filtered.filter((route) => cityCarrierIds.has(route.carrier_id));
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
.filter(
|
||||||
|
(route) =>
|
||||||
|
!query ||
|
||||||
|
(route.route_name ?? "").toLowerCase().includes(query) ||
|
||||||
|
String(route.route_number ?? "").toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
.map((route) => ({
|
||||||
|
id: route.id,
|
||||||
|
carrier_id: route.carrier_id,
|
||||||
|
route_number: route.route_number,
|
||||||
|
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
||||||
|
route_name: route.route_name,
|
||||||
|
}));
|
||||||
|
}, [routes.data, carriers["ru"].data, selectedCityStore.selectedCityId, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -178,6 +199,8 @@ export const RouteListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const MapDataContext = createContext<{
|
|||||||
) => void;
|
) => void;
|
||||||
setSightIconSize: (sightId: number, size: number) => void;
|
setSightIconSize: (sightId: number, size: number) => void;
|
||||||
setFontSize: (size: number) => void;
|
setFontSize: (size: number) => void;
|
||||||
|
setRouteIconSize: (size: number) => void;
|
||||||
saveChanges: () => void;
|
saveChanges: () => void;
|
||||||
}>({
|
}>({
|
||||||
originalRouteData: undefined,
|
originalRouteData: undefined,
|
||||||
@@ -67,6 +68,7 @@ const MapDataContext = createContext<{
|
|||||||
setSightCoordinates: () => {},
|
setSightCoordinates: () => {},
|
||||||
setSightIconSize: () => {},
|
setSightIconSize: () => {},
|
||||||
setFontSize: () => {},
|
setFontSize: () => {},
|
||||||
|
setRouteIconSize: () => {},
|
||||||
saveChanges: () => {},
|
saveChanges: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,6 +182,16 @@ export const MapDataProvider = observer(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setRouteIconSize(size: number) {
|
||||||
|
const clamped = Math.max(1, Math.min(300, size));
|
||||||
|
setRouteChanges((prev) => {
|
||||||
|
if (prev.icon_size === clamped) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return { ...prev, icon_size: clamped };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setMapCenter(latitude: number, longitude: number) {
|
function setMapCenter(latitude: number, longitude: number) {
|
||||||
const epsilon = 1e-6;
|
const epsilon = 1e-6;
|
||||||
|
|
||||||
@@ -579,6 +591,7 @@ export const MapDataProvider = observer(
|
|||||||
setSightCoordinates,
|
setSightCoordinates,
|
||||||
setSightIconSize,
|
setSightIconSize,
|
||||||
setFontSize,
|
setFontSize,
|
||||||
|
setRouteIconSize,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
originalRouteData,
|
originalRouteData,
|
||||||
@@ -594,6 +607,7 @@ export const MapDataProvider = observer(
|
|||||||
setStationIconSize,
|
setStationIconSize,
|
||||||
setSightIconSize,
|
setSightIconSize,
|
||||||
setFontSize,
|
setFontSize,
|
||||||
|
setRouteIconSize,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function RightSidebar() {
|
|||||||
setMapRotation,
|
setMapRotation,
|
||||||
setMapCenter,
|
setMapCenter,
|
||||||
setFontSize: updateFontSize,
|
setFontSize: updateFontSize,
|
||||||
|
setRouteIconSize: updateRouteIconSize,
|
||||||
} = useMapData();
|
} = useMapData();
|
||||||
const { rotation, rotateToAngle, scale, setScaleAtCenter } = useTransform();
|
const { rotation, rotateToAngle, scale, setScaleAtCenter } = useTransform();
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export function RightSidebar() {
|
|||||||
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
const [rotationDegrees, setRotationDegrees] = useState<number>(0);
|
||||||
const [isUserEditing, setIsUserEditing] = useState<boolean>(false);
|
const [isUserEditing, setIsUserEditing] = useState<boolean>(false);
|
||||||
const [fontSize, setFontSize] = useState<number>(100);
|
const [fontSize, setFontSize] = useState<number>(100);
|
||||||
|
const [defaultIconSize, setDefaultIconSize] = useState<number>(100);
|
||||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,6 +54,7 @@ export function RightSidebar() {
|
|||||||
y: originalRouteData.center_longitude ?? 0,
|
y: originalRouteData.center_longitude ?? 0,
|
||||||
});
|
});
|
||||||
setFontSize(originalRouteData.font_size ?? 100);
|
setFontSize(originalRouteData.font_size ?? 100);
|
||||||
|
setDefaultIconSize(originalRouteData.icon_size ?? 100);
|
||||||
}
|
}
|
||||||
}, [originalRouteData]);
|
}, [originalRouteData]);
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ export function RightSidebar() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRotationDegrees(
|
setRotationDegrees(
|
||||||
((Math.round((rotation * 180) / Math.PI) % 360) + 360) % 360
|
((Math.round((rotation * 180) / Math.PI) % 360) + 360) % 360,
|
||||||
);
|
);
|
||||||
}, [rotation]);
|
}, [rotation]);
|
||||||
|
|
||||||
@@ -108,6 +111,20 @@ export function RightSidebar() {
|
|||||||
setFontSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
|
setFontSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
|
||||||
}, [routeData?.font_size, originalRouteData?.font_size]);
|
}, [routeData?.font_size, originalRouteData?.font_size]);
|
||||||
|
|
||||||
|
const handleDefaultIconSizeChange = (value: number) => {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clamped = Math.max(1, Math.min(300, Math.round(value)));
|
||||||
|
setDefaultIconSize(clamped);
|
||||||
|
updateRouteIconSize(clamped);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = routeData?.icon_size ?? originalRouteData?.icon_size ?? 100;
|
||||||
|
setDefaultIconSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
|
||||||
|
}, [routeData?.icon_size, originalRouteData?.icon_size]);
|
||||||
|
|
||||||
if (!routeData) {
|
if (!routeData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -317,6 +334,33 @@ export function RightSidebar() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Размер иконок по умолчанию (%)"
|
||||||
|
variant="filled"
|
||||||
|
value={defaultIconSize}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
handleDefaultIconSizeChange(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: "#222", borderRadius: 4 }}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputLabel-root": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-input": {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
min: 1,
|
||||||
|
max: 300,
|
||||||
|
step: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
label="Поворот (в градусах)"
|
label="Поворот (в градусах)"
|
||||||
|
|||||||
@@ -216,6 +216,12 @@ const rotateVertices = (
|
|||||||
const DRAG_THRESHOLD_PX = 4;
|
const DRAG_THRESHOLD_PX = 4;
|
||||||
const ICON_SIZE_MIN = 1;
|
const ICON_SIZE_MIN = 1;
|
||||||
const ICON_SIZE_MAX = 300;
|
const ICON_SIZE_MAX = 300;
|
||||||
|
const DEBUG_WEBGL_ROUTE_MAP = true;
|
||||||
|
|
||||||
|
const debugWebglLog = (...args: unknown[]) => {
|
||||||
|
if (!DEBUG_WEBGL_ROUTE_MAP) return;
|
||||||
|
console.log("[WebGLRouteMapPrototype]", ...args);
|
||||||
|
};
|
||||||
|
|
||||||
type StationDragState = {
|
type StationDragState = {
|
||||||
stationId: number;
|
stationId: number;
|
||||||
@@ -735,7 +741,16 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
next: Transform,
|
next: Transform,
|
||||||
options?: { immediate?: boolean; skipClamp?: boolean },
|
options?: { immediate?: boolean; skipClamp?: boolean },
|
||||||
) => {
|
) => {
|
||||||
|
const prevTransform = transformRef.current;
|
||||||
const adjusted = options?.skipClamp ? next : clampTransformScale(next);
|
const adjusted = options?.skipClamp ? next : clampTransformScale(next);
|
||||||
|
debugWebglLog("updateTransform", {
|
||||||
|
immediate: Boolean(options?.immediate),
|
||||||
|
skipClamp: Boolean(options?.skipClamp),
|
||||||
|
prevScale: prevTransform?.scale ?? null,
|
||||||
|
nextScale: adjusted.scale,
|
||||||
|
prevTranslation: prevTransform?.translation ?? null,
|
||||||
|
nextTranslation: adjusted.translation,
|
||||||
|
});
|
||||||
|
|
||||||
transformRef.current = adjusted;
|
transformRef.current = adjusted;
|
||||||
if (options?.immediate) {
|
if (options?.immediate) {
|
||||||
@@ -1268,10 +1283,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const safePercent = Math.max(ICON_SIZE_MIN, currentPercent);
|
const safePercent = Math.max(ICON_SIZE_MIN, currentPercent);
|
||||||
const baseSizePxAt100 = Math.max(
|
const baseSizePxAt100 = Math.max(1, initialSizePx / (safePercent / 100));
|
||||||
1,
|
|
||||||
initialSizePx / (safePercent / 100),
|
|
||||||
);
|
|
||||||
|
|
||||||
sightIconResizeStateRef.current = {
|
sightIconResizeStateRef.current = {
|
||||||
sightId,
|
sightId,
|
||||||
@@ -1376,6 +1388,10 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
|
|
||||||
if (typeof ResizeObserver === "undefined") {
|
if (typeof ResizeObserver === "undefined") {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
|
debugWebglLog("container resize (fallback)", {
|
||||||
|
width: container.clientWidth,
|
||||||
|
height: container.clientHeight,
|
||||||
|
});
|
||||||
setCanvasSize({
|
setCanvasSize({
|
||||||
width: container.clientWidth,
|
width: container.clientWidth,
|
||||||
height: container.clientHeight,
|
height: container.clientHeight,
|
||||||
@@ -1389,6 +1405,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
const observer = new ResizeObserver((entries) => {
|
const observer = new ResizeObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const { width, height } = entry.contentRect;
|
const { width, height } = entry.contentRect;
|
||||||
|
debugWebglLog("container resize", { width, height });
|
||||||
setCanvasSize({ width, height });
|
setCanvasSize({ width, height });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1418,6 +1435,15 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
const displayWidth = Math.max(1, Math.floor(canvasSize.width * dpr));
|
const displayWidth = Math.max(1, Math.floor(canvasSize.width * dpr));
|
||||||
const displayHeight = Math.max(1, Math.floor(canvasSize.height * dpr));
|
const displayHeight = Math.max(1, Math.floor(canvasSize.height * dpr));
|
||||||
|
debugWebglLog("drawScene:start", {
|
||||||
|
cssCanvasWidth: canvasSize.width,
|
||||||
|
cssCanvasHeight: canvasSize.height,
|
||||||
|
dpr,
|
||||||
|
displayWidth,
|
||||||
|
displayHeight,
|
||||||
|
routeVerticesCount: rotatedRouteVertices.length / 2,
|
||||||
|
stationVerticesCount: rotatedStationVertices.length / 2,
|
||||||
|
});
|
||||||
|
|
||||||
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
|
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
|
||||||
canvas.width = displayWidth;
|
canvas.width = displayWidth;
|
||||||
@@ -1531,6 +1557,11 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { scale, translation } = transform;
|
const { scale, translation } = transform;
|
||||||
|
debugWebglLog("drawScene:transform", {
|
||||||
|
scale,
|
||||||
|
translation,
|
||||||
|
scaleLimits: scaleLimitsRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
const desiredRouteWidthCss = 7;
|
const desiredRouteWidthCss = 7;
|
||||||
const desiredStationDiameterCss = 12;
|
const desiredStationDiameterCss = 12;
|
||||||
@@ -1545,6 +1576,11 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
rotatedRouteVertices,
|
rotatedRouteVertices,
|
||||||
lineWidth,
|
lineWidth,
|
||||||
);
|
);
|
||||||
|
debugWebglLog("drawScene:route", {
|
||||||
|
desiredRouteWidthCss,
|
||||||
|
lineWidthWorldUnits: lineWidth,
|
||||||
|
thickVerticesCount: thickVertices.length / 2,
|
||||||
|
});
|
||||||
if (thickVertices.length === 0) {
|
if (thickVertices.length === 0) {
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, rotatedRouteVertices, gl.STATIC_DRAW);
|
gl.bufferData(gl.ARRAY_BUFFER, rotatedRouteVertices, gl.STATIC_DRAW);
|
||||||
} else {
|
} else {
|
||||||
@@ -1635,6 +1671,10 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
pointOuterSizePx,
|
pointOuterSizePx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
debugWebglLog("drawScene:stations", {
|
||||||
|
pointOuterSizePx,
|
||||||
|
pointInnerSizePx,
|
||||||
|
});
|
||||||
if (pointProgram.uniformLocations.u_color) {
|
if (pointProgram.uniformLocations.u_color) {
|
||||||
gl.uniform4f(
|
gl.uniform4f(
|
||||||
pointProgram.uniformLocations.u_color,
|
pointProgram.uniformLocations.u_color,
|
||||||
@@ -1814,6 +1854,87 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
drawScene();
|
drawScene();
|
||||||
}, [drawScene]);
|
}, [drawScene]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const camera =
|
||||||
|
transformState ?? transformRef.current ?? lastTransformRef.current;
|
||||||
|
if (!camera || !sightData?.length) return;
|
||||||
|
|
||||||
|
const routeIconSizePercent =
|
||||||
|
routeData?.icon_size ?? originalRouteData?.icon_size ?? 100;
|
||||||
|
|
||||||
|
for (const sight of sightData) {
|
||||||
|
const shouldUseCustomSightIcon =
|
||||||
|
sight.is_default_icon === false && !isMediaIdEmpty(sight.icon);
|
||||||
|
if (!shouldUseCustomSightIcon) continue;
|
||||||
|
|
||||||
|
const customSightIconScaleFactor =
|
||||||
|
camera.scale / Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6);
|
||||||
|
const sightIconSizePercent =
|
||||||
|
liveSightIconSizes.get(sight.id) ??
|
||||||
|
(typeof sight.icon_size === "number" && Number.isFinite(sight.icon_size)
|
||||||
|
? sight.icon_size
|
||||||
|
: routeIconSizePercent);
|
||||||
|
const iconSizePx =
|
||||||
|
30 *
|
||||||
|
clamp(sightIconSizePercent / 100, 0.1, 10) *
|
||||||
|
customSightIconScaleFactor;
|
||||||
|
|
||||||
|
debugWebglLog("custom sight icon size", {
|
||||||
|
sightId: sight.id,
|
||||||
|
cameraScale: camera.scale,
|
||||||
|
baseScale: customSightIconBaseScaleRef.current,
|
||||||
|
scaleFactor: customSightIconScaleFactor,
|
||||||
|
iconPercent: sightIconSizePercent,
|
||||||
|
iconSizePx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
sightData,
|
||||||
|
liveSightIconSizes,
|
||||||
|
transformState,
|
||||||
|
routeData?.icon_size,
|
||||||
|
originalRouteData?.icon_size,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stationData?.ru?.length) return;
|
||||||
|
|
||||||
|
const fontSizePercent =
|
||||||
|
routeData?.font_size ?? originalRouteData?.font_size ?? 100;
|
||||||
|
const fontScale = fontSizePercent / 100;
|
||||||
|
const baseStationIconSizePx = 16 * fontScale * 1.2;
|
||||||
|
|
||||||
|
for (const station of stationData.ru) {
|
||||||
|
const hasCustomStationIcon = !isMediaIdEmpty(station.icon);
|
||||||
|
if (!hasCustomStationIcon) continue;
|
||||||
|
|
||||||
|
const stationIconSizePercent =
|
||||||
|
liveStationIconSizes.get(station.id) ??
|
||||||
|
(typeof station.icon_size === "number" && Number.isFinite(station.icon_size)
|
||||||
|
? station.icon_size
|
||||||
|
: 100);
|
||||||
|
const iconSizePx = Math.max(
|
||||||
|
1,
|
||||||
|
Math.round(
|
||||||
|
baseStationIconSizePx * clamp(stationIconSizePercent / 100, 0.1, 10),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
debugWebglLog("custom station icon size", {
|
||||||
|
stationId: station.id,
|
||||||
|
fontSizePercent,
|
||||||
|
baseStationIconSizePx,
|
||||||
|
iconPercent: stationIconSizePercent,
|
||||||
|
iconSizePx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
stationData?.ru,
|
||||||
|
liveStationIconSizes,
|
||||||
|
routeData?.font_size,
|
||||||
|
originalRouteData?.font_size,
|
||||||
|
]);
|
||||||
|
|
||||||
const applyCenterFromCoordinates = useCallback(
|
const applyCenterFromCoordinates = useCallback(
|
||||||
(latitude: number, longitude: number) => {
|
(latitude: number, longitude: number) => {
|
||||||
const roundedLat = Math.round(latitude * 1e6) / 1e6;
|
const roundedLat = Math.round(latitude * 1e6) / 1e6;
|
||||||
@@ -2167,6 +2288,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const translatedStation = stationData?.[language]?.[index];
|
const translatedStation = stationData?.[language]?.[index];
|
||||||
|
const enStation = stationData?.["en"]?.[index];
|
||||||
|
|
||||||
const local = coordinatesToLocal(
|
const local = coordinatesToLocal(
|
||||||
station.latitude,
|
station.latitude,
|
||||||
@@ -2220,11 +2342,12 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
const rotationCss = `${rotationAngle}rad`;
|
const rotationCss = `${rotationAngle}rad`;
|
||||||
const counterRotationCss = `${-rotationAngle}rad`;
|
const counterRotationCss = `${-rotationAngle}rad`;
|
||||||
|
|
||||||
|
const secondaryStation =
|
||||||
|
language === "ru" ? enStation : translatedStation;
|
||||||
const showSecondary =
|
const showSecondary =
|
||||||
language !== "ru" &&
|
secondaryStation &&
|
||||||
translatedStation &&
|
secondaryStation.name &&
|
||||||
translatedStation.name &&
|
secondaryStation.name !== station.name;
|
||||||
translatedStation.name !== station.name;
|
|
||||||
|
|
||||||
const fontSizePercent =
|
const fontSizePercent =
|
||||||
routeData?.font_size ?? originalRouteData?.font_size ?? 100;
|
routeData?.font_size ?? originalRouteData?.font_size ?? 100;
|
||||||
@@ -2446,7 +2569,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{translatedStation?.name}
|
{secondaryStation?.name}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -2572,8 +2695,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
const cssX = labelX / dpr;
|
const cssX = labelX / dpr;
|
||||||
const cssY = labelY / dpr;
|
const cssY = labelY / dpr;
|
||||||
const shouldUseCustomSightIcon =
|
const shouldUseCustomSightIcon =
|
||||||
sight.is_default_icon === false &&
|
sight.is_default_icon === false && !isMediaIdEmpty(sight.icon);
|
||||||
!isMediaIdEmpty(sight.icon);
|
|
||||||
const sightIconUrl = shouldUseCustomSightIcon
|
const sightIconUrl = shouldUseCustomSightIcon
|
||||||
? `${mediaBaseUrl}${sight.icon}/download?token=${mediaToken}`
|
? `${mediaBaseUrl}${sight.icon}/download?token=${mediaToken}`
|
||||||
: SIGHT_ICON_URL;
|
: SIGHT_ICON_URL;
|
||||||
@@ -2581,21 +2703,21 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
? camera.scale /
|
? camera.scale /
|
||||||
Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6)
|
Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6)
|
||||||
: 1;
|
: 1;
|
||||||
const sightIconSizePercent =
|
const sightIconSizePercent = sight.is_default_icon === false
|
||||||
liveSightIconSizes.get(sight.id) ??
|
? (liveSightIconSizes.get(sight.id) ??
|
||||||
(typeof sight.icon_size === "number" &&
|
(typeof sight.icon_size === "number" &&
|
||||||
Number.isFinite(sight.icon_size)
|
Number.isFinite(sight.icon_size)
|
||||||
? sight.icon_size
|
? sight.icon_size
|
||||||
: (routeData?.icon_size ??
|
: 100))
|
||||||
originalRouteData?.icon_size ??
|
: (routeData?.icon_size ?? originalRouteData?.icon_size ?? 100);
|
||||||
100));
|
|
||||||
const iconSize =
|
const iconSize =
|
||||||
30 *
|
30 *
|
||||||
clamp(sightIconSizePercent / 100, 0.1, 10) *
|
clamp(sightIconSizePercent / 100, 0.1, 10) *
|
||||||
customSightIconScaleFactor;
|
customSightIconScaleFactor;
|
||||||
const showSightResizeUi =
|
const showSightResizeUi =
|
||||||
hoveredSightIconId === sight.id ||
|
sight.is_default_icon !== true &&
|
||||||
resizingSightIconId === sight.id;
|
(hoveredSightIconId === sight.id ||
|
||||||
|
resizingSightIconId === sight.id);
|
||||||
const iconLeft = cssX - iconSize;
|
const iconLeft = cssX - iconSize;
|
||||||
const iconTop = cssY - iconSize;
|
const iconTop = cssY - iconSize;
|
||||||
const labelHeight = 24;
|
const labelHeight = 24;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
languageStore,
|
languageStore,
|
||||||
sightsStore,
|
sightsStore,
|
||||||
selectedCityStore,
|
selectedCityStore,
|
||||||
|
SearchInput,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
@@ -28,6 +29,7 @@ export const SightListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canReadCities = authStore.canRead("cities");
|
const canReadCities = authStore.canRead("cities");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSights = async () => {
|
const fetchSights = async () => {
|
||||||
@@ -120,13 +122,16 @@ export const SightListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
}, [sights, selectedCityStore.selectedCityId, canReadCities, authStore.meCities]);
|
}, [sights, selectedCityStore.selectedCityId, canReadCities, authStore.meCities]);
|
||||||
|
|
||||||
const canWriteSights = authStore.canWrite("sights");
|
const query = searchQuery.trim().toLowerCase();
|
||||||
|
const rows = filteredSights
|
||||||
|
.filter((sight: any) => !query || (sight.name ?? "").toLowerCase().includes(query))
|
||||||
|
.map((sight) => ({
|
||||||
|
id: sight.id,
|
||||||
|
name: sight.name,
|
||||||
|
city_id: sight.city_id,
|
||||||
|
}));
|
||||||
|
|
||||||
const rows = filteredSights.map((sight) => ({
|
const canWriteSights = authStore.canWrite("sights");
|
||||||
id: sight.id,
|
|
||||||
name: sight.name,
|
|
||||||
city_id: sight.city_id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -155,6 +160,8 @@ export const SightListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, languageStore, snapshotStore } from "@shared";
|
import { authStore, languageStore, snapshotStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { DatabaseBackup, Trash2 } from "lucide-react";
|
import { DatabaseBackup, Trash2 } from "lucide-react";
|
||||||
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
|
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
|
||||||
@@ -19,6 +19,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [paginationModel, setPaginationModel] = useState({
|
const [paginationModel, setPaginationModel] = useState({
|
||||||
page: 0,
|
page: 0,
|
||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
@@ -89,22 +90,35 @@ export const SnapshotListPage = observer(() => {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = snapshots.map((snapshot) => ({
|
const rows = useMemo(() => {
|
||||||
id: snapshot.ID,
|
const query = searchQuery.trim().toLowerCase();
|
||||||
name: snapshot.Name,
|
return snapshots
|
||||||
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
.filter(
|
||||||
created_at: formatCreationTime(snapshot.CreationTime),
|
(snapshot) =>
|
||||||
}));
|
!query ||
|
||||||
|
(snapshot.Name ?? "").toLowerCase().includes(query) ||
|
||||||
|
(snapshots.find((s) => s.ID === snapshot.ParentID)?.Name ?? "").toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
.map((snapshot) => ({
|
||||||
|
id: snapshot.ID,
|
||||||
|
name: snapshot.Name,
|
||||||
|
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
||||||
|
created_at: formatCreationTime(snapshot.CreationTime),
|
||||||
|
}));
|
||||||
|
}, [snapshots, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl ">Экспорт Медиа</h1>
|
<h1 className="text-2xl ">Экспорт Медиа</h1>
|
||||||
|
|
||||||
{canCreateSnapshot && (
|
{canCreateSnapshot && (
|
||||||
<CreateButton label="Создать экспорт медиа" path="/snapshot/create" />
|
<CreateButton label="Создать экспорт медиа" path="/snapshot/create" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import {
|
|||||||
languageStore,
|
languageStore,
|
||||||
stationsStore,
|
stationsStore,
|
||||||
selectedCityStore,
|
selectedCityStore,
|
||||||
|
SearchInput,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2, Minus, Route } from "lucide-react";
|
import { Pencil, Trash2, Minus, Route } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -36,6 +37,7 @@ export const StationListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteStations = authStore.canWrite("stations");
|
const canWriteStations = authStore.canWrite("stations");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStations = async () => {
|
const fetchStations = async () => {
|
||||||
@@ -121,21 +123,23 @@ export const StationListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredStations = () => {
|
const rows = useMemo(() => {
|
||||||
const { selectedCityId } = selectedCityStore;
|
const { selectedCityId } = selectedCityStore;
|
||||||
if (!selectedCityId) {
|
const query = searchQuery.trim().toLowerCase();
|
||||||
return stationLists[language].data;
|
return stationLists[language].data
|
||||||
}
|
.filter((station: any) => !selectedCityId || station.city_id === selectedCityId)
|
||||||
return stationLists[language].data.filter(
|
.filter(
|
||||||
(station: any) => station.city_id === selectedCityId
|
(station: any) =>
|
||||||
);
|
!query ||
|
||||||
};
|
(station.name ?? "").toLowerCase().includes(query) ||
|
||||||
|
(station.description ?? "").toLowerCase().includes(query)
|
||||||
const rows = filteredStations().map((station: any) => ({
|
)
|
||||||
id: station.id,
|
.map((station: any) => ({
|
||||||
name: station.name,
|
id: station.id,
|
||||||
description: station.description,
|
name: station.name,
|
||||||
}));
|
description: station.description,
|
||||||
|
}));
|
||||||
|
}, [stationLists[language].data, selectedCityStore.selectedCityId, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -161,6 +165,8 @@ export const StationListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, userStore } from "@shared";
|
import { authStore, userStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -21,6 +21,7 @@ export const UserListPage = observer(() => {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
});
|
});
|
||||||
const canWriteUsers = authStore.canWrite("users");
|
const canWriteUsers = authStore.canWrite("users");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
@@ -107,12 +108,21 @@ export const UserListPage = observer(() => {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = users.data?.map((user) => ({
|
const rows = useMemo(() => {
|
||||||
id: user.id,
|
const query = searchQuery.trim().toLowerCase();
|
||||||
email: user.email,
|
return (users.data ?? [])
|
||||||
is_admin: user.is_admin || (user.roles ?? []).includes("admin"),
|
.filter((user) =>
|
||||||
name: user.name,
|
!query ||
|
||||||
}));
|
(user.name ?? "").toLowerCase().includes(query) ||
|
||||||
|
(user.email ?? "").toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
.map((user) => ({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
is_admin: user.is_admin || (user.roles ?? []).includes("admin"),
|
||||||
|
name: user.name,
|
||||||
|
}));
|
||||||
|
}, [users.data, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -136,6 +146,8 @@ export const UserListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, carrierStore, languageStore, vehicleStore } from "@shared";
|
import { authStore, carrierStore, languageStore, vehicleStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
|
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -24,6 +24,7 @@ export const VehicleListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteVehicles = authStore.canWrite("devices");
|
const canWriteVehicles = authStore.canWrite("devices");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -136,27 +137,40 @@ export const VehicleListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = vehicles.data?.map((vehicle) => ({
|
const rows = useMemo(() => {
|
||||||
id: vehicle.vehicle.id,
|
const query = searchQuery.trim().toLowerCase();
|
||||||
tail_number: vehicle.vehicle.tail_number,
|
return (vehicles.data ?? [])
|
||||||
type: vehicle.vehicle.type,
|
.filter(
|
||||||
carrier: vehicle.vehicle.carrier,
|
(vehicle) =>
|
||||||
city: carriers[language].data?.find(
|
!query ||
|
||||||
(carrier) => carrier.id === vehicle.vehicle.carrier_id
|
(vehicle.vehicle.tail_number ?? "").toLowerCase().includes(query) ||
|
||||||
)?.city,
|
(vehicle.vehicle.carrier ?? "").toLowerCase().includes(query)
|
||||||
}));
|
)
|
||||||
|
.map((vehicle) => ({
|
||||||
|
id: vehicle.vehicle.id,
|
||||||
|
tail_number: vehicle.vehicle.tail_number,
|
||||||
|
type: vehicle.vehicle.type,
|
||||||
|
carrier: vehicle.vehicle.carrier,
|
||||||
|
city: carriers[language].data?.find(
|
||||||
|
(carrier) => carrier.id === vehicle.vehicle.carrier_id
|
||||||
|
)?.city,
|
||||||
|
}));
|
||||||
|
}, [vehicles.data, carriers[language].data, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Транспортные средства</h1>
|
<h1 className="text-2xl">Транспортные средства</h1>
|
||||||
|
|
||||||
<CreateButton
|
<CreateButton
|
||||||
label="Создать транспортное средство"
|
label="Создать транспортное средство"
|
||||||
path="/vehicle/create"
|
path="/vehicle/create"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
{canWriteVehicles && ids.length > 0 && (
|
{canWriteVehicles && ids.length > 0 && (
|
||||||
<div className="flex justify-end mb-5 duration-300">
|
<div className="flex justify-end mb-5 duration-300">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -193,11 +193,11 @@ class CarrierStore {
|
|||||||
const cityName = this.resolveCityName(this.createCarrierData.city_id, language);
|
const cityName = this.resolveCityName(this.createCarrierData.city_id, language);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
full_name: this.createCarrierData[language].full_name,
|
full_name: (this.createCarrierData[language].full_name || "").trim(),
|
||||||
short_name: this.createCarrierData[language].short_name,
|
short_name: (this.createCarrierData[language].short_name || "").trim(),
|
||||||
city: cityName,
|
city: cityName,
|
||||||
city_id: this.createCarrierData.city_id,
|
city_id: this.createCarrierData.city_id,
|
||||||
slogan: this.createCarrierData[language].slogan,
|
slogan: (this.createCarrierData[language].slogan || "").trim(),
|
||||||
...(this.createCarrierData.logo
|
...(this.createCarrierData.logo
|
||||||
? { logo: this.createCarrierData.logo }
|
? { logo: this.createCarrierData.logo }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -218,13 +218,13 @@ class CarrierStore {
|
|||||||
);
|
);
|
||||||
const patchPayload = {
|
const patchPayload = {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
full_name: this.createCarrierData[lang as any].full_name as string,
|
full_name: ((this.createCarrierData[lang as any].full_name as string) || "").trim(),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
short_name: this.createCarrierData[lang as any].short_name as string,
|
short_name: ((this.createCarrierData[lang as any].short_name as string) || "").trim(),
|
||||||
city: cityNameForLang || cityName,
|
city: cityNameForLang || cityName,
|
||||||
city_id: this.createCarrierData.city_id,
|
city_id: this.createCarrierData.city_id,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
slogan: this.createCarrierData[lang as any].slogan as string,
|
slogan: ((this.createCarrierData[lang as any].slogan as string) || "").trim(),
|
||||||
...(this.createCarrierData.logo
|
...(this.createCarrierData.logo
|
||||||
? { logo: this.createCarrierData.logo }
|
? { logo: this.createCarrierData.logo }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -321,6 +321,9 @@ class CarrierStore {
|
|||||||
const cityName = this.resolveCityName(this.editCarrierData.city_id, lang);
|
const cityName = this.resolveCityName(this.editCarrierData.city_id, lang);
|
||||||
const response = await languageInstance(lang).patch(`/carrier/${id}`, {
|
const response = await languageInstance(lang).patch(`/carrier/${id}`, {
|
||||||
...this.editCarrierData[lang],
|
...this.editCarrierData[lang],
|
||||||
|
full_name: (this.editCarrierData[lang].full_name || "").trim(),
|
||||||
|
short_name: (this.editCarrierData[lang].short_name || "").trim(),
|
||||||
|
slogan: (this.editCarrierData[lang].slogan || "").trim(),
|
||||||
city: cityName,
|
city: cityName,
|
||||||
city_id: this.editCarrierData.city_id,
|
city_id: this.editCarrierData.city_id,
|
||||||
...(this.editCarrierData.logo
|
...(this.editCarrierData.logo
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class CityStore {
|
|||||||
try {
|
try {
|
||||||
// Create city in primary language
|
// Create city in primary language
|
||||||
const cityPayload = {
|
const cityPayload = {
|
||||||
name,
|
name: name.trim(),
|
||||||
country:
|
country:
|
||||||
countryStore.countries[language as keyof CashedCountries]?.data.find(
|
countryStore.countries[language as keyof CashedCountries]?.data.find(
|
||||||
(c) => c.code === country_code
|
(c) => c.code === country_code
|
||||||
@@ -200,7 +200,7 @@ class CityStore {
|
|||||||
)?.name || "";
|
)?.name || "";
|
||||||
|
|
||||||
const patchPayload = {
|
const patchPayload = {
|
||||||
name: secondaryName || "",
|
name: (secondaryName || "").trim(),
|
||||||
country: countryName,
|
country: countryName,
|
||||||
country_code: country_code || "",
|
country_code: country_code || "",
|
||||||
...(arms ? { arms } : {}),
|
...(arms ? { arms } : {}),
|
||||||
@@ -285,7 +285,7 @@ class CityStore {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await languageInstance(language as Language).patch(`/city/${code}`, {
|
await languageInstance(language as Language).patch(`/city/${code}`, {
|
||||||
name,
|
name: (name || "").trim(),
|
||||||
country: country?.name || "",
|
country: country?.name || "",
|
||||||
country_code: country_code,
|
country_code: country_code,
|
||||||
arms,
|
arms,
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class CountryStore {
|
|||||||
if (code && this.createCountryData[language].name) {
|
if (code && this.createCountryData[language].name) {
|
||||||
await languageInstance(language as Language).post("/country", {
|
await languageInstance(language as Language).post("/country", {
|
||||||
code: code,
|
code: code,
|
||||||
name: name,
|
name: name.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@@ -156,7 +156,7 @@ class CountryStore {
|
|||||||
await languageInstance(secondaryLanguage as Language).patch(
|
await languageInstance(secondaryLanguage as Language).patch(
|
||||||
`/country/${code}`,
|
`/country/${code}`,
|
||||||
{
|
{
|
||||||
name: name,
|
name: name.trim(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ class CountryStore {
|
|||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
await languageInstance(language as Language).patch(`/country/${code}`, {
|
await languageInstance(language as Language).patch(`/country/${code}`, {
|
||||||
name: name,
|
name: name.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ class CreateSightStore {
|
|||||||
latitude: this.sight.latitude,
|
latitude: this.sight.latitude,
|
||||||
longitude: this.sight.longitude,
|
longitude: this.sight.longitude,
|
||||||
is_default_icon: this.sight.is_default_icon,
|
is_default_icon: this.sight.is_default_icon,
|
||||||
name: this.sight[primaryLanguage].name,
|
name: (this.sight[primaryLanguage].name || "").trim(),
|
||||||
address: this.sight[primaryLanguage].address,
|
address: this.sight[primaryLanguage].address,
|
||||||
thumbnail: this.sight.thumbnail,
|
thumbnail: this.sight.thumbnail,
|
||||||
icon: this.sight.icon,
|
icon: this.sight.icon,
|
||||||
@@ -521,7 +521,7 @@ class CreateSightStore {
|
|||||||
latitude: this.sight.latitude,
|
latitude: this.sight.latitude,
|
||||||
longitude: this.sight.longitude,
|
longitude: this.sight.longitude,
|
||||||
is_default_icon: this.sight.is_default_icon,
|
is_default_icon: this.sight.is_default_icon,
|
||||||
name: this.sight[lang].name,
|
name: (this.sight[lang].name || "").trim(),
|
||||||
address: this.sight[lang].address,
|
address: this.sight[lang].address,
|
||||||
thumbnail: this.sight.thumbnail,
|
thumbnail: this.sight.thumbnail,
|
||||||
icon: this.sight.icon,
|
icon: this.sight.icon,
|
||||||
|
|||||||
@@ -299,9 +299,9 @@ class EditSightStore {
|
|||||||
...this.sight.common,
|
...this.sight.common,
|
||||||
translations: {
|
translations: {
|
||||||
name: {
|
name: {
|
||||||
ru: this.sight.ru.name,
|
ru: (this.sight.ru.name || "").trim(),
|
||||||
en: this.sight.en.name,
|
en: (this.sight.en.name || "").trim(),
|
||||||
zh: this.sight.zh.name,
|
zh: (this.sight.zh.name || "").trim(),
|
||||||
},
|
},
|
||||||
address: {
|
address: {
|
||||||
ru: this.sight.ru.address,
|
ru: this.sight.ru.address,
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ class RouteStore {
|
|||||||
}
|
}
|
||||||
const dataToSend: any = {
|
const dataToSend: any = {
|
||||||
...this.editRouteData,
|
...this.editRouteData,
|
||||||
|
route_name: (this.editRouteData.route_name || "").trim(),
|
||||||
|
route_number: (this.editRouteData.route_number || "").trim(),
|
||||||
|
route_sys_number: (this.editRouteData.route_sys_number || "").trim(),
|
||||||
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
||||||
center_longitude: parseFloat(this.editRouteData.center_longitude),
|
center_longitude: parseFloat(this.editRouteData.center_longitude),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ class SnapshotStore {
|
|||||||
|
|
||||||
const response = await authInstance.post(
|
const response = await authInstance.post(
|
||||||
`/snapshots`,
|
`/snapshots`,
|
||||||
{ name },
|
{ name: name.trim() },
|
||||||
{ headers: { "X-Request-ID": this.lastRequestId } }
|
{ headers: { "X-Request-ID": this.lastRequestId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -287,10 +287,10 @@ class StationsStore {
|
|||||||
const response = await languageInstance(language).patch(
|
const response = await languageInstance(language).patch(
|
||||||
`/station/${id}`,
|
`/station/${id}`,
|
||||||
{
|
{
|
||||||
name: name || "",
|
name: (name || "").trim(),
|
||||||
system_name: name || "",
|
system_name: (name || "").trim(),
|
||||||
description: description || "",
|
description: (description || "").trim(),
|
||||||
address: address || "",
|
address: (address || "").trim(),
|
||||||
...commonDataPayload,
|
...commonDataPayload,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -415,10 +415,10 @@ class StationsStore {
|
|||||||
const { name, address } = this.createStationData[language];
|
const { name, address } = this.createStationData[language];
|
||||||
const description = this.createStationData.common.description;
|
const description = this.createStationData.common.description;
|
||||||
const response = await languageInstance(language).post("/station", {
|
const response = await languageInstance(language).post("/station", {
|
||||||
name: name || "",
|
name: (name || "").trim(),
|
||||||
system_name: name || "",
|
system_name: (name || "").trim(),
|
||||||
description: description || "",
|
description: (description || "").trim(),
|
||||||
address: address || "",
|
address: (address || "").trim(),
|
||||||
...commonDataPayload,
|
...commonDataPayload,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -436,10 +436,10 @@ class StationsStore {
|
|||||||
const response = await languageInstance(lang).patch(
|
const response = await languageInstance(lang).patch(
|
||||||
`/station/${stationId}`,
|
`/station/${stationId}`,
|
||||||
{
|
{
|
||||||
name: name || "",
|
name: (name || "").trim(),
|
||||||
system_name: name || "",
|
system_name: (name || "").trim(),
|
||||||
description: description || "",
|
description: (description || "").trim(),
|
||||||
address: address || "",
|
address: (address || "").trim(),
|
||||||
...commonDataPayload,
|
...commonDataPayload,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -92,7 +92,11 @@ class UserStore {
|
|||||||
if (this.users.data.length > 0) {
|
if (this.users.data.length > 0) {
|
||||||
id = this.users.data[this.users.data.length - 1].id + 1;
|
id = this.users.data[this.users.data.length - 1].id + 1;
|
||||||
}
|
}
|
||||||
const payload: Partial<User> = { ...this.createUserData };
|
const payload: Partial<User> = {
|
||||||
|
...this.createUserData,
|
||||||
|
name: (this.createUserData.name || "").trim(),
|
||||||
|
email: (this.createUserData.email || "").trim(),
|
||||||
|
};
|
||||||
const baseRoles = new Set<string>(payload.roles ?? []);
|
const baseRoles = new Set<string>(payload.roles ?? []);
|
||||||
baseRoles.add("articles_ro");
|
baseRoles.add("articles_ro");
|
||||||
baseRoles.add("articles_rw");
|
baseRoles.add("articles_rw");
|
||||||
@@ -141,7 +145,11 @@ class UserStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
editUser = async (id: number) => {
|
editUser = async (id: number) => {
|
||||||
const payload = { ...this.editUserData };
|
const payload = {
|
||||||
|
...this.editUserData,
|
||||||
|
name: (this.editUserData.name || "").trim(),
|
||||||
|
email: (this.editUserData.email || "").trim(),
|
||||||
|
};
|
||||||
if (!payload.icon) delete payload.icon;
|
if (!payload.icon) delete payload.icon;
|
||||||
if (!payload.password?.trim()) delete payload.password;
|
if (!payload.password?.trim()) delete payload.password;
|
||||||
|
|
||||||
|
|||||||
@@ -122,9 +122,9 @@ class VehicleStore {
|
|||||||
model?: string,
|
model?: string,
|
||||||
) => {
|
) => {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
tail_number: tailNumber,
|
tail_number: tailNumber.trim(),
|
||||||
type,
|
type,
|
||||||
carrier,
|
carrier: carrier.trim(),
|
||||||
carrier_id: carrierId,
|
carrier_id: carrierId,
|
||||||
};
|
};
|
||||||
// TODO: когда будет бекенд — добавить model в payload и в ответ
|
// TODO: когда будет бекенд — добавить model в payload и в ответ
|
||||||
@@ -182,9 +182,9 @@ class VehicleStore {
|
|||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
tail_number: data.tail_number,
|
tail_number: data.tail_number.trim(),
|
||||||
type: data.type,
|
type: data.type,
|
||||||
carrier: data.carrier,
|
carrier: data.carrier.trim(),
|
||||||
carrier_id: data.carrier_id,
|
carrier_id: data.carrier_id,
|
||||||
};
|
};
|
||||||
if (data.model != null && data.model !== "") payload.model = data.model;
|
if (data.model != null && data.model !== "") payload.model = data.model;
|
||||||
|
|||||||
37
src/shared/ui/SearchInput/index.tsx
Normal file
37
src/shared/ui/SearchInput/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Search, X } from "lucide-react";
|
||||||
|
|
||||||
|
interface SearchInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchInput = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "Поиск...",
|
||||||
|
}: SearchInputProps) => {
|
||||||
|
return (
|
||||||
|
<div className="relative mb-4 w-full max-w-sm">
|
||||||
|
<Search
|
||||||
|
size={16}
|
||||||
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="w-full pl-9 pr-8 py-2 border border-gray-300 rounded-md text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
/>
|
||||||
|
{value && (
|
||||||
|
<button
|
||||||
|
onClick={() => onChange("")}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,3 +5,4 @@ export * from "./CoordinatesInput";
|
|||||||
export * from "./AnimatedCircleButton";
|
export * from "./AnimatedCircleButton";
|
||||||
export * from "./LoadingSpinner";
|
export * from "./LoadingSpinner";
|
||||||
export * from "./MultiSelect";
|
export * from "./MultiSelect";
|
||||||
|
export * from "./SearchInput";
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user