Compare commits

5 Commits

31 changed files with 528 additions and 171 deletions

14
.env
View File

@@ -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'

View File

@@ -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}

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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":

View File

@@ -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())
); );
}; };

View File

@@ -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

View File

@@ -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,

View File

@@ -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}

View File

@@ -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,
] ]
); );

View File

@@ -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="Поворот (в градусах)"

View File

@@ -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;

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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(() => {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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),
}; };

View File

@@ -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 } }
); );

View File

@@ -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,
} }
); );

View File

@@ -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;

View File

@@ -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;

View 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>
);
};

View File

@@ -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