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(() => { } /> -
+
{ }} />
+
- -
+ + +
+ Права доступа + + + + + + + + + + + Ресурс + Нет доступа + Чтение + Чтение/Запись + + Доп. права + + + + + {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="Техническое обслуживание (ТО)" + /> + + ) : ( + + - + + )} + + + ); + })} + +
+
+
+ + { 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 && ( - preview - )} - - {sight.common.watermark_rd && ( - preview - )} - - ) : ( - - )} - - - - 0 ? ( + + + {sight.common.watermark_lu && ( + preview + )} + {sight.common.watermark_rd && ( + preview + )} + + ) : ( + + + + )} + + + {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