diff --git a/package.json b/package.json
index acf9d88..d44c7bb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "white-nights",
"private": true,
- "version": "1.0.6",
+ "version": "1.0.7",
"type": "module",
"license": "UNLICENSED",
"scripts": {
diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx
index 5b2a34c..a287045 100644
--- a/src/app/router/index.tsx
+++ b/src/app/router/index.tsx
@@ -92,6 +92,9 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
requiredPermissions.length > 0 &&
!requiredPermissions.every((permission) => authStore.canAccess(permission))
) {
+ if (location.pathname === "/devices" && authStore.hasRole("devices_maintenance_rw")) {
+ return <>{children}>;
+ }
return ;
}
diff --git a/src/client/src/api/ApiStore/store.ts b/src/client/src/api/ApiStore/store.ts
index ea21f60..8d2d1e4 100644
--- a/src/client/src/api/ApiStore/store.ts
+++ b/src/client/src/api/ApiStore/store.ts
@@ -24,6 +24,43 @@ import {
// @ts-ignore
import { orderStationsByRoute } from "../../utils/routeStationsUtils";
import { resamplePath } from "../../utils/animationUtils";
+import { colorStore } from "../../stores/ColorStore";
+
+function hexToRgbString(hex: string): string | null {
+ const clean = hex.trim().replace(/^#/, "");
+ const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
+ if (full.length !== 6) return null;
+ const r = parseInt(full.slice(0, 2), 16);
+ const g = parseInt(full.slice(2, 4), 16);
+ const b = parseInt(full.slice(4, 6), 16);
+ return `${r}, ${g}, ${b}`;
+}
+
+function darkenHex(hex: string, amount: number): string {
+ const rgb = hexToRgbString(hex);
+ if (!rgb) return hex;
+ const [r, g, b] = rgb.split(",").map(Number);
+ const factor = 1 - amount;
+ const dr = Math.round(r * factor);
+ const dg = Math.round(g * factor);
+ const db = Math.round(b * factor);
+ return `#${dr.toString(16).padStart(2, "0")}${dg.toString(16).padStart(2, "0")}${db.toString(16).padStart(2, "0")}`;
+}
+
+function applyCarrierColors(carrier: { main_color?: string; left_color?: string; right_color?: string }) {
+ const mainColor = carrier.main_color || "#006f3a";
+ const leftColor = carrier.left_color || "#006f3a";
+ const rightColor = carrier.right_color || "#006f3a";
+ const mainDark = darkenHex(mainColor, 0.3);
+
+ document.documentElement.style.setProperty("--carrier-main", mainColor);
+ document.documentElement.style.setProperty("--carrier-main-rgb", hexToRgbString(mainColor) ?? "0, 111, 58");
+ document.documentElement.style.setProperty("--carrier-main-dark", mainDark);
+ document.documentElement.style.setProperty("--carrier-left", leftColor);
+ document.documentElement.style.setProperty("--carrier-left-rgb", hexToRgbString(leftColor) ?? "0, 111, 58");
+ document.documentElement.style.setProperty("--carrier-right", rightColor);
+ document.documentElement.style.setProperty("--carrier-right-rgb", hexToRgbString(rightColor) ?? "0, 111, 58");
+}
class ApiStore {
isLoading = true;
@@ -115,6 +152,10 @@ class ApiStore {
getCarrier = async () => {
this.carrier = await getCarrier(this.route!.carrier_id!);
+ applyCarrierColors(this.carrier);
+ if (this.carrier.main_color) {
+ colorStore.setMainColor(this.carrier.main_color);
+ }
};
getCity = async () => {
diff --git a/src/client/src/api/ApiStore/types.ts b/src/client/src/api/ApiStore/types.ts
index 73e5b2b..4baab33 100644
--- a/src/client/src/api/ApiStore/types.ts
+++ b/src/client/src/api/ApiStore/types.ts
@@ -78,6 +78,9 @@ export type GetCarrierResponse = {
logo: string;
short_name: string;
slogan: string;
+ main_color?: string;
+ left_color?: string;
+ right_color?: string;
};
export type GetCityResponse = {
diff --git a/src/client/src/components/ListOfSights.jsx b/src/client/src/components/ListOfSights.jsx
index d5d17bb..9475468 100644
--- a/src/client/src/components/ListOfSights.jsx
+++ b/src/client/src/components/ListOfSights.jsx
@@ -417,7 +417,7 @@ const ListOfSights = observer(() => {
key={currentSelectedSight.id}
media={sightFrameMedia}
sight_id={currentSelectedSight.id}
- sight_name={currentSelectedSight.short_name || currentSelectedSight.name}
+ sight_name={currentSelectedSight.name}
selectedLanguageRight={selectedLanguageRight}
/>
)}
diff --git a/src/client/src/components/ListOfSights/SightFrame.jsx b/src/client/src/components/ListOfSights/SightFrame.jsx
index 2ae027e..f10a744 100644
--- a/src/client/src/components/ListOfSights/SightFrame.jsx
+++ b/src/client/src/components/ListOfSights/SightFrame.jsx
@@ -72,7 +72,14 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
idleSeconds = 0;
};
- const events = ["mousedown", "mousemove", "keypress", "scroll", "touchstart", "click"];
+ const events = [
+ "mousedown",
+ "mousemove",
+ "keypress",
+ "scroll",
+ "touchstart",
+ "click",
+ ];
events.forEach((event) => {
window.addEventListener(event, resetIdle, { passive: true });
});
@@ -208,7 +215,10 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
const introSection = {
id: media?.id || "intro-title",
heading:
- sight?.short_name || sight?.name || sight_name || "Название достопримечательности",
+ sight?.short_name ||
+ sight?.name ||
+ sight_name ||
+ "Название достопримечательности",
body: "",
};
const allSections = [introSection, ...rightArticles];
@@ -286,9 +296,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
alt=""
className={className}
onError={(e) => {
- console.warn(
- `Failed to load image: ${currentMediaData.path}`,
- );
+ console.warn(`Failed to load image: ${currentMediaData.path}`);
e.target.style.display = "none";
}}
/>
@@ -303,9 +311,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
playsInline
className={className}
onError={(e) => {
- console.warn(
- `Failed to load video: ${currentMediaData.path}`,
- );
+ console.warn(`Failed to load video: ${currentMediaData.path}`);
e.target.style.display = "none";
}}
>
@@ -356,9 +362,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
type="button"
className="three-d-control-btn"
title="Уменьшить"
- onPointerUp={() =>
- threeViewControlRef.current?.zoomOut?.()
- }
+ onPointerUp={() => threeViewControlRef.current?.zoomOut?.()}
>
@@ -366,9 +370,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
type="button"
className="three-d-control-btn"
title="Увеличить"
- onPointerUp={() =>
- threeViewControlRef.current?.zoomIn?.()
- }
+ onPointerUp={() => threeViewControlRef.current?.zoomIn?.()}
>
@@ -410,87 +412,100 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
/>
- {isFullscreen3D &&
+ {isFullscreen3D && (
- {[
- {
- label: "Вращать",
- icon:

,
- },
- {
- label: "Приблизить / Отдалить",
- icon:

,
- },
- {
- label: "Переместить",
- icon:

,
- },
- ].map((item, index, arr) => (
-
-
+ {[
+ {
+ label: "Вращать",
+ icon: (
+
+ ),
+ },
+ {
+ label: "Приблизить / Отдалить",
+ icon: (
+
+ ),
+ },
+ {
+ label: "Переместить",
+ icon: (
+
+ ),
+ },
+ ].map((item, index, arr) => (
+
- {item.icon}
-
-
- {item.label}
-
-
- ))}
+
+ {item.icon}
+
+
+ {item.label}
+
+
+ ))}
+
- }
+ )}
);
default:
@@ -647,7 +662,9 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
overflowWrap: "break-word",
}}
>
- {selectedSection === 0 ? processedSightName : (sightData?.short_name || sight_name)}
+ {selectedSection === 0
+ ? processedSightName
+ : sightData?.short_name || sight_name}
)}
@@ -679,9 +696,18 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
paddingBottom: "4.5px",
cursor: "pointer",
}}
- onPointerUp={() => { setSelectedSection(0); setIsFullscreen3D(false); }}
+ onPointerUp={() => {
+ setSelectedSection(0);
+ setIsFullscreen3D(false);
+ }}
>
-
+
)}
{contentError ? (
@@ -691,7 +717,10 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
articleSections.length > 1 &&
articleSections.slice(1).map((section, index) => (
{ setSelectedSection(index + 1); setIsFullscreen3D(false); }}
+ onPointerUp={() => {
+ setSelectedSection(index + 1);
+ setIsFullscreen3D(false);
+ }}
key={section.id || section.heading || index}
className={`sight-frame-menu-point ${
index + 1 === selectedSection ? "active" : ""
diff --git a/src/client/src/components/ListOfSights/TransferWidget.jsx b/src/client/src/components/ListOfSights/TransferWidget.jsx
index 9ed0b2b..1c0c355 100644
--- a/src/client/src/components/ListOfSights/TransferWidget.jsx
+++ b/src/client/src/components/ListOfSights/TransferWidget.jsx
@@ -95,21 +95,36 @@ const TransferWidget = observer(function TransferWidget({
}
const getTransferLabel = () => {
- if (selectedLanguageRight === "ru") {
- return stationName
- ? `Пересадки остановки ${stationName}:`
- : "Ближайшая остановка не обнаружена";
+ if (!stationName) {
+ if (selectedLanguageRight === "en") return "Nearest station not found";
+ if (selectedLanguageRight === "zh") return "最近的站点未找到";
+ return "Ближайшая остановка не обнаружена";
}
if (selectedLanguageRight === "en") {
- return stationName
- ? `Available transfers at station ${stationName}`
- : "Nearest station not found";
+ return (
+ <>
+ Transfer at stop
+ «{stationName}»:
+ >
+ );
}
- return stationName
- ? `在车站可用的换乘:${stationName}`
- : "最近的站点未找到";
+ if (selectedLanguageRight === "zh") {
+ return (
+ <>
+ 换乘站
+ «{stationName}»:
+ >
+ );
+ }
+
+ return (
+ <>
+ Пересадка на остановке
+ «{stationName}»:
+ >
+ );
};
const getNoTransfersMessage = () => {
diff --git a/src/client/src/components/ReactMarkdown/ReactMarkdown.css b/src/client/src/components/ReactMarkdown/ReactMarkdown.css
index 1647800..3f4e7a1 100644
--- a/src/client/src/components/ReactMarkdown/ReactMarkdown.css
+++ b/src/client/src/components/ReactMarkdown/ReactMarkdown.css
@@ -71,7 +71,7 @@
}
.react-markdown-container blockquote {
- border-left: 4px solid #006F3A;
+ border-left: 4px solid var(--carrier-main, #006F3A);
padding-left: 16px;
margin-top: 16px;
margin-bottom: 16px;
diff --git a/src/client/src/components/ThreeViewErrorBoundary.css b/src/client/src/components/ThreeViewErrorBoundary.css
index 717317a..9605852 100644
--- a/src/client/src/components/ThreeViewErrorBoundary.css
+++ b/src/client/src/components/ThreeViewErrorBoundary.css
@@ -17,7 +17,7 @@
114deg,
rgba(255, 255, 255, 0.1) 8.71%,
rgba(255, 255, 255, 0.05) 69.69%
- ), #006F3A;
+ ), var(--carrier-main, #006F3A);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
color: white;
diff --git a/src/client/src/components/map/WebGLMap.tsx b/src/client/src/components/map/WebGLMap.tsx
index 2c94600..cdd7276 100644
--- a/src/client/src/components/map/WebGLMap.tsx
+++ b/src/client/src/components/map/WebGLMap.tsx
@@ -881,25 +881,36 @@ export const WebGLMap = observer(() => {
// Сегментный индекс каждой упорядоченной станции на routePath (canvas-пространство)
const orderedStationSegs = useMemo(() => {
- if (!orderedRouteStations || !stationData || routePath.length < 4) return [] as number[];
+ if (!orderedRouteStations || !stationData || routePath.length < 4)
+ return [] as number[];
return (orderedRouteStations as any[]).map((ordStation) => {
- const stIdx = stationData.findIndex((s: any) => String(s.id) === String(ordStation.id));
+ const stIdx = stationData.findIndex(
+ (s: any) => String(s.id) === String(ordStation.id),
+ );
if (stIdx < 0) return -1;
const sx = stationPoints[stIdx * 2];
const sy = stationPoints[stIdx * 2 + 1];
if (sx === undefined || sy === undefined) return -1;
- let best = -1, bestD = Infinity;
+ let best = -1,
+ bestD = Infinity;
for (let i = 0; i < routePath.length - 2; i += 2) {
- const p1x = routePath[i], p1y = routePath[i + 1];
- const p2x = routePath[i + 2], p2y = routePath[i + 3];
- const dx = p2x - p1x, dy = p2y - p1y;
+ const p1x = routePath[i],
+ p1y = routePath[i + 1];
+ const p2x = routePath[i + 2],
+ p2y = routePath[i + 3];
+ const dx = p2x - p1x,
+ dy = p2y - p1y;
const len2 = dx * dx + dy * dy;
if (!len2) continue;
const t = ((sx - p1x) * dx + (sy - p1y) * dy) / len2;
const cl = Math.max(0, Math.min(1, t));
- const px = p1x + cl * dx, py = p1y + cl * dy;
+ const px = p1x + cl * dx,
+ py = p1y + cl * dy;
const d = Math.hypot(sx - px, sy - py);
- if (d < bestD) { bestD = d; best = i / 2; }
+ if (d < bestD) {
+ bestD = d;
+ best = i / 2;
+ }
}
return best;
});
@@ -1144,7 +1155,9 @@ export const WebGLMap = observer(() => {
const curIdx = apiStore.positionIndex;
const prevIdx = prevPositionIndexRef.current;
const pathLen = apiStore.route?.path?.length ?? 0;
- const isWrap = prevIdx >= 0 && pathLen > 0 &&
+ const isWrap =
+ prevIdx >= 0 &&
+ pathLen > 0 &&
Math.abs(curIdx - prevIdx) > pathLen / 4;
prevPositionIndexRef.current = curIdx;
@@ -1414,32 +1427,72 @@ export const WebGLMap = observer(() => {
gl.uniform1f(u_pointSize, pointInnerSizePx);
- if (tramSegIndex >= 0 && orderedRouteStations && stationData && orderedStationSegs.length > 0) {
+ if (
+ tramSegIndex >= 0 &&
+ orderedRouteStations &&
+ stationData &&
+ orderedStationSegs.length > 0
+ ) {
const passedPts1: number[] = [];
const unpassedPts1: number[] = [];
for (let i = 0; i < orderedRouteStations.length; i++) {
const orderedStation = (orderedRouteStations as any[])[i];
const stationSeg = orderedStationSegs[i] ?? -1;
if (!orderedStation || stationSeg < 0) continue;
- const isPassed = simulationDirection === 1 ? stationSeg < tramSegIndex : stationSeg > tramSegIndex;
- const stIdx = stationData.findIndex((s: any) => String(s.id) === String(orderedStation.id));
+ const isPassed =
+ simulationDirection === 1
+ ? stationSeg < tramSegIndex
+ : stationSeg > tramSegIndex;
+ const stIdx = stationData.findIndex(
+ (s: any) => String(s.id) === String(orderedStation.id),
+ );
if (stIdx < 0) continue;
const sx = stationPoints[stIdx * 2] as number;
const sy = stationPoints[stIdx * 2 + 1] as number;
- if (isPassed) { passedPts1.push(sx, sy); } else { unpassedPts1.push(sx, sy); }
+ if (isPassed) {
+ passedPts1.push(sx, sy);
+ } else {
+ unpassedPts1.push(sx, sy);
+ }
}
if (passedPts1.length > 0) {
- gl.uniform4f(u_color_pts, ((PATH_COLOR >> 16) & 0xff) / 255, ((PATH_COLOR >> 8) & 0xff) / 255, (PATH_COLOR & 0xff) / 255, 1);
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(passedPts1), gl.STATIC_DRAW);
+ gl.uniform4f(
+ u_color_pts,
+ ((PATH_COLOR >> 16) & 0xff) / 255,
+ ((PATH_COLOR >> 8) & 0xff) / 255,
+ (PATH_COLOR & 0xff) / 255,
+ 1,
+ );
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array(passedPts1),
+ gl.STATIC_DRAW,
+ );
gl.drawArrays(gl.POINTS, 0, passedPts1.length / 2);
}
if (unpassedPts1.length > 0) {
- gl.uniform4f(u_color_pts, ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255, ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255, (UNPASSED_STATION_COLOR & 0xff) / 255, 1);
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpassedPts1), gl.STATIC_DRAW);
+ gl.uniform4f(
+ u_color_pts,
+ ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255,
+ ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255,
+ (UNPASSED_STATION_COLOR & 0xff) / 255,
+ 1,
+ );
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array(unpassedPts1),
+ gl.STATIC_DRAW,
+ );
gl.drawArrays(gl.POINTS, 0, unpassedPts1.length / 2);
}
} else {
- gl.uniform4f(u_color_pts, ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255, ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255, (UNPASSED_STATION_COLOR & 0xff) / 255, 1);
+ gl.uniform4f(
+ u_color_pts,
+ ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255,
+ ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255,
+ (UNPASSED_STATION_COLOR & 0xff) / 255,
+ 1,
+ );
gl.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW);
gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);
}
@@ -1513,12 +1566,17 @@ export const WebGLMap = observer(() => {
const passedStationIds = new Set
();
const unpassedStationIds = new Set();
- if (tramSegIndex >= 0 && orderedRouteStations && orderedStationSegs.length === orderedRouteStations.length) {
+ if (
+ tramSegIndex >= 0 &&
+ orderedRouteStations &&
+ orderedStationSegs.length === orderedRouteStations.length
+ ) {
for (let i = 0; i < orderedRouteStations.length; i++) {
const station = (orderedRouteStations as any[])[i];
const seg = orderedStationSegs[i] ?? -1;
if (!station || seg < 0) continue;
- const isPassed = simulationDirection === 1 ? seg < tramSegIndex : seg > tramSegIndex;
+ const isPassed =
+ simulationDirection === 1 ? seg < tramSegIndex : seg > tramSegIndex;
if (isPassed) passedStationIds.add(String(station.id));
else unpassedStationIds.add(String(station.id));
}
@@ -1662,11 +1720,26 @@ export const WebGLMap = observer(() => {
const sin = Math.sin(rotationAngle);
const startStationData = orderedRouteStations?.[0]
- ? stationData.find((station: any) => station.id.toString() === String(orderedRouteStations[0].id))
- : stationData.find((station: any) => station.id.toString() === apiStore.context?.startStopId);
+ ? stationData.find(
+ (station: any) =>
+ station.id.toString() === String(orderedRouteStations[0].id),
+ )
+ : stationData.find(
+ (station: any) =>
+ station.id.toString() === apiStore.context?.startStopId,
+ );
const endStationData = orderedRouteStations?.length
- ? stationData.find((station: any) => station.id.toString() === String(orderedRouteStations[orderedRouteStations.length - 1].id))
- : stationData.find((station: any) => station.id.toString() === apiStore.context?.endStopId);
+ ? stationData.find(
+ (station: any) =>
+ station.id.toString() ===
+ String(
+ orderedRouteStations[orderedRouteStations.length - 1].id,
+ ),
+ )
+ : stationData.find(
+ (station: any) =>
+ station.id.toString() === apiStore.context?.endStopId,
+ );
const terminalStations: number[] = [];
@@ -1766,7 +1839,13 @@ export const WebGLMap = observer(() => {
}
return best;
})();
- return tramSegIndex !== -1 && seg !== -1 && (simulationDirection === 1 ? seg < tramSegIndex : seg > tramSegIndex);
+ return (
+ tramSegIndex !== -1 &&
+ seg !== -1 &&
+ (simulationDirection === 1
+ ? seg < tramSegIndex
+ : seg > tramSegIndex)
+ );
})()
: false;
@@ -1799,7 +1878,13 @@ export const WebGLMap = observer(() => {
}
return best;
})();
- return tramSegIndex !== -1 && seg !== -1 && (simulationDirection === 1 ? seg < tramSegIndex : seg > tramSegIndex);
+ return (
+ tramSegIndex !== -1 &&
+ seg !== -1 &&
+ (simulationDirection === 1
+ ? seg < tramSegIndex
+ : seg > tramSegIndex)
+ );
})()
: false;
@@ -1825,11 +1910,24 @@ export const WebGLMap = observer(() => {
const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
if (startStationData && endStationData) {
- const startIsPassed = simulationDirection === 1 ? true : isStartPassed;
+ const startIsPassed =
+ simulationDirection === 1 ? true : isStartPassed;
const endIsPassed = simulationDirection === -1 ? true : isEndPassed;
- gl.uniform4f(u_color_pts, startIsPassed ? r_passed : r_unpassed, startIsPassed ? g_passed : g_unpassed, startIsPassed ? b_passed : b_unpassed, 1.0);
+ gl.uniform4f(
+ u_color_pts,
+ startIsPassed ? r_passed : r_unpassed,
+ startIsPassed ? g_passed : g_unpassed,
+ startIsPassed ? b_passed : b_unpassed,
+ 1.0,
+ );
gl.drawArrays(gl.POINTS, 0, 1);
- gl.uniform4f(u_color_pts, endIsPassed ? r_passed : r_unpassed, endIsPassed ? g_passed : g_unpassed, endIsPassed ? b_passed : b_unpassed, 1.0);
+ gl.uniform4f(
+ u_color_pts,
+ endIsPassed ? r_passed : r_unpassed,
+ endIsPassed ? g_passed : g_unpassed,
+ endIsPassed ? b_passed : b_unpassed,
+ 1.0,
+ );
gl.drawArrays(gl.POINTS, 1, 1);
} else {
const isStartStation = startStationData !== undefined;
@@ -2529,7 +2627,7 @@ export const WebGLMap = observer(() => {
)
: false;
- const badgeColor = "#006F3A";
+ const badgeColor = "var(--carrier-main, #006F3A)";
const listPanelWidth = 200;
const listItemHeight = 30;
const listMaxHeight = 250;
@@ -2607,7 +2705,6 @@ export const WebGLMap = observer(() => {
{
e.stopPropagation()}
diff --git a/src/client/src/components/side-menu/LeftWidget.jsx b/src/client/src/components/side-menu/LeftWidget.jsx
index 454924a..5e2f243 100644
--- a/src/client/src/components/side-menu/LeftWidget.jsx
+++ b/src/client/src/components/side-menu/LeftWidget.jsx
@@ -120,8 +120,8 @@ const LeftWidget = observer(
selectedLanguage === "ru"
? routeSights.find((sight) => sight.id === selectedSightId)
: selectedLanguage === "en"
- ? routeSightsEn.find((sight) => sight.id === selectedSightId)
- : routeSightsZh.find((sight) => sight.id === selectedSightId);
+ ? routeSightsEn.find((sight) => sight.id === selectedSightId)
+ : routeSightsZh.find((sight) => sight.id === selectedSightId);
const leftArticle = sight.left_article;
@@ -129,18 +129,18 @@ const LeftWidget = observer(
selectedLanguage === "ru"
? sightArticles.get(leftArticle + "_" + selectedLanguage)
: selectedLanguage === "en"
- ? sightArticlesEn.get(leftArticle + "_" + selectedLanguage)
- : sightArticlesZh.get(leftArticle + "_" + selectedLanguage);
+ ? sightArticlesEn.get(leftArticle + "_" + selectedLanguage)
+ : sightArticlesZh.get(leftArticle + "_" + selectedLanguage);
const media = await ContentAPI.getMediaPreview(
leftArticleData.media[0].id,
- selectedLanguage
+ selectedLanguage,
);
const response = {
mediaPath: media.path,
mediaType: media.type,
- title: sight.short_name || sight.name || leftArticleData.heading,
+ title: leftArticleData.heading,
text: leftArticleData.body,
address: sight.address,
};
@@ -178,7 +178,7 @@ const LeftWidget = observer(
setIsImageLoaded(false);
console.error(
"Ошибка загрузки изображения для достопримечательности:",
- selectedSightId
+ selectedSightId,
);
if (isVisible) {
setTimeout(() => {
@@ -244,13 +244,13 @@ const LeftWidget = observer(
{selectedLanguage === "ru"
? "Выберите достопримечательность для просмотра деталей."
: selectedLanguage === "zh"
- ? "选择一个地标来查看详细信息。"
- : "Select a landmark to view details."}
+ ? "选择一个地标来查看详细信息。"
+ : "Select a landmark to view details."}
) : null}
);
- }
+ },
);
export default LeftWidget;
diff --git a/src/client/src/components/side-menu/SideMenu.jsx b/src/client/src/components/side-menu/SideMenu.jsx
index c92a564..fff6c01 100644
--- a/src/client/src/components/side-menu/SideMenu.jsx
+++ b/src/client/src/components/side-menu/SideMenu.jsx
@@ -358,7 +358,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
pointerEvents: "auto",
background:
isSightsOpen || isStationOpen
- ? `linear-gradient(to bottom right, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), rgba(76, 175, 75, 0.4)`
+ ? `linear-gradient(to bottom right, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), rgba(var(--carrier-left-rgb, 76, 175, 75), 0.4)`
: undefined,
backdropFilter:
isSightsOpen || isStationOpen ? "blur(10px)" : undefined,
@@ -492,7 +492,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
}, 300);
}
}}
- className={`side-menu-button side-menu-button--sights ${
+ className={`side-menu-button ${
isSightsOpen ? "side-menu-button--active" : ""
}`}
>
diff --git a/src/client/src/components/side-menu/StationsList.jsx b/src/client/src/components/side-menu/StationsList.jsx
index 4d4e709..f9955b3 100644
--- a/src/client/src/components/side-menu/StationsList.jsx
+++ b/src/client/src/components/side-menu/StationsList.jsx
@@ -11,6 +11,42 @@ import { apiStore } from "../../api/ApiStore/store";
import { useClickDetection } from "../../hooks/useClickDetection";
import { TouchableLayout } from "../TouchableLayout";
+const SightTransferItem = ({ name, style, onPointerUp }) => {
+ const containerRef = useRef(null);
+ const textRef = useRef(null);
+ const [shouldAnimate, setShouldAnimate] = useState(false);
+
+ useLayoutEffect(() => {
+ const checkWidth = () => {
+ if (containerRef.current && textRef.current) {
+ const containerWidth = containerRef.current.offsetWidth;
+ const textWidth = textRef.current.scrollWidth;
+ const shouldAnimateValue = textWidth > containerWidth;
+ setShouldAnimate(shouldAnimateValue);
+ if (shouldAnimateValue) {
+ containerRef.current.style.setProperty("--container-width", `${containerWidth}px`);
+ }
+ }
+ };
+ checkWidth();
+ window.addEventListener("resize", checkWidth);
+ return () => window.removeEventListener("resize", checkWidth);
+ }, [name]);
+
+ return (
+
+
+ {name}
+
+
+ );
+};
+
const StationItem = ({
station,
handlePointerDown,
@@ -101,9 +137,9 @@ const StationItem = ({
>
{sights.length > 0 ? (
sights.map((sight, index) => (
- {
e.stopPropagation();
if (onSightClick) {
- // Вычисляем позицию элемента для правильного позиционирования левого виджета
- const element = e.currentTarget;
- const elementRect = element.getBoundingClientRect();
-
- // Используем позицию элемента относительно viewport (elementRect.top)
- // чтобы верхняя граница виджета совпадала с верхней границей элемента
- const elementTop = elementRect.top;
- onSightClick(sight.id, elementTop);
+ const elementRect = e.currentTarget.getBoundingClientRect();
+ onSightClick(sight.id, elementRect.top);
}
}}
- >
- {getSightName(sight)}
-
+ />
))
) : (
diff --git a/src/client/src/stores/ColorStore.ts b/src/client/src/stores/ColorStore.ts
index f24723a..d1d8da8 100644
--- a/src/client/src/stores/ColorStore.ts
+++ b/src/client/src/stores/ColorStore.ts
@@ -1,21 +1,45 @@
import { makeAutoObservable, runInAction } from "mobx";
-const COLOR_WHITE = { h: 151, s: 0, l: 100 };
-const COLOR_GREEN = { h: 151, s: 100, l: 22 };
-
const TRANSITION_DURATION = 60000;
const TICK_INTERVAL = 100;
const TICK_STEP = TICK_INTERVAL / TRANSITION_DURATION;
+function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
+ const clean = hex.trim().replace(/^#/, "");
+ const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
+ if (full.length !== 6) return null;
+ return {
+ r: parseInt(full.slice(0, 2), 16),
+ g: parseInt(full.slice(2, 4), 16),
+ b: parseInt(full.slice(4, 6), 16),
+ };
+}
+
+function interpolateRgb(
+ from: { r: number; g: number; b: number },
+ to: { r: number; g: number; b: number },
+ t: number
+): string {
+ const r = Math.round(from.r + (to.r - from.r) * t);
+ const g = Math.round(from.g + (to.g - from.g) * t);
+ const b = Math.round(from.b + (to.b - from.b) * t);
+ return `rgb(${r}, ${g}, ${b})`;
+}
+
+const WHITE = { r: 255, g: 255, b: 255 };
+const DEFAULT_MAIN = { r: 0, g: 111, b: 58 };
+
interface ColorStore {
currentColor: string;
setCurrentColor: (color: string) => void;
+ setMainColor: (hex: string) => void;
startColorAnimation: () => void;
stopColorAnimation: () => void;
}
class ColorStore implements ColorStore {
currentColor: string = "#fff";
+ private mainColor: { r: number; g: number; b: number } = DEFAULT_MAIN;
private progress: number = 0;
private direction: number = 1;
private tickInterval: ReturnType
| null = null;
@@ -28,12 +52,12 @@ class ColorStore implements ColorStore {
this.currentColor = color;
};
- private interpolateColor(progress: number): string {
- const h = Math.round(COLOR_WHITE.h + (COLOR_GREEN.h - COLOR_WHITE.h) * progress);
- const s = Math.round(COLOR_WHITE.s + (COLOR_GREEN.s - COLOR_WHITE.s) * progress);
- const l = Math.round(COLOR_WHITE.l + (COLOR_GREEN.l - COLOR_WHITE.l) * progress);
- return `hsl(${h}, ${s}%, ${l}%)`;
- }
+ setMainColor = (hex: string) => {
+ const parsed = hexToRgb(hex);
+ if (parsed) {
+ this.mainColor = parsed;
+ }
+ };
startColorAnimation = () => {
if (this.tickInterval) return;
@@ -50,7 +74,7 @@ class ColorStore implements ColorStore {
this.direction = 1;
}
- this.currentColor = this.interpolateColor(this.progress);
+ this.currentColor = interpolateRgb(WHITE, this.mainColor, this.progress);
});
}, TICK_INTERVAL);
};
diff --git a/src/client/src/styles/AppealWidget.css b/src/client/src/styles/AppealWidget.css
index 4daf683..fd6bf47 100644
--- a/src/client/src/styles/AppealWidget.css
+++ b/src/client/src/styles/AppealWidget.css
@@ -11,7 +11,7 @@
rgba(255, 255, 255, 0) 8.71%,
rgba(255, 255, 255, 0.16) 69.69%
),
- #006F3A;
+ var(--carrier-main, #006F3A);
box-sizing: border-box;
}
diff --git a/src/client/src/styles/LeftWidget.css b/src/client/src/styles/LeftWidget.css
index 8b8d725..1cae8e3 100644
--- a/src/client/src/styles/LeftWidget.css
+++ b/src/client/src/styles/LeftWidget.css
@@ -14,7 +14,7 @@
rgba(255, 255, 255, 0) 8.71%,
rgba(255, 255, 255, 0.16) 69.69%
),
- #006F3A;
+ var(--carrier-left, #006F3A);
will-change: transform, opacity;
backface-visibility: hidden;
}
@@ -93,6 +93,16 @@
padding-left: 10px;
padding-bottom: 6px;
width: 100%;
+ overflow: hidden;
+}
+
+.side-menu-sight-transfer span {
+ display: inline-block;
+ white-space: nowrap;
+}
+
+.side-menu-sight-transfer span.marquee-text {
+ animation: side-menu-marquee 14s linear infinite;
}
/* Анимация для списка пересадок */
diff --git a/src/client/src/styles/ListOfSights.css b/src/client/src/styles/ListOfSights.css
index a36cfa3..9893ac4 100644
--- a/src/client/src/styles/ListOfSights.css
+++ b/src/client/src/styles/ListOfSights.css
@@ -17,7 +17,7 @@
rgba(255, 255, 255, 0) 8.71%,
rgba(255, 255, 255, 0.16) 69.69%
),
- #006f3a;
+ var(--carrier-right, #006f3a);
color: white;
max-height: 68px;
@@ -63,7 +63,11 @@
border-radius: 10px;
width: 128px;
- background-color: #0e8953;
+ background-color: color-mix(
+ in srgb,
+ var(--carrier-right, #006f3a) 80%,
+ black
+ );
}
.list-of-sights-title {
@@ -194,7 +198,7 @@
rgba(255, 255, 255, 0) 8.71%,
rgba(255, 255, 255, 0.16) 69.69%
),
- #006f3a;
+ var(--carrier-right, #006f3a);
max-height: calc(100vh - 128px);
}
@@ -237,7 +241,7 @@
rgba(255, 255, 255, 0.22) 0%,
rgba(255, 255, 255, 0.04) 100%
),
- rgba(0, 111, 58, 0.72);
+ rgba(var(--carrier-right-rgb, 0, 111, 58), 0.72);
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
box-sizing: border-box;
color: white;
@@ -304,7 +308,7 @@
background: linear-gradient(
to right,
transparent 35%,
- #0e8953 50%,
+ color-mix(in srgb, var(--carrier-right, #006f3a) 80%, black) 50%,
transparent 65%
);
border-radius: 3px;
@@ -341,7 +345,7 @@
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
- rgba(0, 111, 58, 0.4);
+ rgba(var(--carrier-right-rgb, 0, 111, 58), 0.4);
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
backdrop-filter: blur(10px);
box-sizing: border-box;
@@ -607,14 +611,14 @@
position: absolute;
border-radius: 10px;
- border: 1px solid #006f3a;
+ border: 1px solid var(--carrier-main, #006f3a);
background:
linear-gradient(
180deg,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
- rgba(0, 111, 58, 0.4);
+ rgba(var(--carrier-main-rgb, 0, 111, 58), 0.4);
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
backdrop-filter: blur(10px);
@@ -747,7 +751,7 @@
border-radius: 32px;
right: 20px;
bottom: 20px;
- background: #006f3a;
+ background: var(--carrier-right, #006f3a);
z-index: 9999;
display: flex;
}
diff --git a/src/client/src/styles/RouteWidget.css b/src/client/src/styles/RouteWidget.css
index 17e64e9..33ae546 100644
--- a/src/client/src/styles/RouteWidget.css
+++ b/src/client/src/styles/RouteWidget.css
@@ -34,7 +34,7 @@
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
- rgba(0, 111, 58, 0.4);
+ rgba(var(--carrier-main-rgb, 0, 111, 58), 0.4);
backdrop-filter: blur(10px);
pointer-events: auto;
z-index: 10000001;
diff --git a/src/client/src/styles/SideMenu.css b/src/client/src/styles/SideMenu.css
index 37f815f..ea3bc15 100644
--- a/src/client/src/styles/SideMenu.css
+++ b/src/client/src/styles/SideMenu.css
@@ -13,7 +13,7 @@
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
- #006f3a;
+ var(--carrier-left, #006f3a);
}
.side-menu-label {
@@ -51,10 +51,6 @@
border-radius: 10px;
}
-.side-menu-button--sights {
- background-color: #fcd500;
-}
-
.side-menu-button--active {
background-color: #fcd500;
color: #000;
@@ -138,10 +134,10 @@
}
3.33% {
- fill: rgb(76, 175, 75);
+ fill: var(--carrier-left, rgb(76, 175, 75));
}
50% {
- fill: rgb(76, 175, 75);
+ fill: var(--carrier-left, rgb(76, 175, 75));
}
53.33% {
fill: #ffffff;
@@ -191,7 +187,7 @@
top: -2px;
width: 100px;
height: 7px;
- background-color: #0e8953;
+ background-color: color-mix(in srgb, var(--carrier-left, #006f3a) 80%, black);
border-radius: 10px;
}
@@ -207,7 +203,7 @@
rgba(255, 255, 255, 0) 8.71%,
rgba(255, 255, 255, 0.16) 69.69%
),
- #006f3a;
+ var(--carrier-left, #006f3a);
position: absolute;
width: 288px;
transform: translateY(100%);
@@ -247,7 +243,8 @@
margin-right: 20px;
margin-bottom: 6px;
margin-top: 6px;
- border-bottom: 1px solid #0e8953;
+ border-bottom: 1px solid
+ color-mix(in srgb, var(--carrier-left, #006f3a) 80%, black);
font-family: "Roboto";
font-size: 16px;
font-weight: 300;
diff --git a/src/client/src/styles/WeatherWidget.css b/src/client/src/styles/WeatherWidget.css
index 87a8f0a..8e6536f 100644
--- a/src/client/src/styles/WeatherWidget.css
+++ b/src/client/src/styles/WeatherWidget.css
@@ -19,7 +19,7 @@
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
- rgba(0, 111, 58, 0.4);
+ rgba(179, 165, 152, 0.4);
}
.weather-widget-time {
diff --git a/src/features/navigation/ui/index.tsx b/src/features/navigation/ui/index.tsx
index de3ee52..2884819 100644
--- a/src/features/navigation/ui/index.tsx
+++ b/src/features/navigation/ui/index.tsx
@@ -17,6 +17,11 @@ const isItemVisible = (item: (typeof NAVIGATION_ITEMS.primary)[number]): boolean
);
}
+ // Пользователь с ролью ТО всегда видит раздел устройств
+ if (item.path === "/devices" && authStore.hasRole("devices_maintenance_rw")) {
+ return true;
+ }
+
const routePermissions = item.path ? ROUTE_REQUIRED_RESOURCES[item.path] ?? [] : [];
const canAccessRoute = routePermissions.every((permission) =>
authStore.canAccess(permission),
diff --git a/src/pages/Carrier/CarrierCreatePage/index.tsx b/src/pages/Carrier/CarrierCreatePage/index.tsx
index 40b1c47..ee1d3cf 100644
--- a/src/pages/Carrier/CarrierCreatePage/index.tsx
+++ b/src/pages/Carrier/CarrierCreatePage/index.tsx
@@ -6,8 +6,6 @@ import {
MenuItem,
FormControl,
InputLabel,
- ToggleButtonGroup,
- ToggleButton,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
@@ -29,13 +27,12 @@ import {
import { useState, useEffect } from "react";
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
-type ColorFields = { main_color: string; left_color: string; right_color: string; rgb_color: string };
+type ColorFields = { main_color: string; left_color: string; right_color: string };
const colorFields = (data: ColorFields) => ({
main_color: data.main_color,
left_color: data.left_color,
right_color: data.right_color,
- rgb_color: data.rgb_color,
});
const ColorPickerField = ({
@@ -81,7 +78,6 @@ const ColorPickerField = ({
);
export const CarrierCreatePage = observer(() => {
- const [colorMode, setColorMode] = useState<"rgb" | "three">("three");
const navigate = useNavigate();
const { createCarrierData, setCreateCarrierData } = carrierStore;
const { language } = languageStore;
@@ -274,51 +270,11 @@ export const CarrierCreatePage = observer(() => {
}
/>
-
-
-
- Режим цвета:
- {
- if (!val) return;
- setColorMode(val);
- if (val === "rgb") {
- setCreateCarrierData(
- createCarrierData[language].full_name,
- createCarrierData[language].short_name,
- createCarrierData.city_id,
- createCarrierData[language].slogan,
- selectedMediaId || "",
- language,
- { main_color: "", left_color: "", right_color: "", rgb_color: createCarrierData.rgb_color }
- );
- } else {
- setCreateCarrierData(
- createCarrierData[language].full_name,
- createCarrierData[language].short_name,
- createCarrierData.city_id,
- createCarrierData[language].slogan,
- selectedMediaId || "",
- language,
- { main_color: createCarrierData.main_color, left_color: createCarrierData.left_color, right_color: createCarrierData.right_color, rgb_color: "" }
- );
- }
- }}
- >
- Один цвет
- Три цвета
-
-
-
* при переключении цвет сбрасывается
-
-
- {colorMode === "rgb" ? (
+
+
setCreateCarrierData(
createCarrierData[language].full_name,
@@ -327,59 +283,54 @@ export const CarrierCreatePage = observer(() => {
createCarrierData[language].slogan,
selectedMediaId || "",
language,
- { ...colorFields(createCarrierData), rgb_color: val }
+ { ...colorFields(createCarrierData), main_color: val }
)
}
/>
- ) : (
- <>
-
- setCreateCarrierData(
- createCarrierData[language].full_name,
- createCarrierData[language].short_name,
- createCarrierData.city_id,
- createCarrierData[language].slogan,
- selectedMediaId || "",
- language,
- { ...colorFields(createCarrierData), main_color: val }
- )
- }
- />
-
- setCreateCarrierData(
- createCarrierData[language].full_name,
- createCarrierData[language].short_name,
- createCarrierData.city_id,
- createCarrierData[language].slogan,
- selectedMediaId || "",
- language,
- { ...colorFields(createCarrierData), left_color: val }
- )
- }
- />
-
- setCreateCarrierData(
- createCarrierData[language].full_name,
- createCarrierData[language].short_name,
- createCarrierData.city_id,
- createCarrierData[language].slogan,
- selectedMediaId || "",
- language,
- { ...colorFields(createCarrierData), right_color: val }
- )
- }
- />
- >
- )}
+
+ Используется в: виджет маршрута, виджет обращений, значки на карте, скопление достопримечательностей на карте, информационный виджет
+
+
+
+
+ setCreateCarrierData(
+ createCarrierData[language].full_name,
+ createCarrierData[language].short_name,
+ createCarrierData.city_id,
+ createCarrierData[language].slogan,
+ selectedMediaId || "",
+ language,
+ { ...colorFields(createCarrierData), left_color: val }
+ )
+ }
+ />
+
+ Используется в: боковое меню, левый виджет достопримечательности
+
+
+
+
+ setCreateCarrierData(
+ createCarrierData[language].full_name,
+ createCarrierData[language].short_name,
+ createCarrierData.city_id,
+ createCarrierData[language].slogan,
+ selectedMediaId || "",
+ language,
+ { ...colorFields(createCarrierData), right_color: val }
+ )
+ }
+ />
+
+ Используется в: список достопримечательностей, страница достопримечательности
+
+
diff --git a/src/pages/Carrier/CarrierEditPage/index.tsx b/src/pages/Carrier/CarrierEditPage/index.tsx
index 9217e35..05d232e 100644
--- a/src/pages/Carrier/CarrierEditPage/index.tsx
+++ b/src/pages/Carrier/CarrierEditPage/index.tsx
@@ -7,8 +7,6 @@ import {
FormControl,
InputLabel,
Box,
- ToggleButtonGroup,
- ToggleButton,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Save } from "lucide-react";
@@ -32,13 +30,12 @@ import {
UploadMediaDialog,
} from "@shared";
-type ColorFields = { main_color: string; left_color: string; right_color: string; rgb_color: string };
+type ColorFields = { main_color: string; left_color: string; right_color: string };
const colorFields = (data: ColorFields) => ({
main_color: data.main_color,
left_color: data.left_color,
right_color: data.right_color,
- rgb_color: data.rgb_color,
});
const ColorPickerField = ({
@@ -90,7 +87,6 @@ export const CarrierEditPage = observer(() => {
const { language } = languageStore;
const canReadCities = authStore.canRead("cities");
- const [colorMode, setColorMode] = useState<"rgb" | "three">("rgb");
const [isLoading, setIsLoading] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(true);
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
@@ -126,13 +122,7 @@ export const CarrierEditPage = observer(() => {
main_color: carrierData.ru?.main_color || "",
left_color: carrierData.ru?.left_color || "",
right_color: carrierData.ru?.right_color || "",
- rgb_color: carrierData.ru?.rgb_color || "",
};
- if (colors.rgb_color) {
- setColorMode("rgb");
- } else {
- setColorMode("three");
- }
setEditCarrierData(
carrierData.ru?.full_name || "",
carrierData.ru?.short_name || "",
@@ -339,51 +329,11 @@ export const CarrierEditPage = observer(() => {
}
/>
-
-
-
- Режим цвета:
- {
- if (!val) return;
- setColorMode(val);
- if (val === "rgb") {
- setEditCarrierData(
- editCarrierData[language].full_name,
- editCarrierData[language].short_name,
- editCarrierData.city_id,
- editCarrierData[language].slogan,
- editCarrierData.logo,
- language,
- { main_color: "", left_color: "", right_color: "", rgb_color: editCarrierData.rgb_color }
- );
- } else {
- setEditCarrierData(
- editCarrierData[language].full_name,
- editCarrierData[language].short_name,
- editCarrierData.city_id,
- editCarrierData[language].slogan,
- editCarrierData.logo,
- language,
- { main_color: editCarrierData.main_color, left_color: editCarrierData.left_color, right_color: editCarrierData.right_color, rgb_color: "" }
- );
- }
- }}
- >
- Один цвет
- Три цвета
-
-
-
* при переключении цвет сбрасывается
-
-
- {colorMode === "rgb" ? (
+
+
setEditCarrierData(
editCarrierData[language].full_name,
@@ -392,59 +342,54 @@ export const CarrierEditPage = observer(() => {
editCarrierData[language].slogan,
editCarrierData.logo,
language,
- { ...colorFields(editCarrierData), rgb_color: val }
+ { ...colorFields(editCarrierData), main_color: val }
)
}
/>
- ) : (
- <>
-
- setEditCarrierData(
- editCarrierData[language].full_name,
- editCarrierData[language].short_name,
- editCarrierData.city_id,
- editCarrierData[language].slogan,
- editCarrierData.logo,
- language,
- { ...colorFields(editCarrierData), main_color: val }
- )
- }
- />
-
- setEditCarrierData(
- editCarrierData[language].full_name,
- editCarrierData[language].short_name,
- editCarrierData.city_id,
- editCarrierData[language].slogan,
- editCarrierData.logo,
- language,
- { ...colorFields(editCarrierData), left_color: val }
- )
- }
- />
-
- setEditCarrierData(
- editCarrierData[language].full_name,
- editCarrierData[language].short_name,
- editCarrierData.city_id,
- editCarrierData[language].slogan,
- editCarrierData.logo,
- language,
- { ...colorFields(editCarrierData), right_color: val }
- )
- }
- />
- >
- )}
+
+ Используется в: виджет маршрута, виджет обращений, значки на карте, скопление достопримечательностей на карте, информационный виджет
+
+
+
+
+ setEditCarrierData(
+ editCarrierData[language].full_name,
+ editCarrierData[language].short_name,
+ editCarrierData.city_id,
+ editCarrierData[language].slogan,
+ editCarrierData.logo,
+ language,
+ { ...colorFields(editCarrierData), left_color: val }
+ )
+ }
+ />
+
+ Используется в: боковое меню, левый виджет достопримечательности
+
+
+
+
+ setEditCarrierData(
+ editCarrierData[language].full_name,
+ editCarrierData[language].short_name,
+ editCarrierData.city_id,
+ editCarrierData[language].slogan,
+ editCarrierData.logo,
+ language,
+ { ...colorFields(editCarrierData), right_color: val }
+ )
+ }
+ />
+
+ Используется в: список достопримечательностей, страница достопримечательности
+
+
diff --git a/src/pages/Route/RouteCreatePage/index.tsx b/src/pages/Route/RouteCreatePage/index.tsx
index 769a550..5f99872 100644
--- a/src/pages/Route/RouteCreatePage/index.tsx
+++ b/src/pages/Route/RouteCreatePage/index.tsx
@@ -57,7 +57,7 @@ export const RouteCreatePage = observer(() => {
const [turn, setTurn] = useState("");
const [centerLat, setCenterLat] = useState("");
const [centerLng, setCenterLng] = useState("");
- const [videoTimer, setVideoTimer] = useState(60);
+ const [videoTimer, setVideoTimer] = useState(420);
const [videoPreview, setVideoPreview] = useState("");
const [icon, setIcon] = useState("");
const [isLoading, setIsLoading] = useState(false);
diff --git a/src/pages/Route/RouteEditPage/index.tsx b/src/pages/Route/RouteEditPage/index.tsx
index c70c7ac..1c332ec 100644
--- a/src/pages/Route/RouteEditPage/index.tsx
+++ b/src/pages/Route/RouteEditPage/index.tsx
@@ -557,7 +557,7 @@ export const RouteEditPage = observer(() => {
className="w-full"
label="Таймер видео заставки (сек)"
type="number"
- value={editRouteData.video_timer ?? 60}
+ value={editRouteData.video_timer ?? 420}
onChange={(e) => {
const val = Math.max(1, Math.round(Number(e.target.value)));
if (Number.isFinite(val)) {
diff --git a/src/pages/Route/RouteListPage/index.tsx b/src/pages/Route/RouteListPage/index.tsx
index 9635780..712203f 100644
--- a/src/pages/Route/RouteListPage/index.tsx
+++ b/src/pages/Route/RouteListPage/index.tsx
@@ -139,7 +139,7 @@ export const RouteListPage = observer(() => {
headerAlign: "center" as const,
sortable: true,
renderHeader: (params: any) => (
-
+
{params.colDef.headerName}
),
@@ -157,7 +157,7 @@ export const RouteListPage = observer(() => {
headerAlign: "center" as const,
sortable: true,
renderHeader: (params: any) => (
-
+
{params.colDef.headerName}
),
diff --git a/src/pages/Route/route-preview/LeftSidebar.tsx b/src/pages/Route/route-preview/LeftSidebar.tsx
index 39677d9..926d9d2 100644
--- a/src/pages/Route/route-preview/LeftSidebar.tsx
+++ b/src/pages/Route/route-preview/LeftSidebar.tsx
@@ -1,4 +1,4 @@
-import { Box, Stack, Typography, Button } from "@mui/material";
+import { Button } from "@mui/material";
import { useNavigate, useNavigationType } from "react-router";
import { MediaViewer } from "@widgets";
import { useMapData } from "./MapDataContext";
@@ -15,22 +15,22 @@ type LeftSidebarProps = {
export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
const navigate = useNavigate();
- const navigationType = useNavigationType(); // PUSH, POP, REPLACE
+ const navigationType = useNavigationType();
const { routeData } = useMapData();
- const [carrierThumbnail, setCarrierThumbnail] = useState(null);
const [carrierLogo, setCarrierLogo] = useState(null);
+ const [carrierSlogan, setCarrierSlogan] = useState(null);
+ const [carrierShortName, setCarrierShortName] = useState(null);
+
useEffect(() => {
- async function fetchCarrierThumbnail() {
+ async function fetchCarrierData() {
if (routeData?.carrier_id) {
- const { city_id, logo } = (
- await authInstance.get(`/carrier/${routeData.carrier_id}`)
- ).data;
- const { arms } = (await authInstance.get(`/city/${city_id}`)).data;
- setCarrierThumbnail(arms);
- setCarrierLogo(logo);
+ const carrier = (await authInstance.get(`/carrier/${routeData.carrier_id}`)).data;
+ setCarrierLogo(carrier.logo);
+ setCarrierSlogan(carrier.slogan ?? null);
+ setCarrierShortName(carrier.short_name ?? null);
}
}
- fetchCarrierThumbnail();
+ fetchCarrierData();
}, [routeData?.carrier_id]);
const handleBack = () => {
@@ -42,131 +42,162 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
};
return (
-
-
+ }
+ >
+ Назад
+
+
+
+ {/* Основное меню — повторяет .side-menu */}
+
-
-
+
+ {/* Слоган — .side-menu-label */}
+ {carrierSlogan && (
+
}
>
- Назад
-
+ {carrierSlogan}
+
+ )}
-
+
-
- {carrierThumbnail && !isMediaIdEmpty(carrierThumbnail) && (
-
- )}
-
- При поддержке Правительства
-
-
-
-
-
-
-
-
+ Достопримечательности
+
+
+ Остановки
-
- {carrierLogo && !isMediaIdEmpty(carrierLogo) && (
-
- )}
-
+ {/* .side-menu-carrier-block */}
+
+ {carrierLogo && !isMediaIdEmpty(carrierLogo) && (
+
+
+
+ )}
+ {carrierShortName && (
+
+ {carrierShortName}
+
+ )}
+
-
- #ВсемПоПути
-
-
+ {/* .side-menu-bottom-photo */}
+

+
+
-
+
);
});
diff --git a/src/pages/Route/route-preview/webgl-prototype/RouteWidget.module.css b/src/pages/Route/route-preview/webgl-prototype/RouteWidget.module.css
index f690ad3..637f475 100644
--- a/src/pages/Route/route-preview/webgl-prototype/RouteWidget.module.css
+++ b/src/pages/Route/route-preview/webgl-prototype/RouteWidget.module.css
@@ -34,7 +34,7 @@
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
- rgba(179, 165, 152, 0.4);
+ rgba(var(--carrier-main-rgb, 0, 111, 58), 0.4);
backdrop-filter: blur(10px);
pointer-events: auto;
z-index: 10000001;
diff --git a/src/pages/Snapshot/SnapshotListPage/index.tsx b/src/pages/Snapshot/SnapshotListPage/index.tsx
index d0d5871..af305eb 100644
--- a/src/pages/Snapshot/SnapshotListPage/index.tsx
+++ b/src/pages/Snapshot/SnapshotListPage/index.tsx
@@ -76,6 +76,26 @@ export const SnapshotListPage = observer(() => {
};
const columns: GridColDef[] = [
+ {
+ field: "color",
+ headerName: "",
+ width: 28,
+ sortable: false,
+ disableColumnMenu: true,
+ renderCell: (params: GridRenderCellParams) => (
+
+
+
+ ),
+ },
{
field: "name",
headerName: "Название",
@@ -150,12 +170,13 @@ export const SnapshotListPage = observer(() => {
.toLowerCase()
.includes(query),
)
- .map((snapshot) => ({
+ .map((snapshot, index) => ({
id: snapshot.ID,
name: snapshot.Name,
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
created_at: formatCreationTime(snapshot.CreationTime),
occupied_disk_space_gb: snapshot.occupied_disk_space_gb,
+ color: SEGMENT_COLORS[index % SEGMENT_COLORS.length],
}));
}, [snapshots, searchQuery]);
@@ -181,7 +202,7 @@ export const SnapshotListPage = observer(() => {
setIsEmptySnapshotModalOpen(true);
}}
>
- Создать пустой снапшот
+ Создать пустой экспорт
)}
{canCreateSnapshot && (
@@ -203,7 +224,7 @@ export const SnapshotListPage = observer(() => {
- {rows.map((row, i) => {
+ {rows.map((row) => {
const pct =
row.occupied_disk_space_gb != null && totalGB > 0
? (row.occupied_disk_space_gb / totalGB) * 100
@@ -214,8 +235,7 @@ export const SnapshotListPage = observer(() => {
key={row.id}
style={{
width: `${pct}%`,
- backgroundColor:
- SEGMENT_COLORS[i % SEGMENT_COLORS.length],
+ backgroundColor: row.color,
}}
title={`${row.name}: ${row.occupied_disk_space_gb?.toFixed(1)} ГБ`}
/>
@@ -233,7 +253,7 @@ export const SnapshotListPage = observer(() => {
- {rows.map((row, i) => {
+ {rows.map((row) => {
if (row.occupied_disk_space_gb == null || row.occupied_disk_space_gb <= 0)
return null;
return (
@@ -243,10 +263,7 @@ export const SnapshotListPage = observer(() => {
>
{row.name}
@@ -325,7 +342,7 @@ export const SnapshotListPage = observer(() => {
fullWidth
maxWidth="xs"
>
- Создать пустой снапшот
+ Создать пустой экспорт
{
},
{
field: "sightCount",
- headerName: "Достопримечательности",
+ headerName: "Привязки",
width: 180,
align: "center" as const,
headerAlign: "center" as const,
sortable: true,
renderHeader: (params) => (
-
+
{params.colDef.headerName}
),
@@ -114,7 +114,7 @@ export const StationListPage = observer(() => {
headerAlign: "center" as const,
sortable: true,
renderHeader: (params) => (
-
+
{params.colDef.headerName}
),
diff --git a/src/pages/User/UserCreatePage/index.tsx b/src/pages/User/UserCreatePage/index.tsx
index e778016..721cfc5 100644
--- a/src/pages/User/UserCreatePage/index.tsx
+++ b/src/pages/User/UserCreatePage/index.tsx
@@ -1,4 +1,19 @@
-import { Button, Paper, TextField } from "@mui/material";
+import {
+ Button,
+ Paper,
+ TextField,
+ Checkbox,
+ Typography,
+ Box,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableRow,
+ Radio,
+ RadioGroup,
+ Divider,
+} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
import { useNavigate } from "react-router-dom";
@@ -14,6 +29,40 @@ import {
import { useState, useEffect } from "react";
import { ImageUploadCard } from "@widgets";
+const ROLE_RESOURCES = [
+ { key: "snapshot", label: "Экспорт" },
+ { key: "devices", label: "Устройства" },
+ { key: "vehicles", label: "Транспорт" },
+ { key: "users", label: "Пользователи" },
+ { key: "sights", label: "Достопримечательности" },
+ { key: "stations", label: "Остановки" },
+ { key: "routes", label: "Маршруты" },
+ { key: "countries", label: "Страны" },
+ { key: "cities", label: "Города" },
+ { key: "carriers", label: "Перевозчики" },
+] as const;
+
+type PermissionLevel = "none" | "ro" | "rw";
+
+function getPermissionLevel(roles: string[], resource: string): PermissionLevel {
+ if (roles.includes(`${resource}_rw`)) return "rw";
+ if (roles.includes(`${resource}_ro`)) return "ro";
+ return "none";
+}
+
+function applyPermissionChange(
+ roles: string[],
+ resource: string,
+ level: PermissionLevel,
+): string[] {
+ const filtered = roles.filter(
+ (r) => r !== `${resource}_ro` && r !== `${resource}_rw`,
+ );
+ if (level === "ro") return [...filtered, `${resource}_ro`];
+ if (level === "rw") return [...filtered, `${resource}_rw`];
+ return filtered;
+}
+
export const UserCreatePage = observer(() => {
const navigate = useNavigate();
const { createUserData, setCreateUserData, createUser } = userStore;
@@ -26,13 +75,33 @@ export const UserCreatePage = observer(() => {
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
+ const [localRoles, setLocalRoles] = useState(
+ createUserData.roles ?? ["articles_ro", "articles_rw", "media_ro", "media_rw"]
+ );
+
useEffect(() => {
mediaStore.getMedia();
}, []);
+ useEffect(() => {
+ const allRw = ROLE_RESOURCES.every(({ key }) => localRoles.includes(`${key}_rw`));
+ const isAdmin = allRw && !localRoles.includes("devices_maintenance_rw");
+ if (isAdmin !== createUserData.is_admin) {
+ setCreateUserData(
+ createUserData.name || "",
+ createUserData.email || "",
+ createUserData.password || "",
+ isAdmin,
+ createUserData.icon
+ );
+ }
+ }, [localRoles]);
+
const handleCreate = async () => {
try {
setIsLoading(true);
+ // Убеждаемся, что роли в сторе обновлены перед созданием
+ userStore.createUserData.roles = localRoles;
await createUser();
toast.success("Пользователь успешно создан");
navigate("/user");
@@ -67,18 +136,15 @@ export const UserCreatePage = observer(() => {
: selectedMedia?.id ?? createUserData.icon ?? null;
return (
-
-
-
-
+
+
+
+
+ Основные данные
-
{
label="Пароль"
value={createUserData.password || ""}
required
+ type="password"
onChange={(e) =>
setCreateUserData(
createUserData.name || "",
@@ -127,7 +194,7 @@ export const UserCreatePage = observer(() => {
}
/>
-
+
{
}}
/>
+
-
}
- onClick={handleCreate}
- disabled={
- isLoading || !createUserData.name || !createUserData.password
- }
- >
- {isLoading ? (
-
- ) : (
- "Создать"
- )}
-
-
+
+
+
+ Права доступа
+
+
+
+
+
+
+
+
+
+
+ Ресурс
+ Нет доступа
+ Чтение
+ Чтение/Запись
+
+ Доп. права
+
+
+
+
+ {ROLE_RESOURCES.map(({ key, label }) => {
+ const level = getPermissionLevel(localRoles, key);
+ const isSnapshotResource = key === "snapshot";
+
+ const handleChange = (val: string) => {
+ setLocalRoles((prev) => {
+ let updated = applyPermissionChange(prev, key, val as PermissionLevel);
+
+ if (key === "devices") {
+ updated = applyPermissionChange(
+ updated,
+ "vehicles",
+ val as PermissionLevel,
+ );
+ }
+
+ return updated;
+ });
+ };
+
+ const isDevicesResource = key === "devices";
+
+ const handleSnapshotCreateChange = (checked: boolean) => {
+ if (!isSnapshotResource) {
+ return;
+ }
+ setLocalRoles((prev) => {
+ const withoutSnapshotCreate = prev.filter(
+ (role) => role !== "snapshot_create"
+ );
+ return checked
+ ? [...withoutSnapshotCreate, "snapshot_create"]
+ : withoutSnapshotCreate;
+ });
+ };
+
+ const handleMaintenanceChange = (checked: boolean) => {
+ setLocalRoles((prev) => {
+ const without = prev.filter((r) => r !== "devices_maintenance_rw");
+ return checked ? [...without, "devices_maintenance_rw"] : without;
+ });
+ };
+
+ return (
+
+ {label}
+
+ handleChange(e.target.value)}
+ sx={{ justifyContent: "center", flexWrap: "nowrap" }}
+ >
+
+
+
+
+ {isSnapshotResource ? (
+
+ -
+
+ ) : (
+ handleChange(e.target.value)}
+ sx={{ justifyContent: "center", flexWrap: "nowrap" }}
+ >
+
+
+ )}
+
+
+ handleChange(e.target.value)}
+ sx={{ justifyContent: "center", flexWrap: "nowrap" }}
+ >
+
+
+
+
+ {isSnapshotResource ? (
+
+ handleSnapshotCreateChange(e.target.checked)
+ }
+ size="small"
+ title="Разрешает создавать новые снапшоты"
+ />
+ ) : isDevicesResource ? (
+
+ handleMaintenanceChange(e.target.checked)}
+ size="small"
+ title="Техническое обслуживание (ТО)"
+ />
+
+ ) : (
+
+ -
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+ }
+ onClick={handleCreate}
+ disabled={
+ isLoading || !createUserData.name || !createUserData.password || !createUserData.email
+ }
+ >
+ {isLoading ? (
+
+ ) : (
+ "Создать"
+ )}
+
{
languageStore.setLanguage("ru");
}, []);
+ useEffect(() => {
+ const allRw = ROLE_RESOURCES.every(({ key }) => localRoles.includes(`${key}_rw`));
+ const isAdmin = allRw && !localRoles.includes("devices_maintenance_rw");
+ if (isAdmin !== editUserData.is_admin) {
+ setEditUserData(
+ editUserData.name || "",
+ editUserData.email || "",
+ editUserData.password || "",
+ isAdmin,
+ editUserData.icon || ""
+ );
+ }
+ }, [localRoles]);
+
useEffect(() => {
(async () => {
if (id) {
@@ -311,35 +324,33 @@ export const UserEditPage = observer(() => {
Права доступа
- {
- if (e.target.checked) {
- setLocalRoles((prev) => {
- let next = prev.filter((r) => r !== "admin");
- for (const { key } of ROLE_RESOURCES) {
- next = next.filter((r) => r !== `${key}_ro` && r !== `${key}_rw`);
- next.push(`${key}_rw`);
- }
- if (!next.includes("snapshot_create")) {
- next.push("snapshot_create");
- }
- if (!next.includes("devices_maintenance_rw")) {
- next.push("devices_maintenance_rw");
- }
- next.push("admin");
- return next;
- });
- } else {
- setLocalRoles((prev) => prev.filter((r) => r !== "admin"));
- }
- }}
- />
- }
- label="Полный доступ (admin)"
- />
+
+
+
+
@@ -371,20 +382,6 @@ export const UserEditPage = observer(() => {
);
}
- const allRw = ROLE_RESOURCES.every(({ key: k }) =>
- updated.includes(`${k}_rw`),
- );
- if (allRw && !updated.includes("admin")) {
- const next = [...updated];
- if (!next.includes("snapshot_create")) {
- next.push("snapshot_create");
- }
- next.push("admin");
- return next;
- }
- if (!allRw) {
- return updated.filter((r) => r !== "admin");
- }
return updated;
});
};
@@ -462,12 +459,14 @@ export const UserEditPage = observer(() => {
title="Разрешает создавать новые снапшоты"
/>
) : isDevicesResource ? (
- handleMaintenanceChange(e.target.checked)}
- size="small"
- title="Разрешает переводить устройства в режим технического обслуживания"
- />
+
+ handleMaintenanceChange(e.target.checked)}
+ size="small"
+ title="Техническое обслуживание (ТО)"
+ />
+
) : (
-
diff --git a/src/shared/store/CarrierStore/index.tsx b/src/shared/store/CarrierStore/index.tsx
index db1898c..273a785 100644
--- a/src/shared/store/CarrierStore/index.tsx
+++ b/src/shared/store/CarrierStore/index.tsx
@@ -19,7 +19,6 @@ export type Carrier = {
main_color: string;
left_color: string;
right_color: string;
- rgb_color: string;
};
type CarrierData = {
@@ -116,7 +115,6 @@ class CarrierStore {
main_color: "",
left_color: "",
right_color: "",
- rgb_color: "",
ru: {
full_name: "",
short_name: "",
@@ -141,7 +139,7 @@ class CarrierStore {
slogan: string,
logoId: string,
language: Language,
- colors?: { main_color?: string; left_color?: string; right_color?: string; rgb_color?: string }
+ colors?: { main_color?: string; left_color?: string; right_color?: string }
) => {
this.createCarrierData.city_id = cityId;
this.createCarrierData.logo = logoId;
@@ -149,7 +147,6 @@ class CarrierStore {
if (colors.main_color !== undefined) this.createCarrierData.main_color = colors.main_color;
if (colors.left_color !== undefined) this.createCarrierData.left_color = colors.left_color;
if (colors.right_color !== undefined) this.createCarrierData.right_color = colors.right_color;
- if (colors.rgb_color !== undefined) this.createCarrierData.rgb_color = colors.rgb_color;
}
this.createCarrierData[language] = {
full_name: fullName,
@@ -211,7 +208,6 @@ class CarrierStore {
city_id: this.createCarrierData.city_id,
slogan: (this.createCarrierData[language].slogan || "").trim(),
...(this.createCarrierData.logo ? { logo: this.createCarrierData.logo } : {}),
- ...(this.createCarrierData.rgb_color ? { rgb_color: this.createCarrierData.rgb_color } : {}),
...(this.createCarrierData.main_color ? { main_color: this.createCarrierData.main_color } : {}),
...(this.createCarrierData.left_color ? { left_color: this.createCarrierData.left_color } : {}),
...(this.createCarrierData.right_color ? { right_color: this.createCarrierData.right_color } : {}),
@@ -260,7 +256,6 @@ class CarrierStore {
main_color: "",
left_color: "",
right_color: "",
- rgb_color: "",
ru: {
full_name: "",
short_name: "",
@@ -300,7 +295,6 @@ class CarrierStore {
main_color: "",
left_color: "",
right_color: "",
- rgb_color: "",
};
setEditCarrierData = (
@@ -310,7 +304,7 @@ class CarrierStore {
slogan: string,
logoId: string,
language: Language,
- colors?: { main_color?: string; left_color?: string; right_color?: string; rgb_color?: string }
+ colors?: { main_color?: string; left_color?: string; right_color?: string }
) => {
this.editCarrierData.city_id = cityId;
this.editCarrierData.logo = logoId;
@@ -318,7 +312,6 @@ class CarrierStore {
if (colors.main_color !== undefined) this.editCarrierData.main_color = colors.main_color;
if (colors.left_color !== undefined) this.editCarrierData.left_color = colors.left_color;
if (colors.right_color !== undefined) this.editCarrierData.right_color = colors.right_color;
- if (colors.rgb_color !== undefined) this.editCarrierData.rgb_color = colors.rgb_color;
}
this.editCarrierData[language] = {
full_name: fullName,
@@ -338,7 +331,6 @@ class CarrierStore {
city: cityName,
city_id: this.editCarrierData.city_id,
...(this.editCarrierData.logo ? { logo: this.editCarrierData.logo } : {}),
- ...(this.editCarrierData.rgb_color ? { rgb_color: this.editCarrierData.rgb_color } : {}),
...(this.editCarrierData.main_color ? { main_color: this.editCarrierData.main_color } : {}),
...(this.editCarrierData.left_color ? { left_color: this.editCarrierData.left_color } : {}),
...(this.editCarrierData.right_color ? { right_color: this.editCarrierData.right_color } : {}),
diff --git a/src/shared/store/RouteStore/index.ts b/src/shared/store/RouteStore/index.ts
index 7825706..9061b44 100644
--- a/src/shared/store/RouteStore/index.ts
+++ b/src/shared/store/RouteStore/index.ts
@@ -153,7 +153,7 @@ class RouteStore {
scale_max: 0,
scale_min: 0,
video_preview: "" as string | undefined,
- video_timer: 60,
+ video_timer: 420,
};
setEditRouteData = (data: any) => {
diff --git a/src/widgets/DevicesTable/index.tsx b/src/widgets/DevicesTable/index.tsx
index 93bc46f..b2c418b 100644
--- a/src/widgets/DevicesTable/index.tsx
+++ b/src/widgets/DevicesTable/index.tsx
@@ -628,41 +628,57 @@ export const DevicesTable = observer(() => {
justifyContent: "center",
}}
>
- {canWriteDevices && (
-
+ {!isMaintenanceOnly && (
+ <>
+ {canWriteDevices && (
+
+ )}
+
+
+
+ >
)}
-
-
-
);
},
@@ -714,9 +718,11 @@ export const DevicesTable = observer(() => {
const visibleColumns = useMemo(() => {
if (isMaintenanceOnly) {
- return columns.filter((c) =>
- ["model", "tail_number", "maintenance_mode_on"].includes(c.field),
- );
+ return columns
+ .filter((c) =>
+ ["model", "tail_number", "maintenance_mode_on", "actions"].includes(c.field),
+ )
+ .map((c) => ({ ...c, flex: 1, width: undefined, minWidth: undefined }));
}
if (!canWriteDevices) {
return columns.filter(
@@ -729,20 +735,26 @@ export const DevicesTable = observer(() => {
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
- await Promise.all([
- getVehicles(),
- getDevices(),
- getSnapshots(),
- getRoutes(),
- ]);
+ if (isMaintenanceOnly) {
+ await Promise.all([getVehicles(), getDevices()]);
+ } else {
+ await Promise.all([
+ getVehicles(),
+ getDevices(),
+ getSnapshots(),
+ getRoutes(),
+ ]);
+ }
setIsLoading(false);
};
fetchData();
- }, [getDevices, getSnapshots, getVehicles, getRoutes]);
+ }, [getDevices, getSnapshots, getVehicles, getRoutes, isMaintenanceOnly]);
useEffect(() => {
- carrierStore.getCarriers("ru");
- }, []);
+ if (!isMaintenanceOnly) {
+ carrierStore.getCarriers("ru");
+ }
+ }, [isMaintenanceOnly]);
const handleOpenSendSnapshotModal = () => {
if (!canWriteDevices) {
diff --git a/src/widgets/ReactMarkdownEditor/index.tsx b/src/widgets/ReactMarkdownEditor/index.tsx
index d758a5b..694c721 100644
--- a/src/widgets/ReactMarkdownEditor/index.tsx
+++ b/src/widgets/ReactMarkdownEditor/index.tsx
@@ -120,7 +120,6 @@ export const ReactMarkdownEditor = ({
"table",
"horizontal-rule",
"preview",
- "fullscreen",
"guide",
],
};
diff --git a/src/widgets/SightTabs/LeftWidgetTab/index.tsx b/src/widgets/SightTabs/LeftWidgetTab/index.tsx
index 65c67bf..eb23932 100644
--- a/src/widgets/SightTabs/LeftWidgetTab/index.tsx
+++ b/src/widgets/SightTabs/LeftWidgetTab/index.tsx
@@ -243,149 +243,123 @@ export const LeftWidgetTab = observer(
flex: 1,
display: "flex",
flexDirection: "column",
- maxWidth: "320px",
+ maxWidth: "316px",
gap: 0.5,
}}
>
-
-
- {data.left.media.length > 0 ? (
- <>
-
- {sight.common.watermark_lu && (
-
- )}
-
- {sight.common.watermark_rd && (
-
- )}
- >
- ) : (
-
- )}
-
-
-
- 0 ? (
+
+
+ {sight.common.watermark_lu && (
+
+ )}
+ {sight.common.watermark_rd && (
+
+ )}
+
+ ) : (
+
+
+
+ )}
+
+
+
{data?.left?.heading || "Название информации"}
-
-
+
{sight[language as Language].address}
-
-
-
- {data?.left?.body && (
-
-
- )}
-
+
+ {data?.left?.body && (
+
+
+
+ )}
+
+
)}
diff --git a/src/widgets/SightTabs/RightWidgetTab/SightFramePreview.css b/src/widgets/SightTabs/RightWidgetTab/SightFramePreview.css
index 3db37c5..0722f7d 100644
--- a/src/widgets/SightTabs/RightWidgetTab/SightFramePreview.css
+++ b/src/widgets/SightTabs/RightWidgetTab/SightFramePreview.css
@@ -12,7 +12,7 @@
rgba(255, 255, 255, 0) 8.71%,
rgba(255, 255, 255, 0.16) 69.69%
),
- #806c59;
+ #006f3a;
}
.sfp-sight-frame-media-stack {
@@ -22,7 +22,6 @@
width: calc(100% - 4px);
height: 300px;
overflow: hidden;
- background: #111;
}
.sfp-sight-frame-media-item {
@@ -67,21 +66,21 @@
.sfp-sight-frame-title {
display: flex;
align-items: center;
- padding: 7px 16px;
+ padding: 10px 20px;
width: 100%;
text-align: left;
font-family: "Roboto";
font-size: 24px;
font-weight: 600;
line-height: 120%;
- border-bottom: 1px solid rgba(255, 255, 255, 0.8);
+ border-bottom: 1px solid var(--Glass-stroke, rgba(255, 255, 255, 0.8));
background:
linear-gradient(
180deg,
rgba(255, 255, 255, 0.22) 0%,
rgba(255, 255, 255, 0.04) 100%
),
- rgba(179, 165, 152, 0.72);
+ rgba(0, 111, 58, 0.72);
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
box-sizing: border-box;
color: white;
@@ -118,7 +117,7 @@
padding: 16px;
box-sizing: border-box;
max-height: calc(80vh - 354px);
- min-height: 80px;
+ min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
@@ -156,6 +155,9 @@
.sfp-sight-frame-text h3,
.sfp-sight-frame-text li {
color: #fff;
+ font-size: 18px;
+ line-height: 150%;
+ font-family: "Roboto";
}
.sfp-sight-frame-menu {
@@ -173,7 +175,7 @@
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
- rgba(179, 165, 152, 0.4);
+ rgba(0, 111, 58, 0.4);
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
backdrop-filter: blur(10px);
box-sizing: border-box;
@@ -265,3 +267,4 @@
.sfp-sight-frame-media-stack.three-d-view {
background-color: #111 !important;
}
+
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo
index f5b2933..edd324a 100644
--- a/tsconfig.tsbuildinfo
+++ b/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/client/src/app.d.ts","./src/client/src/api/apiconfig.d.ts","./src/client/src/api/apistore/api.ts","./src/client/src/api/apistore/index.ts","./src/client/src/api/apistore/store.ts","./src/client/src/api/apistore/types.ts","./src/client/src/assets/constants.d.ts","./src/client/src/components/overlayscrollbarswrapper.d.ts","./src/client/src/components/simulationsettings.tsx","./src/client/src/components/threeviewerrorboundary.tsx","./src/client/src/components/reactmarkdown/index.tsx","./src/client/src/components/touchablelayout/index.tsx","./src/client/src/components/map/constants.tsx","./src/client/src/components/map/infinitecanvas.tsx","./src/client/src/components/map/map.tsx","./src/client/src/components/map/mapdatacontext.tsx","./src/client/src/components/map/sight.tsx","./src/client/src/components/map/station.tsx","./src/client/src/components/map/tramicon.tsx","./src/client/src/components/map/tramiconwebgl.tsx","./src/client/src/components/map/travelpath.tsx","./src/client/src/components/map/webglmap.tsx","./src/client/src/components/map/custom.d.ts","./src/client/src/components/map/transformcontext.tsx","./src/client/src/components/map/types.tsx","./src/client/src/components/map/utils.tsx","./src/client/src/components/widgets/panoramview.tsx","./src/client/src/components/widgets/threeview.tsx","./src/client/src/components/widgets/threeviewicons.tsx","./src/client/src/context/geolocationcontext.tsx","./src/client/src/hooks/useanimatedposition.ts","./src/client/src/hooks/useroutefollowingposition.ts","./src/client/src/stores/cameraanimationstore.ts","./src/client/src/stores/colorstore.ts","./src/client/src/stores/geolocationstore.ts","./src/client/src/stores/index.ts","./src/client/src/stores/hooks/usecameraanimationstore.ts","./src/client/src/stores/hooks/usecolorstore.ts","./src/client/src/stores/hooks/usegeolocationstore.ts","./src/client/src/utils/routepathanimator.ts","./src/client/src/utils/animationutils.ts","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/demopage/index.tsx","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/route/route-preview/web-gl/languageselector.tsx","./src/pages/route/route-preview/webgl-prototype/routewidget.tsx","./src/pages/route/route-preview/webgl-prototype/webglroutemapprototype.tsx","./src/pages/sight/linkedstations.tsx","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/api/mobxfetch/index.ts","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/gltfcachemanager.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/lib/permissions/index.ts","./src/shared/modals/index.ts","./src/shared/modals/articleselectorcreatedialog/index.tsx","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/api.ts","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/modelloadingstore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/testingmodestore/api.ts","./src/shared/store/testingmodestore/index.ts","./src/shared/store/userstore/api.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/api.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/store/vehiclestore/types.ts","./src/shared/ui/animatedcirclebutton.tsx","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/loadingspinner/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/modelloadingindicator/index.tsx","./src/shared/ui/multiselect/index.tsx","./src/shared/ui/searchinput/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/devicelogsmodal.tsx","./src/widgets/devicestable/vehiclesessionsmodal.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/sightframepreview.tsx","./src/widgets/sighttabs/rightwidgettab/sightframethreeview.tsx","./src/widgets/sighttabs/rightwidgettab/sightframethreeviewicons.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/testingmodebanner/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/editstationtransfersmodal/index.tsx","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"}
\ No newline at end of file
+{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/client/src/app.d.ts","./src/client/src/api/apiconfig.d.ts","./src/client/src/api/apistore/api.ts","./src/client/src/api/apistore/index.ts","./src/client/src/api/apistore/store.ts","./src/client/src/api/apistore/types.ts","./src/client/src/assets/constants.d.ts","./src/client/src/components/overlayscrollbarswrapper.d.ts","./src/client/src/components/simulationsettings.tsx","./src/client/src/components/threeviewerrorboundary.tsx","./src/client/src/components/reactmarkdown/index.tsx","./src/client/src/components/touchablelayout/index.tsx","./src/client/src/components/map/constants.tsx","./src/client/src/components/map/infinitecanvas.tsx","./src/client/src/components/map/map.tsx","./src/client/src/components/map/mapdatacontext.tsx","./src/client/src/components/map/sight.tsx","./src/client/src/components/map/station.tsx","./src/client/src/components/map/tramicon.tsx","./src/client/src/components/map/tramiconwebgl.tsx","./src/client/src/components/map/travelpath.tsx","./src/client/src/components/map/webglmap.tsx","./src/client/src/components/map/custom.d.ts","./src/client/src/components/map/transformcontext.tsx","./src/client/src/components/map/types.tsx","./src/client/src/components/map/utils.tsx","./src/client/src/components/widgets/panoramview.tsx","./src/client/src/components/widgets/threeview.tsx","./src/client/src/components/widgets/threeviewicons.tsx","./src/client/src/context/geolocationcontext.tsx","./src/client/src/hooks/useanimatedposition.ts","./src/client/src/hooks/useroutefollowingposition.ts","./src/client/src/stores/cameraanimationstore.ts","./src/client/src/stores/colorstore.ts","./src/client/src/stores/geolocationstore.ts","./src/client/src/stores/index.ts","./src/client/src/stores/hooks/usecameraanimationstore.ts","./src/client/src/stores/hooks/usecolorstore.ts","./src/client/src/stores/hooks/usegeolocationstore.ts","./src/client/src/utils/routepathanimator.ts","./src/client/src/utils/animationutils.ts","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/demopage/index.tsx","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/route/route-preview/web-gl/languageselector.tsx","./src/pages/route/route-preview/webgl-prototype/routewidget.tsx","./src/pages/route/route-preview/webgl-prototype/webglroutemapprototype.tsx","./src/pages/sight/linkedstations.tsx","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/api/mobxfetch/index.ts","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/gltfcachemanager.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/lib/permissions/index.ts","./src/shared/modals/index.ts","./src/shared/modals/articleselectorcreatedialog/index.tsx","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/api.ts","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/modelloadingstore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/testingmodestore/api.ts","./src/shared/store/testingmodestore/index.ts","./src/shared/store/userstore/api.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/api.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/store/vehiclestore/types.ts","./src/shared/ui/animatedcirclebutton.tsx","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/loadingspinner/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/modelloadingindicator/index.tsx","./src/shared/ui/multiselect/index.tsx","./src/shared/ui/searchinput/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/devicelogsmodal.tsx","./src/widgets/devicestable/vehiclesessionsmodal.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/sightframepreview.tsx","./src/widgets/sighttabs/rightwidgettab/sightframethreeview.tsx","./src/widgets/sighttabs/rightwidgettab/sightframethreeviewicons.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/testingmodebanner/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/editstationtransfersmodal/index.tsx","./src/widgets/modals/selectarticledialog/index.tsx"],"errors":true,"version":"5.8.3"}
\ No newline at end of file