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