Compare commits
16 Commits
e3469763ce
...
feature/we
| Author | SHA1 | Date | |
|---|---|---|---|
| cc38f2e66c | |||
| d4c5db61ea | |||
| 55cdea17ea | |||
| 3725c7f569 | |||
| 2659c6a5b8 | |||
| 1bb3f43979 | |||
| 7e539f550b | |||
| fbf6b0dc9d | |||
| a997cdb198 | |||
| bf45dcdbfc | |||
| 83ccdef790 | |||
| 51d1870198 | |||
| 193f53c029 | |||
| 4bda233b63 | |||
| d758dbffa6 | |||
| 6af95bb449 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "white-nights",
|
"name": "white-nights",
|
||||||
"version": "1.0.6",
|
"version": "1.0.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "white-nights",
|
"name": "white-nights",
|
||||||
"version": "1.0.6",
|
"version": "1.0.8",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "white-nights",
|
"name": "white-nights",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.6",
|
"version": "1.0.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Router } from "./router";
|
import { Router } from "./router";
|
||||||
import { CustomTheme } from "@shared";
|
import { CustomTheme, languageStore } from "@shared";
|
||||||
import { ThemeProvider } from "@mui/material/styles";
|
import { ThemeProvider } from "@mui/material/styles";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { GlobalErrorBoundary } from "./GlobalErrorBoundary";
|
import { GlobalErrorBoundary } from "./GlobalErrorBoundary";
|
||||||
import { TestingModeBanner } from "@widgets";
|
import { TestingModeBanner } from "@widgets";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
export const App: React.FC = () => (
|
export const App: React.FC = observer(() => {
|
||||||
|
React.useEffect(() => {
|
||||||
|
document.documentElement.lang = languageStore.language;
|
||||||
|
}, [languageStore.language]);
|
||||||
|
|
||||||
|
return (
|
||||||
<GlobalErrorBoundary>
|
<GlobalErrorBoundary>
|
||||||
<ThemeProvider theme={CustomTheme.Light}>
|
<ThemeProvider theme={CustomTheme.Light}>
|
||||||
<TestingModeBanner />
|
<TestingModeBanner />
|
||||||
@@ -15,4 +21,5 @@ export const App: React.FC = () => (
|
|||||||
<Router />
|
<Router />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</GlobalErrorBoundary>
|
</GlobalErrorBoundary>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
|||||||
requiredPermissions.length > 0 &&
|
requiredPermissions.length > 0 &&
|
||||||
!requiredPermissions.every((permission) => authStore.canAccess(permission))
|
!requiredPermissions.every((permission) => authStore.canAccess(permission))
|
||||||
) {
|
) {
|
||||||
|
if (location.pathname === "/devices" && authStore.hasRole("devices_maintenance_rw")) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
return <Navigate to="/" replace />;
|
return <Navigate to="/" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,54 @@ import {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { orderStationsByRoute } from "../../utils/routeStationsUtils";
|
import { orderStationsByRoute } from "../../utils/routeStationsUtils";
|
||||||
import { resamplePath } from "../../utils/animationUtils";
|
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 lightenRgbString(hex: string, amount: number): string | null {
|
||||||
|
const rgb = hexToRgbString(hex);
|
||||||
|
if (!rgb) return null;
|
||||||
|
const [r, g, b] = rgb.split(",").map(Number);
|
||||||
|
const lr = Math.round(r + (255 - r) * amount);
|
||||||
|
const lg = Math.round(g + (255 - g) * amount);
|
||||||
|
const lb = Math.round(b + (255 - b) * amount);
|
||||||
|
return `${lr}, ${lg}, ${lb}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
document.documentElement.style.setProperty("--carrier-right-menu-rgb", lightenRgbString(rightColor, 0.38) ?? "179, 165, 152");
|
||||||
|
}
|
||||||
|
|
||||||
class ApiStore {
|
class ApiStore {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@@ -115,6 +163,10 @@ class ApiStore {
|
|||||||
|
|
||||||
getCarrier = async () => {
|
getCarrier = async () => {
|
||||||
this.carrier = await getCarrier(this.route!.carrier_id!);
|
this.carrier = await getCarrier(this.route!.carrier_id!);
|
||||||
|
applyCarrierColors(this.carrier);
|
||||||
|
if (this.carrier.main_color) {
|
||||||
|
colorStore.setMainColor(this.carrier.main_color);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getCity = async () => {
|
getCity = async () => {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export type GetRouteResponse = {
|
|||||||
center_latitude: number;
|
center_latitude: number;
|
||||||
center_longitude: number;
|
center_longitude: number;
|
||||||
governor_appeal: number;
|
governor_appeal: number;
|
||||||
|
button_text?: string;
|
||||||
id: number;
|
id: number;
|
||||||
path: [number, number][];
|
path: [number, number][];
|
||||||
rotate: number;
|
rotate: number;
|
||||||
@@ -78,6 +79,9 @@ export type GetCarrierResponse = {
|
|||||||
logo: string;
|
logo: string;
|
||||||
short_name: string;
|
short_name: string;
|
||||||
slogan: string;
|
slogan: string;
|
||||||
|
main_color?: string;
|
||||||
|
left_color?: string;
|
||||||
|
right_color?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetCityResponse = {
|
export type GetCityResponse = {
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ const Fullscreen3DModal = ({ isOpen, onClose, fileUrl }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="fullscreen-3d-actions">
|
<div className="fullscreen-3d-actions">
|
||||||
<button title="Увеличить масштаб" disabled={scale >= 1}>
|
<button disabled={scale >= 1}>
|
||||||
<img src={scale_plus} alt="Увеличить" />
|
<img src={scale_plus} alt="Увеличить" />
|
||||||
</button>
|
</button>
|
||||||
<button title="Уменьшить масштаб" disabled={scale <= 0.1}>
|
<button disabled={scale <= 0.1}>
|
||||||
<img src={scale_minus} alt="Уменьшить" />
|
<img src={scale_minus} alt="Уменьшить" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onPointerUp={onClose} title="Закрыть">
|
<button onPointerUp={onClose}>
|
||||||
<img src={closeIcon} alt="Закрыть" />
|
<img src={closeIcon} alt="Закрыть" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -411,13 +411,13 @@ const ListOfSights = observer(() => {
|
|||||||
}, [currentSelectedSight]);
|
}, [currentSelectedSight]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="right-widget">
|
<div className="right-widget" lang={selectedLanguageRight}>
|
||||||
{currentSelectedSight && (
|
{currentSelectedSight && (
|
||||||
<SightFrame
|
<SightFrame
|
||||||
key={currentSelectedSight.id}
|
key={currentSelectedSight.id}
|
||||||
media={sightFrameMedia}
|
media={sightFrameMedia}
|
||||||
sight_id={currentSelectedSight.id}
|
sight_id={currentSelectedSight.id}
|
||||||
sight_name={currentSelectedSight.short_name || currentSelectedSight.name}
|
sight_name={currentSelectedSight.name}
|
||||||
selectedLanguageRight={selectedLanguageRight}
|
selectedLanguageRight={selectedLanguageRight}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
const LANGUAGES = {
|
const LANGUAGES = {
|
||||||
EN: "en",
|
EN: "en",
|
||||||
@@ -18,6 +18,26 @@ const ListHeader = function ListHeader({
|
|||||||
isTransferWidgetOpen,
|
isTransferWidgetOpen,
|
||||||
onBackToNearest,
|
onBackToNearest,
|
||||||
}) {
|
}) {
|
||||||
|
const [isIdle, setIsIdle] = useState(false);
|
||||||
|
const timerRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resetTimer = () => {
|
||||||
|
setIsIdle(false);
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = setTimeout(() => setIsIdle(true), 15000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = ["pointermove", "pointerdown", "touchstart", "keydown"];
|
||||||
|
events.forEach((e) => window.addEventListener(e, resetTimer));
|
||||||
|
resetTimer();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
events.forEach((e) => window.removeEventListener(e, resetTimer));
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
return selectedLanguageRight === LANGUAGES.RU
|
return selectedLanguageRight === LANGUAGES.RU
|
||||||
? "Достопримечательности"
|
? "Достопримечательности"
|
||||||
@@ -41,14 +61,11 @@ const ListHeader = function ListHeader({
|
|||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="21"
|
width="20"
|
||||||
height="12"
|
height="20"
|
||||||
viewBox="0 0 21 12"
|
viewBox="0 0 21 12"
|
||||||
fill="none"
|
fill="none"
|
||||||
style={{
|
className={`chevron-svg${isOpen ? " is-open" : ""}${isIdle ? " is-idle" : ""}`}
|
||||||
transform: isOpen ? "rotate(180deg)" : "rotate(0deg)",
|
|
||||||
transition: "transform 0.15s ease-in-out",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<g clipPath="url(#clip0_658_91932)">
|
<g clipPath="url(#clip0_658_91932)">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const SightComponent = function SightComponent({
|
|||||||
aria-label={`Выбрать достопримечательность ${title}`}
|
aria-label={`Выбрать достопримечательность ${title}`}
|
||||||
>
|
>
|
||||||
<div className="sight-image">{renderThumbnail()}</div>
|
<div className="sight-image">{renderThumbnail()}</div>
|
||||||
<div className="sight-title" title={title}>
|
<div className="sight-title">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
import React, { useState, useEffect, useRef, useMemo, useLayoutEffect, useCallback } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useGeolocationStore } from "../../stores";
|
import { useGeolocationStore } from "../../stores";
|
||||||
@@ -43,8 +43,95 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
const [threeViewResetKey, setThreeViewResetKey] = useState(0);
|
const [threeViewResetKey, setThreeViewResetKey] = useState(0);
|
||||||
const threeViewControlRef = useRef(null);
|
const threeViewControlRef = useRef(null);
|
||||||
const mediaCache = useRef({});
|
const mediaCache = useRef({});
|
||||||
|
const idleTimerRef = useRef(null);
|
||||||
|
|
||||||
const textWrapperRef = useRef(null);
|
const textWrapperRef = useRef(null);
|
||||||
|
const menuRef = useRef(null);
|
||||||
|
const [menuNeedsScroll, setMenuNeedsScroll] = useState(false);
|
||||||
|
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||||
|
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||||
|
|
||||||
|
const updateScrollState = useCallback(() => {
|
||||||
|
const menu = menuRef.current;
|
||||||
|
if (!menu) return;
|
||||||
|
setCanScrollLeft(menu.scrollLeft > 2);
|
||||||
|
setCanScrollRight(menu.scrollLeft + menu.clientWidth < menu.scrollWidth - 2);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const menu = menuRef.current;
|
||||||
|
if (!menu || !menuNeedsScroll) {
|
||||||
|
setCanScrollLeft(false);
|
||||||
|
setCanScrollRight(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateScrollState();
|
||||||
|
menu.addEventListener('scroll', updateScrollState);
|
||||||
|
return () => menu.removeEventListener('scroll', updateScrollState);
|
||||||
|
}, [menuNeedsScroll, updateScrollState]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const menu = menuRef.current;
|
||||||
|
if (!menu) return;
|
||||||
|
const children = Array.from(menu.querySelectorAll('.sight-frame-menu-point'));
|
||||||
|
if (children.length < 2) {
|
||||||
|
setMenuNeedsScroll(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const style = getComputedStyle(menu);
|
||||||
|
const availableWidth = menu.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
||||||
|
const totalChildrenWidth = children.reduce((sum, el) => sum + el.offsetWidth, 0);
|
||||||
|
const evenGap = (availableWidth - totalChildrenWidth) / (children.length + 1);
|
||||||
|
setMenuNeedsScroll(evenGap < 10);
|
||||||
|
}, [articleSections, selectedSection]);
|
||||||
|
|
||||||
|
// Автозакрытие fullscreen 3D при бездействии (60 сек)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFullscreen3D) {
|
||||||
|
if (idleTimerRef.current) {
|
||||||
|
clearInterval(idleTimerRef.current);
|
||||||
|
idleTimerRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let idleSeconds = 0;
|
||||||
|
|
||||||
|
const checkIdle = () => {
|
||||||
|
idleSeconds += 1;
|
||||||
|
if (idleSeconds >= 60) {
|
||||||
|
setIsFullscreen3D(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
idleTimerRef.current = setInterval(checkIdle, 1000);
|
||||||
|
|
||||||
|
const resetIdle = () => {
|
||||||
|
idleSeconds = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
"mousedown",
|
||||||
|
"mousemove",
|
||||||
|
"keypress",
|
||||||
|
"scroll",
|
||||||
|
"touchstart",
|
||||||
|
"click",
|
||||||
|
];
|
||||||
|
events.forEach((event) => {
|
||||||
|
window.addEventListener(event, resetIdle, { passive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (idleTimerRef.current) {
|
||||||
|
clearInterval(idleTimerRef.current);
|
||||||
|
idleTimerRef.current = null;
|
||||||
|
}
|
||||||
|
events.forEach((event) => {
|
||||||
|
window.removeEventListener(event, resetIdle);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [isFullscreen3D]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
routeSights,
|
routeSights,
|
||||||
@@ -166,7 +253,10 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
const introSection = {
|
const introSection = {
|
||||||
id: media?.id || "intro-title",
|
id: media?.id || "intro-title",
|
||||||
heading:
|
heading:
|
||||||
sight?.short_name || sight?.name || sight_name || "Название достопримечательности",
|
sight?.short_name ||
|
||||||
|
sight?.name ||
|
||||||
|
sight_name ||
|
||||||
|
"Название достопримечательности",
|
||||||
body: "",
|
body: "",
|
||||||
};
|
};
|
||||||
const allSections = [introSection, ...rightArticles];
|
const allSections = [introSection, ...rightArticles];
|
||||||
@@ -244,9 +334,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
alt=""
|
alt=""
|
||||||
className={className}
|
className={className}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
console.warn(
|
console.warn(`Failed to load image: ${currentMediaData.path}`);
|
||||||
`Failed to load image: ${currentMediaData.path}`,
|
|
||||||
);
|
|
||||||
e.target.style.display = "none";
|
e.target.style.display = "none";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -261,9 +349,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
playsInline
|
playsInline
|
||||||
className={className}
|
className={className}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
console.warn(
|
console.warn(`Failed to load video: ${currentMediaData.path}`);
|
||||||
`Failed to load video: ${currentMediaData.path}`,
|
|
||||||
);
|
|
||||||
e.target.style.display = "none";
|
e.target.style.display = "none";
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -313,27 +399,20 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="three-d-control-btn"
|
className="three-d-control-btn"
|
||||||
title="Уменьшить"
|
onPointerUp={() => threeViewControlRef.current?.zoomOut?.()}
|
||||||
onPointerUp={() =>
|
|
||||||
threeViewControlRef.current?.zoomOut?.()
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<MinusIcon />
|
<MinusIcon />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="three-d-control-btn"
|
className="three-d-control-btn"
|
||||||
title="Увеличить"
|
onPointerUp={() => threeViewControlRef.current?.zoomIn?.()}
|
||||||
onPointerUp={() =>
|
|
||||||
threeViewControlRef.current?.zoomIn?.()
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="three-d-control-btn"
|
className="three-d-control-btn"
|
||||||
title={isFullscreen3D ? "Свернуть" : "Развернуть"}
|
|
||||||
onPointerUp={() => {
|
onPointerUp={() => {
|
||||||
if (isFullscreen3D) {
|
if (isFullscreen3D) {
|
||||||
setIsFullscreen3D(false);
|
setIsFullscreen3D(false);
|
||||||
@@ -348,11 +427,6 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={"fullscreen-3d-button"}
|
className={"fullscreen-3d-button"}
|
||||||
title={
|
|
||||||
isFullscreen3D
|
|
||||||
? "Закрыть полноэкранный режим"
|
|
||||||
: "Открыть в полноэкранном режиме"
|
|
||||||
}
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -368,7 +442,8 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{isFullscreen3D && <div
|
{isFullscreen3D && (
|
||||||
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 94,
|
top: 94,
|
||||||
@@ -380,7 +455,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
<div
|
<div
|
||||||
className="cluster-sights-list"
|
className="cluster-sights-list"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to bottom right, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), rgba(0, 111, 58, 0.4)`,
|
background: `linear-gradient(to bottom right, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), rgba(var(--carrier-right-rgb, 0, 111, 58), 0.4)`,
|
||||||
backdropFilter: "blur(10px)",
|
backdropFilter: "blur(10px)",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
width: 200,
|
width: 200,
|
||||||
@@ -394,15 +469,26 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
label: "Вращать",
|
label: "Вращать",
|
||||||
icon: <img src={rotate3DIcon} alt="" width="14" height="14" />,
|
icon: (
|
||||||
|
<img
|
||||||
|
src={rotate3DIcon}
|
||||||
|
alt=""
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Приблизить / Отдалить",
|
label: "Приблизить / Отдалить",
|
||||||
icon: <img src={zoom3DIcon} alt="" width="14" height="14" />,
|
icon: (
|
||||||
|
<img src={zoom3DIcon} alt="" width="14" height="14" />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Переместить",
|
label: "Переместить",
|
||||||
icon: <img src={pan3DIcon} alt="" width="14" height="14" />,
|
icon: (
|
||||||
|
<img src={pan3DIcon} alt="" width="14" height="14" />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
].map((item, index, arr) => (
|
].map((item, index, arr) => (
|
||||||
<div
|
<div
|
||||||
@@ -448,7 +534,8 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@@ -605,7 +692,9 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
overflowWrap: "break-word",
|
overflowWrap: "break-word",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedSection === 0 ? processedSightName : (sightData?.short_name || sight_name)}
|
{selectedSection === 0
|
||||||
|
? processedSightName
|
||||||
|
: sightData?.short_name || sight_name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -623,8 +712,14 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="sight-frame-menu">
|
<div className="sight-frame-menu-wrapper">
|
||||||
{selectedSection !== 0 && (
|
<div className="sight-frame-menu-fade left" style={{ opacity: canScrollLeft ? 1 : 0 }} />
|
||||||
|
<div className="sight-frame-menu-fade right" style={{ opacity: canScrollRight ? 1 : 0 }} />
|
||||||
|
<div
|
||||||
|
className="sight-frame-menu"
|
||||||
|
ref={menuRef}
|
||||||
|
style={menuNeedsScroll ? { justifyContent: 'space-between' } : undefined}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -636,12 +731,24 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
paddingTop: "4.5px",
|
paddingTop: "4.5px",
|
||||||
paddingBottom: "4.5px",
|
paddingBottom: "4.5px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
|
opacity: selectedSection !== 0 ? 1 : 0,
|
||||||
|
transform: selectedSection !== 0 ? "scale(1)" : "scale(0.5)",
|
||||||
|
transition: "opacity 0.3s ease, transform 0.3s ease",
|
||||||
|
pointerEvents: selectedSection !== 0 ? "auto" : "none",
|
||||||
|
}}
|
||||||
|
onPointerUp={() => {
|
||||||
|
setSelectedSection(0);
|
||||||
|
setIsFullscreen3D(false);
|
||||||
}}
|
}}
|
||||||
onPointerUp={() => { setSelectedSection(0); setIsFullscreen3D(false); }}
|
|
||||||
>
|
>
|
||||||
<img src={subtractHomeIcon} alt="" width="24" height="21" style={{ display: "block" }} />
|
<img
|
||||||
|
src={subtractHomeIcon}
|
||||||
|
alt=""
|
||||||
|
width="24"
|
||||||
|
height="21"
|
||||||
|
style={{ display: "block" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{contentError ? (
|
{contentError ? (
|
||||||
<p className="error-message">{contentError}</p>
|
<p className="error-message">{contentError}</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -649,7 +756,10 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
articleSections.length > 1 &&
|
articleSections.length > 1 &&
|
||||||
articleSections.slice(1).map((section, index) => (
|
articleSections.slice(1).map((section, index) => (
|
||||||
<div
|
<div
|
||||||
onPointerUp={() => { setSelectedSection(index + 1); setIsFullscreen3D(false); }}
|
onPointerUp={() => {
|
||||||
|
setSelectedSection(index + 1);
|
||||||
|
setIsFullscreen3D(false);
|
||||||
|
}}
|
||||||
key={section.id || section.heading || index}
|
key={section.id || section.heading || index}
|
||||||
className={`sight-frame-menu-point ${
|
className={`sight-frame-menu-point ${
|
||||||
index + 1 === selectedSection ? "active" : ""
|
index + 1 === selectedSection ? "active" : ""
|
||||||
@@ -663,6 +773,7 @@ const SightFrame = observer(({ media, sight_id, sight_name }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -95,21 +95,36 @@ const TransferWidget = observer(function TransferWidget({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTransferLabel = () => {
|
const getTransferLabel = () => {
|
||||||
if (selectedLanguageRight === "ru") {
|
if (!stationName) {
|
||||||
return stationName
|
if (selectedLanguageRight === "en") return "Nearest station not found";
|
||||||
? `Пересадки остановки ${stationName}:`
|
if (selectedLanguageRight === "zh") return "最近的站点未找到";
|
||||||
: "Ближайшая остановка не обнаружена";
|
return "Ближайшая остановка не обнаружена";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedLanguageRight === "en") {
|
if (selectedLanguageRight === "en") {
|
||||||
return stationName
|
return (
|
||||||
? `Available transfers at station ${stationName}`
|
<>
|
||||||
: "Nearest station not found";
|
Transfer at stop<br />
|
||||||
|
«{stationName}»:
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stationName
|
if (selectedLanguageRight === "zh") {
|
||||||
? `在车站可用的换乘:${stationName}`
|
return (
|
||||||
: "最近的站点未找到";
|
<>
|
||||||
|
换乘站<br />
|
||||||
|
«{stationName}»:
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
Пересадка на остановке<br />
|
||||||
|
«{stationName}»:
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNoTransfersMessage = () => {
|
const getNoTransfersMessage = () => {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.react-markdown-container blockquote {
|
.react-markdown-container blockquote {
|
||||||
border-left: 4px solid #006F3A;
|
border-left: 4px solid var(--carrier-main, #006F3A);
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
114deg,
|
114deg,
|
||||||
rgba(255, 255, 255, 0.1) 8.71%,
|
rgba(255, 255, 255, 0.1) 8.71%,
|
||||||
rgba(255, 255, 255, 0.05) 69.69%
|
rgba(255, 255, 255, 0.05) 69.69%
|
||||||
), #006F3A;
|
), var(--carrier-main, #006F3A);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ function useThumbSync(scrollableRef: React.RefObject<HTMLDivElement | null>) {
|
|||||||
height: 60,
|
height: 60,
|
||||||
top: 0,
|
top: 0,
|
||||||
hasScroll: false,
|
hasScroll: false,
|
||||||
|
isAtTop: true,
|
||||||
|
isAtBottom: false,
|
||||||
});
|
});
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const rafRef = useRef<number | null>(null);
|
const rafRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const update = useCallback(() => {
|
const update = useCallback(() => {
|
||||||
@@ -31,8 +35,11 @@ function useThumbSync(scrollableRef: React.RefObject<HTMLDivElement | null>) {
|
|||||||
const st = el.scrollTop;
|
const st = el.scrollTop;
|
||||||
const th = ch;
|
const th = ch;
|
||||||
|
|
||||||
|
const isAtTop = st <= 0;
|
||||||
|
const isAtBottom = st + ch >= sh - 1;
|
||||||
|
|
||||||
if (sh <= ch) {
|
if (sh <= ch) {
|
||||||
setState({ height: th, top: 0, hasScroll: false });
|
setState((prev) => ({ ...prev, hasScroll: false, isAtTop: true, isAtBottom: true }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +48,7 @@ function useThumbSync(scrollableRef: React.RefObject<HTMLDivElement | null>) {
|
|||||||
const scrollRange = sh - ch;
|
const scrollRange = sh - ch;
|
||||||
const top = range <= 0 ? 0 : (st / scrollRange) * range;
|
const top = range <= 0 ? 0 : (st / scrollRange) * range;
|
||||||
|
|
||||||
setState({ height: thumbHeight, top, hasScroll: true });
|
setState({ height: thumbHeight, top, hasScroll: true, isAtTop, isAtBottom });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,7 +75,24 @@ function useThumbSync(scrollableRef: React.RefObject<HTMLDivElement | null>) {
|
|||||||
};
|
};
|
||||||
}, [update]);
|
}, [update]);
|
||||||
|
|
||||||
return state;
|
useEffect(() => {
|
||||||
|
if (state.hasScroll) {
|
||||||
|
if (hideTimerRef.current) {
|
||||||
|
clearTimeout(hideTimerRef.current);
|
||||||
|
hideTimerRef.current = null;
|
||||||
|
}
|
||||||
|
setVisible(true);
|
||||||
|
} else {
|
||||||
|
hideTimerRef.current = setTimeout(() => {
|
||||||
|
setVisible(false);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
||||||
|
};
|
||||||
|
}, [state.hasScroll]);
|
||||||
|
|
||||||
|
return { ...state, visible };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TouchableLayout = forwardRef<HTMLDivElement, TouchableLayoutProps>(
|
export const TouchableLayout = forwardRef<HTMLDivElement, TouchableLayoutProps>(
|
||||||
@@ -234,9 +258,12 @@ export const TouchableLayout = forwardRef<HTMLDivElement, TouchableLayoutProps>(
|
|||||||
};
|
};
|
||||||
}, [thumb.hasScroll]);
|
}, [thumb.hasScroll]);
|
||||||
|
|
||||||
const containerClassName = className
|
const containerClassName = [
|
||||||
? `scrollable-container ${className}`
|
"scrollable-container",
|
||||||
: "scrollable-container";
|
className,
|
||||||
|
thumb.isAtTop ? "is-at-top" : "",
|
||||||
|
thumb.isAtBottom ? "is-at-bottom" : "",
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
|
||||||
const viewportStyle: React.CSSProperties = maxHeight
|
const viewportStyle: React.CSSProperties = maxHeight
|
||||||
? {
|
? {
|
||||||
@@ -251,17 +278,21 @@ export const TouchableLayout = forwardRef<HTMLDivElement, TouchableLayoutProps>(
|
|||||||
<div ref={scrollableRef} className="scrollable">
|
<div ref={scrollableRef} className="scrollable">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{thumb.hasScroll && (
|
<div
|
||||||
<div ref={trackRef} className="custom-scrollbar-track">
|
ref={trackRef}
|
||||||
|
className="custom-scrollbar-track"
|
||||||
|
style={{ opacity: thumb.hasScroll ? 1 : 0 }}
|
||||||
|
>
|
||||||
|
{thumb.visible && (
|
||||||
<div
|
<div
|
||||||
ref={thumbRef}
|
ref={thumbRef}
|
||||||
className="custom-scrollbar-thumb"
|
className="custom-scrollbar-thumb"
|
||||||
style={{ height: thumb.height, top: thumb.top }}
|
style={{ height: thumb.height, top: thumb.top }}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
UNPASSED_STATION_COLOR,
|
UNPASSED_STATION_COLOR,
|
||||||
BUS_COLOR,
|
BUS_COLOR,
|
||||||
BASE_ICON_SIZE,
|
BASE_ICON_SIZE,
|
||||||
CLUSTER_RADIUS_BASE,
|
|
||||||
} from "./Constants";
|
} from "./Constants";
|
||||||
import { SCALE_FACTOR } from "../../assets/Constants";
|
import { SCALE_FACTOR } from "../../assets/Constants";
|
||||||
import { apiStore } from "../../api/ApiStore/store";
|
import { apiStore } from "../../api/ApiStore/store";
|
||||||
@@ -156,6 +155,19 @@ const useSightClustering = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasCustomIcon =
|
||||||
|
sight.is_default_icon === false && !isMediaIdEmpty(sight.icon ?? null);
|
||||||
|
|
||||||
|
if (hasCustomIcon) {
|
||||||
|
sight.visited = true;
|
||||||
|
clusteredResult.push({
|
||||||
|
type: "point",
|
||||||
|
id: String(sight.id),
|
||||||
|
data: sight,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const clusterSights: SightData[] = [];
|
const clusterSights: SightData[] = [];
|
||||||
const queue = [sight];
|
const queue = [sight];
|
||||||
sight.visited = true;
|
sight.visited = true;
|
||||||
@@ -165,8 +177,12 @@ const useSightClustering = (
|
|||||||
clusterSights.push(current);
|
clusterSights.push(current);
|
||||||
|
|
||||||
for (const potentialNeighbor of unclusteredSights) {
|
for (const potentialNeighbor of unclusteredSights) {
|
||||||
|
const neighborHasCustomIcon =
|
||||||
|
potentialNeighbor.is_default_icon === false &&
|
||||||
|
!isMediaIdEmpty(potentialNeighbor.icon ?? null);
|
||||||
if (
|
if (
|
||||||
!potentialNeighbor.visited &&
|
!potentialNeighbor.visited &&
|
||||||
|
!neighborHasCustomIcon &&
|
||||||
clusterSights.length < 4 &&
|
clusterSights.length < 4 &&
|
||||||
getDistance(current, potentialNeighbor) < distanceThreshold
|
getDistance(current, potentialNeighbor) < distanceThreshold
|
||||||
) {
|
) {
|
||||||
@@ -176,6 +192,10 @@ const useSightClustering = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const leftover of queue) {
|
||||||
|
leftover.visited = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (clusterSights.length > 1) {
|
if (clusterSights.length > 1) {
|
||||||
let furthestSight: SightData | null = null;
|
let furthestSight: SightData | null = null;
|
||||||
let maxDistanceToPath = -1;
|
let maxDistanceToPath = -1;
|
||||||
@@ -382,13 +402,15 @@ export const WebGLMap = observer(() => {
|
|||||||
return livePercent;
|
return livePercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sight?.is_default_icon === false) {
|
||||||
if (
|
if (
|
||||||
sight != null &&
|
|
||||||
typeof sight.icon_size === "number" &&
|
typeof sight.icon_size === "number" &&
|
||||||
Number.isFinite(sight.icon_size)
|
Number.isFinite(sight.icon_size)
|
||||||
) {
|
) {
|
||||||
return sight.icon_size;
|
return sight.icon_size;
|
||||||
}
|
}
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof routeData?.icon_size === "number" &&
|
typeof routeData?.icon_size === "number" &&
|
||||||
@@ -881,25 +903,36 @@ export const WebGLMap = observer(() => {
|
|||||||
|
|
||||||
// Сегментный индекс каждой упорядоченной станции на routePath (canvas-пространство)
|
// Сегментный индекс каждой упорядоченной станции на routePath (canvas-пространство)
|
||||||
const orderedStationSegs = useMemo(() => {
|
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) => {
|
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;
|
if (stIdx < 0) return -1;
|
||||||
const sx = stationPoints[stIdx * 2];
|
const sx = stationPoints[stIdx * 2];
|
||||||
const sy = stationPoints[stIdx * 2 + 1];
|
const sy = stationPoints[stIdx * 2 + 1];
|
||||||
if (sx === undefined || sy === undefined) return -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) {
|
for (let i = 0; i < routePath.length - 2; i += 2) {
|
||||||
const p1x = routePath[i], p1y = routePath[i + 1];
|
const p1x = routePath[i],
|
||||||
const p2x = routePath[i + 2], p2y = routePath[i + 3];
|
p1y = routePath[i + 1];
|
||||||
const dx = p2x - p1x, dy = p2y - p1y;
|
const p2x = routePath[i + 2],
|
||||||
|
p2y = routePath[i + 3];
|
||||||
|
const dx = p2x - p1x,
|
||||||
|
dy = p2y - p1y;
|
||||||
const len2 = dx * dx + dy * dy;
|
const len2 = dx * dx + dy * dy;
|
||||||
if (!len2) continue;
|
if (!len2) continue;
|
||||||
const t = ((sx - p1x) * dx + (sy - p1y) * dy) / len2;
|
const t = ((sx - p1x) * dx + (sy - p1y) * dy) / len2;
|
||||||
const cl = Math.max(0, Math.min(1, t));
|
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);
|
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;
|
return best;
|
||||||
});
|
});
|
||||||
@@ -1144,7 +1177,9 @@ export const WebGLMap = observer(() => {
|
|||||||
const curIdx = apiStore.positionIndex;
|
const curIdx = apiStore.positionIndex;
|
||||||
const prevIdx = prevPositionIndexRef.current;
|
const prevIdx = prevPositionIndexRef.current;
|
||||||
const pathLen = apiStore.route?.path?.length ?? 0;
|
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;
|
Math.abs(curIdx - prevIdx) > pathLen / 4;
|
||||||
prevPositionIndexRef.current = curIdx;
|
prevPositionIndexRef.current = curIdx;
|
||||||
|
|
||||||
@@ -1414,32 +1449,72 @@ export const WebGLMap = observer(() => {
|
|||||||
|
|
||||||
gl.uniform1f(u_pointSize, pointInnerSizePx);
|
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 passedPts1: number[] = [];
|
||||||
const unpassedPts1: number[] = [];
|
const unpassedPts1: number[] = [];
|
||||||
for (let i = 0; i < orderedRouteStations.length; i++) {
|
for (let i = 0; i < orderedRouteStations.length; i++) {
|
||||||
const orderedStation = (orderedRouteStations as any[])[i];
|
const orderedStation = (orderedRouteStations as any[])[i];
|
||||||
const stationSeg = orderedStationSegs[i] ?? -1;
|
const stationSeg = orderedStationSegs[i] ?? -1;
|
||||||
if (!orderedStation || stationSeg < 0) continue;
|
if (!orderedStation || stationSeg < 0) continue;
|
||||||
const isPassed = simulationDirection === 1 ? stationSeg < tramSegIndex : stationSeg > tramSegIndex;
|
const isPassed =
|
||||||
const stIdx = stationData.findIndex((s: any) => String(s.id) === String(orderedStation.id));
|
simulationDirection === 1
|
||||||
|
? stationSeg < tramSegIndex
|
||||||
|
: stationSeg > tramSegIndex;
|
||||||
|
const stIdx = stationData.findIndex(
|
||||||
|
(s: any) => String(s.id) === String(orderedStation.id),
|
||||||
|
);
|
||||||
if (stIdx < 0) continue;
|
if (stIdx < 0) continue;
|
||||||
const sx = stationPoints[stIdx * 2] as number;
|
const sx = stationPoints[stIdx * 2] as number;
|
||||||
const sy = stationPoints[stIdx * 2 + 1] 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) {
|
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.uniform4f(
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(passedPts1), gl.STATIC_DRAW);
|
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);
|
gl.drawArrays(gl.POINTS, 0, passedPts1.length / 2);
|
||||||
}
|
}
|
||||||
if (unpassedPts1.length > 0) {
|
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.uniform4f(
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(unpassedPts1), gl.STATIC_DRAW);
|
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);
|
gl.drawArrays(gl.POINTS, 0, unpassedPts1.length / 2);
|
||||||
}
|
}
|
||||||
} else {
|
} 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.bufferData(gl.ARRAY_BUFFER, stationPoints, gl.STATIC_DRAW);
|
||||||
gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);
|
gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);
|
||||||
}
|
}
|
||||||
@@ -1513,12 +1588,17 @@ export const WebGLMap = observer(() => {
|
|||||||
const passedStationIds = new Set<string>();
|
const passedStationIds = new Set<string>();
|
||||||
const unpassedStationIds = new Set<string>();
|
const unpassedStationIds = new Set<string>();
|
||||||
|
|
||||||
if (tramSegIndex >= 0 && orderedRouteStations && orderedStationSegs.length === orderedRouteStations.length) {
|
if (
|
||||||
|
tramSegIndex >= 0 &&
|
||||||
|
orderedRouteStations &&
|
||||||
|
orderedStationSegs.length === orderedRouteStations.length
|
||||||
|
) {
|
||||||
for (let i = 0; i < orderedRouteStations.length; i++) {
|
for (let i = 0; i < orderedRouteStations.length; i++) {
|
||||||
const station = (orderedRouteStations as any[])[i];
|
const station = (orderedRouteStations as any[])[i];
|
||||||
const seg = orderedStationSegs[i] ?? -1;
|
const seg = orderedStationSegs[i] ?? -1;
|
||||||
if (!station || seg < 0) continue;
|
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));
|
if (isPassed) passedStationIds.add(String(station.id));
|
||||||
else unpassedStationIds.add(String(station.id));
|
else unpassedStationIds.add(String(station.id));
|
||||||
}
|
}
|
||||||
@@ -1662,11 +1742,26 @@ export const WebGLMap = observer(() => {
|
|||||||
const sin = Math.sin(rotationAngle);
|
const sin = Math.sin(rotationAngle);
|
||||||
|
|
||||||
const startStationData = orderedRouteStations?.[0]
|
const startStationData = orderedRouteStations?.[0]
|
||||||
? stationData.find((station: any) => station.id.toString() === String(orderedRouteStations[0].id))
|
? stationData.find(
|
||||||
: stationData.find((station: any) => station.id.toString() === apiStore.context?.startStopId);
|
(station: any) =>
|
||||||
|
station.id.toString() === String(orderedRouteStations[0].id),
|
||||||
|
)
|
||||||
|
: stationData.find(
|
||||||
|
(station: any) =>
|
||||||
|
station.id.toString() === apiStore.context?.startStopId,
|
||||||
|
);
|
||||||
const endStationData = orderedRouteStations?.length
|
const endStationData = orderedRouteStations?.length
|
||||||
? stationData.find((station: any) => station.id.toString() === String(orderedRouteStations[orderedRouteStations.length - 1].id))
|
? stationData.find(
|
||||||
: stationData.find((station: any) => station.id.toString() === apiStore.context?.endStopId);
|
(station: any) =>
|
||||||
|
station.id.toString() ===
|
||||||
|
String(
|
||||||
|
orderedRouteStations[orderedRouteStations.length - 1].id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: stationData.find(
|
||||||
|
(station: any) =>
|
||||||
|
station.id.toString() === apiStore.context?.endStopId,
|
||||||
|
);
|
||||||
|
|
||||||
const terminalStations: number[] = [];
|
const terminalStations: number[] = [];
|
||||||
|
|
||||||
@@ -1766,7 +1861,13 @@ export const WebGLMap = observer(() => {
|
|||||||
}
|
}
|
||||||
return best;
|
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;
|
: false;
|
||||||
|
|
||||||
@@ -1799,7 +1900,13 @@ export const WebGLMap = observer(() => {
|
|||||||
}
|
}
|
||||||
return best;
|
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;
|
: false;
|
||||||
|
|
||||||
@@ -1825,11 +1932,24 @@ export const WebGLMap = observer(() => {
|
|||||||
const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
||||||
|
|
||||||
if (startStationData && endStationData) {
|
if (startStationData && endStationData) {
|
||||||
const startIsPassed = simulationDirection === 1 ? true : isStartPassed;
|
const startIsPassed =
|
||||||
|
simulationDirection === 1 ? true : isStartPassed;
|
||||||
const endIsPassed = simulationDirection === -1 ? true : isEndPassed;
|
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.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);
|
gl.drawArrays(gl.POINTS, 1, 1);
|
||||||
} else {
|
} else {
|
||||||
const isStartStation = startStationData !== undefined;
|
const isStartStation = startStationData !== undefined;
|
||||||
@@ -2453,14 +2573,8 @@ export const WebGLMap = observer(() => {
|
|||||||
const rx = cluster.longitude;
|
const rx = cluster.longitude;
|
||||||
const ry = cluster.latitude;
|
const ry = cluster.latitude;
|
||||||
|
|
||||||
const iconSizePercent = resolveSightIconSizePercent();
|
|
||||||
const iconSize =
|
|
||||||
SIGHT_ICON_BASE_SIZE * clamp(iconSizePercent / 100, 0.1, 10);
|
|
||||||
|
|
||||||
const screenX = (rx * scale + position.x) / dpr;
|
const screenX = (rx * scale + position.x) / dpr;
|
||||||
const screenY = (ry * scale + position.y) / dpr;
|
const screenY = (ry * scale + position.y) / dpr;
|
||||||
const iconLeft = screenX - iconSize / 2;
|
|
||||||
const iconTop = screenY - iconSize / 2;
|
|
||||||
|
|
||||||
const isExpanded = activeClusterId === cluster.id;
|
const isExpanded = activeClusterId === cluster.id;
|
||||||
const selectedSightInCluster = cluster.sights.find(
|
const selectedSightInCluster = cluster.sights.find(
|
||||||
@@ -2472,6 +2586,20 @@ export const WebGLMap = observer(() => {
|
|||||||
const selectedIsCustomInCluster =
|
const selectedIsCustomInCluster =
|
||||||
selectedSightInCluster?.is_default_icon === false &&
|
selectedSightInCluster?.is_default_icon === false &&
|
||||||
selectedHasIconInCluster;
|
selectedHasIconInCluster;
|
||||||
|
|
||||||
|
const iconSizePercent = resolveSightIconSizePercent(
|
||||||
|
cluster.sights[0],
|
||||||
|
);
|
||||||
|
const clusterCustomScaleFactor = selectedIsCustomInCluster
|
||||||
|
? scale / Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6)
|
||||||
|
: 1;
|
||||||
|
const iconSize =
|
||||||
|
SIGHT_ICON_BASE_SIZE *
|
||||||
|
clamp(iconSizePercent / 100, 0.1, 10) *
|
||||||
|
clusterCustomScaleFactor;
|
||||||
|
|
||||||
|
const iconLeft = screenX - iconSize;
|
||||||
|
const iconTop = screenY - iconSize;
|
||||||
const hasSelectedAltIconInCluster =
|
const hasSelectedAltIconInCluster =
|
||||||
selectedSightInCluster != null &&
|
selectedSightInCluster != null &&
|
||||||
selectedIsCustomInCluster &&
|
selectedIsCustomInCluster &&
|
||||||
@@ -2529,7 +2657,7 @@ export const WebGLMap = observer(() => {
|
|||||||
)
|
)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
const badgeColor = "#006F3A";
|
const badgeColor = "var(--carrier-main, #006F3A)";
|
||||||
const listPanelWidth = 200;
|
const listPanelWidth = 200;
|
||||||
const listItemHeight = 30;
|
const listItemHeight = 30;
|
||||||
const listMaxHeight = 250;
|
const listMaxHeight = 250;
|
||||||
@@ -2545,13 +2673,10 @@ export const WebGLMap = observer(() => {
|
|||||||
onTouchEnd={handleClusterClick}
|
onTouchEnd={handleClusterClick}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: iconLeft - CLUSTER_RADIUS_BASE - 10,
|
left: iconLeft,
|
||||||
top: iconTop - CLUSTER_RADIUS_BASE - 10,
|
top: iconTop,
|
||||||
width: iconSize + CLUSTER_RADIUS_BASE * 2 + 20,
|
width: iconSize,
|
||||||
height: iconSize + CLUSTER_RADIUS_BASE * 2 + 20,
|
height: iconSize,
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
@@ -2564,15 +2689,22 @@ export const WebGLMap = observer(() => {
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ position: "relative" }}>
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={clusterIconUrl}
|
src={clusterIconUrl}
|
||||||
alt=""
|
alt=""
|
||||||
width={iconSize}
|
|
||||||
height={iconSize}
|
|
||||||
style={{
|
style={{
|
||||||
display: "block",
|
display: "block",
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
filter: hasSelectedSight
|
filter: hasSelectedSight
|
||||||
? hasSelectedAltIconInCluster
|
? hasSelectedAltIconInCluster
|
||||||
? "none"
|
? "none"
|
||||||
@@ -2585,6 +2717,7 @@ export const WebGLMap = observer(() => {
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: -6,
|
top: -6,
|
||||||
right: -6,
|
right: -6,
|
||||||
|
zIndex: 1,
|
||||||
width: 15,
|
width: 15,
|
||||||
height: 15,
|
height: 15,
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
@@ -2607,12 +2740,11 @@ export const WebGLMap = observer(() => {
|
|||||||
<div
|
<div
|
||||||
data-expanded-cluster={cluster.id}
|
data-expanded-cluster={cluster.id}
|
||||||
onTouchStart={handleCircleInteraction}
|
onTouchStart={handleCircleInteraction}
|
||||||
onTouchMove={handleCircleInteraction}
|
|
||||||
onMouseMove={handleCircleInteraction}
|
onMouseMove={handleCircleInteraction}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: screenX - iconSize / 2,
|
left: screenX - iconSize,
|
||||||
top: screenY - iconSize / 2,
|
top: screenY - iconSize,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
@@ -2673,15 +2805,14 @@ export const WebGLMap = observer(() => {
|
|||||||
<div
|
<div
|
||||||
className="cluster-sights-list"
|
className="cluster-sights-list"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to bottom right, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), rgba(0, 111, 58, 0.4)`,
|
background: `linear-gradient(to bottom right, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), rgba(var(--carrier-main-rgb, 0, 111, 58), 0.4)`,
|
||||||
backdropFilter: "blur(10px)",
|
backdropFilter: "blur(10px)",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
width: listPanelWidth,
|
width: listPanelWidth,
|
||||||
maxHeight: hasMoreThanTwo ? listMaxHeight : undefined,
|
maxHeight: hasMoreThanTwo ? listMaxHeight : undefined,
|
||||||
boxShadow:
|
boxShadow:
|
||||||
"0 0 0 1px rgba(255, 255, 255, 0.3) inset, 4px 4px 12px 0 rgba(255, 255, 255, 0.12) inset",
|
"inset 0 0 0 1px rgba(255, 255, 255, 0.3), inset 4px 4px 12px 0 rgba(255, 255, 255, 0.12)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@@ -2824,7 +2955,6 @@ export const WebGLMap = observer(() => {
|
|||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
title={sightName}
|
|
||||||
>
|
>
|
||||||
{sightName}
|
{sightName}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useGeolocationStore } from "../../stores";
|
|||||||
import "../../styles/LeftWidget.css";
|
import "../../styles/LeftWidget.css";
|
||||||
import { apiStore } from "../../api/ApiStore/store";
|
import { apiStore } from "../../api/ApiStore/store";
|
||||||
import { apiBaseURL } from "../../api/apiConfig";
|
import { apiBaseURL } from "../../api/apiConfig";
|
||||||
|
import { ReactMarkdownComponent } from "../ReactMarkdown";
|
||||||
|
import { TouchableLayout } from "../TouchableLayout";
|
||||||
|
|
||||||
const LeftWidget = observer(
|
const LeftWidget = observer(
|
||||||
({ selectedSightId, onClose, isVisible, sightTop }) => {
|
({ selectedSightId, onClose, isVisible, sightTop }) => {
|
||||||
@@ -15,8 +17,7 @@ const LeftWidget = observer(
|
|||||||
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
||||||
const [widgetHeight, setWidgetHeight] = useState(0);
|
const [widgetHeight, setWidgetHeight] = useState(0);
|
||||||
|
|
||||||
const textRef = useRef(null);
|
const layoutRef = useRef(null);
|
||||||
const activeTouch = useRef(null);
|
|
||||||
const widgetRef = useRef(null);
|
const widgetRef = useRef(null);
|
||||||
|
|
||||||
const store = useGeolocationStore();
|
const store = useGeolocationStore();
|
||||||
@@ -37,64 +38,10 @@ const LeftWidget = observer(
|
|||||||
}, [selectedSightData, isImageLoaded, isVisible, isLoading, error]);
|
}, [selectedSightData, isImageLoaded, isVisible, isLoading, error]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollContainer = textRef.current;
|
if (layoutRef.current) {
|
||||||
if (!scrollContainer) return;
|
const scrollable = layoutRef.current.querySelector(".scrollable");
|
||||||
|
if (scrollable) scrollable.scrollTop = 0;
|
||||||
const handleTouchStart = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (e.touches.length === 1) {
|
|
||||||
activeTouch.current = {
|
|
||||||
identifier: e.touches[0].identifier,
|
|
||||||
lastY: e.touches[0].clientY,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleTouchMove = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (activeTouch.current) {
|
|
||||||
for (const touch of e.changedTouches) {
|
|
||||||
if (touch.identifier === activeTouch.current.identifier) {
|
|
||||||
const deltaY = touch.clientY - activeTouch.current.lastY;
|
|
||||||
scrollContainer.scrollTop -= deltaY;
|
|
||||||
activeTouch.current.lastY = touch.clientY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTouchEnd = (e) => {
|
|
||||||
for (const touch of e.changedTouches) {
|
|
||||||
if (
|
|
||||||
activeTouch.current &&
|
|
||||||
touch.identifier === activeTouch.current.identifier
|
|
||||||
) {
|
|
||||||
activeTouch.current = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
scrollContainer.addEventListener("touchstart", handleTouchStart, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
scrollContainer.addEventListener("touchmove", handleTouchMove, {
|
|
||||||
passive: false,
|
|
||||||
});
|
|
||||||
scrollContainer.addEventListener("touchend", handleTouchEnd, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
scrollContainer.addEventListener("touchcancel", handleTouchEnd, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
scrollContainer.removeEventListener("touchstart", handleTouchStart);
|
|
||||||
scrollContainer.removeEventListener("touchmove", handleTouchMove);
|
|
||||||
scrollContainer.removeEventListener("touchend", handleTouchEnd);
|
|
||||||
scrollContainer.removeEventListener("touchcancel", handleTouchEnd);
|
|
||||||
};
|
|
||||||
}, [selectedSightData]);
|
}, [selectedSightData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -123,6 +70,8 @@ const LeftWidget = observer(
|
|||||||
? routeSightsEn.find((sight) => sight.id === selectedSightId)
|
? routeSightsEn.find((sight) => sight.id === selectedSightId)
|
||||||
: routeSightsZh.find((sight) => sight.id === selectedSightId);
|
: routeSightsZh.find((sight) => sight.id === selectedSightId);
|
||||||
|
|
||||||
|
if (!sight) return;
|
||||||
|
|
||||||
const leftArticle = sight.left_article;
|
const leftArticle = sight.left_article;
|
||||||
|
|
||||||
const leftArticleData =
|
const leftArticleData =
|
||||||
@@ -132,15 +81,17 @@ const LeftWidget = observer(
|
|||||||
? sightArticlesEn.get(leftArticle + "_" + selectedLanguage)
|
? sightArticlesEn.get(leftArticle + "_" + selectedLanguage)
|
||||||
: sightArticlesZh.get(leftArticle + "_" + selectedLanguage);
|
: sightArticlesZh.get(leftArticle + "_" + selectedLanguage);
|
||||||
|
|
||||||
|
if (!leftArticleData?.media?.length) return;
|
||||||
|
|
||||||
const media = await ContentAPI.getMediaPreview(
|
const media = await ContentAPI.getMediaPreview(
|
||||||
leftArticleData.media[0].id,
|
leftArticleData.media[0].id,
|
||||||
selectedLanguage
|
selectedLanguage,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
mediaPath: media.path,
|
mediaPath: media.path,
|
||||||
mediaType: media.type,
|
mediaType: media.type,
|
||||||
title: sight.short_name || sight.name || leftArticleData.heading,
|
title: leftArticleData.heading,
|
||||||
text: leftArticleData.body,
|
text: leftArticleData.body,
|
||||||
address: sight.address,
|
address: sight.address,
|
||||||
};
|
};
|
||||||
@@ -178,7 +129,7 @@ const LeftWidget = observer(
|
|||||||
setIsImageLoaded(false);
|
setIsImageLoaded(false);
|
||||||
console.error(
|
console.error(
|
||||||
"Ошибка загрузки изображения для достопримечательности:",
|
"Ошибка загрузки изображения для достопримечательности:",
|
||||||
selectedSightId
|
selectedSightId,
|
||||||
);
|
);
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -208,7 +159,7 @@ const LeftWidget = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={widgetRef} style={widgetTransformStyle} className="left-widget">
|
<div ref={widgetRef} style={widgetTransformStyle} className="left-widget" lang={selectedLanguage}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div>Загрузка информации...</div>
|
<div>Загрузка информации...</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
@@ -234,9 +185,11 @@ const LeftWidget = observer(
|
|||||||
<div className="left-widget-address">
|
<div className="left-widget-address">
|
||||||
{selectedSightData.address}
|
{selectedSightData.address}
|
||||||
</div>
|
</div>
|
||||||
<div ref={textRef} className="left-widget-text">
|
<TouchableLayout ref={layoutRef} className="left-widget-text-scroll">
|
||||||
{selectedSightData.text}
|
<div className="left-widget-text">
|
||||||
|
<ReactMarkdownComponent value={selectedSightData.text} />
|
||||||
</div>
|
</div>
|
||||||
|
</TouchableLayout>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (isVisible || selectedSightData) && !isLoading ? (
|
) : (isVisible || selectedSightData) && !isLoading ? (
|
||||||
@@ -250,7 +203,7 @@ const LeftWidget = observer(
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default LeftWidget;
|
export default LeftWidget;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import "../../styles/SideMenu.css";
|
|||||||
import AppealWidget from "../widgets/AppealWidget";
|
import AppealWidget from "../widgets/AppealWidget";
|
||||||
import { useEffect, useState, useCallback, useRef } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import gouvermentImage from "../../assets/images/test-image.png";
|
|
||||||
import sideMenuPhoto from "/side-menu-photo.png";
|
import sideMenuPhoto from "/side-menu-photo.png";
|
||||||
import RouteWidget from "../widgets/RouteWidget";
|
import RouteWidget from "../widgets/RouteWidget";
|
||||||
import ContentAPI from "../../api/content/content.api";
|
import ContentAPI from "../../api/content/content.api";
|
||||||
@@ -13,6 +12,7 @@ import StationsList from "./StationsList";
|
|||||||
import LeftWidget from "./LeftWidget";
|
import LeftWidget from "./LeftWidget";
|
||||||
import { apiStore } from "../../api/ApiStore/store";
|
import { apiStore } from "../../api/ApiStore/store";
|
||||||
import { getMediaUrl } from "../../api/apiConfig";
|
import { getMediaUrl } from "../../api/apiConfig";
|
||||||
|
import defaultCrest from "../../assets/images/Герб.png";
|
||||||
|
|
||||||
const SideMenu = observer(({ onMenuToggle }) => {
|
const SideMenu = observer(({ onMenuToggle }) => {
|
||||||
const {
|
const {
|
||||||
@@ -263,15 +263,20 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMenuOpenRef = useRef(isMenuOpen);
|
||||||
|
const handleMenuToggleRef = useRef(handleMenuToggle);
|
||||||
|
useEffect(() => { isMenuOpenRef.current = isMenuOpen; }, [isMenuOpen]);
|
||||||
|
useEffect(() => { handleMenuToggleRef.current = handleMenuToggle; });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Автоматическое закрытие сайд-меню после 45 секунд бездействия
|
// Автоматическое закрытие сайд-меню после 60 секунд бездействия
|
||||||
let idleSeconds = 0;
|
let idleSeconds = 0;
|
||||||
|
|
||||||
const checkIdle = () => {
|
const checkIdle = () => {
|
||||||
idleSeconds += 1;
|
idleSeconds += 1;
|
||||||
|
|
||||||
if (idleSeconds >= 45 && isMenuOpen) {
|
if (idleSeconds >= 60 && isMenuOpenRef.current) {
|
||||||
handleMenuToggle(false);
|
handleMenuToggleRef.current(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,7 +305,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
|||||||
window.removeEventListener(event, resetIdle);
|
window.removeEventListener(event, resetIdle);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [isMenuOpen, handleMenuToggle]);
|
}, []);
|
||||||
|
|
||||||
// Закрываем и открываем список достопримечательностей при изменении сортировки
|
// Закрываем и открываем список достопримечательностей при изменении сортировки
|
||||||
const prevSortingByRef = useRef(sortingBy);
|
const prevSortingByRef = useRef(sortingBy);
|
||||||
@@ -357,7 +362,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
|||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
background:
|
background:
|
||||||
isSightsOpen || isStationOpen
|
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,
|
: undefined,
|
||||||
backdropFilter:
|
backdropFilter:
|
||||||
isSightsOpen || isStationOpen ? "blur(10px)" : undefined,
|
isSightsOpen || isStationOpen ? "blur(10px)" : undefined,
|
||||||
@@ -369,13 +374,11 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
|||||||
"background 0.3s ease, backdrop-filter 0.3s ease, box-shadow 0.3s ease",
|
"background 0.3s ease, backdrop-filter 0.3s ease, box-shadow 0.3s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{designData?.creastPath && (
|
|
||||||
<img
|
<img
|
||||||
className="side-menu-crest"
|
className="side-menu-crest"
|
||||||
src={designData?.creastPath}
|
src={designData?.creastPath || defaultCrest}
|
||||||
alt="Герб"
|
alt="Герб"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{carrier?.slogan && (
|
{carrier?.slogan && (
|
||||||
<div className="side-menu-label">{carrier.slogan}</div>
|
<div className="side-menu-label">{carrier.slogan}</div>
|
||||||
)}
|
)}
|
||||||
@@ -444,14 +447,16 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
|||||||
}}
|
}}
|
||||||
className="appeal-button"
|
className="appeal-button"
|
||||||
>
|
>
|
||||||
{selectedLanguage == "ru"
|
{route?.button_text
|
||||||
|
? route.button_text
|
||||||
|
: selectedLanguage == "ru"
|
||||||
? "Обращение губернатора"
|
? "Обращение губернатора"
|
||||||
: selectedLanguage == "zh"
|
: selectedLanguage == "zh"
|
||||||
? "州长致辞"
|
? "州长致辞"
|
||||||
: "Governor's appeal"}
|
: "Governor's appeal"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="side-menu-buttons">
|
<div className="side-menu-buttons" style={{ marginTop: route?.governor_appeal > 0 ? '40px' : '260px' }}>
|
||||||
<div
|
<div
|
||||||
onPointerUp={() => {
|
onPointerUp={() => {
|
||||||
if (!isSightsOpen) {
|
if (!isSightsOpen) {
|
||||||
@@ -493,7 +498,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`side-menu-button side-menu-button--sights ${
|
className={`side-menu-button ${
|
||||||
isSightsOpen ? "side-menu-button--active" : ""
|
isSightsOpen ? "side-menu-button--active" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -502,7 +507,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
|||||||
: selectedLanguage == "zh"
|
: selectedLanguage == "zh"
|
||||||
? "景点"
|
? "景点"
|
||||||
: "Attractions"}
|
: "Attractions"}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onPointerUp={() => {
|
onPointerUp={() => {
|
||||||
if (!isStationOpen) {
|
if (!isStationOpen) {
|
||||||
@@ -550,7 +555,7 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
|||||||
: selectedLanguage == "zh"
|
: selectedLanguage == "zh"
|
||||||
? "车站"
|
? "车站"
|
||||||
: "Stations"}
|
: "Stations"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="side-menu-tag">
|
<div className="side-menu-tag">
|
||||||
{/* {selectedLanguage == "ru"
|
{/* {selectedLanguage == "ru"
|
||||||
@@ -579,11 +584,17 @@ const SideMenu = observer(({ onMenuToggle }) => {
|
|||||||
<RouteWidget />
|
<RouteWidget />
|
||||||
|
|
||||||
<AppealWidget
|
<AppealWidget
|
||||||
widgetImgPath={gouvermentImage}
|
widgetImgPath={(() => {
|
||||||
|
const m = sightArticles.get(route?.governor_appeal + "_ru")?.media;
|
||||||
|
const mediaId = Array.isArray(m) ? m[0]?.id : m?.id;
|
||||||
|
return mediaId ? getMediaUrl(mediaId) : undefined;
|
||||||
|
})()}
|
||||||
|
isOpen={isWidgetOpen}
|
||||||
style={{
|
style={{
|
||||||
transform: isWidgetOpen ? "translateX(0)" : "translateX(-200%)",
|
transform: isWidgetOpen ? "translateX(0)" : "translateX(-200%)",
|
||||||
transition: "transform 0.5s ease",
|
transition: "transform 0.5s ease",
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
|
pointerEvents: isWidgetOpen ? "auto" : "none",
|
||||||
}}
|
}}
|
||||||
widgetLabel={
|
widgetLabel={
|
||||||
selectedLanguage == "ru"
|
selectedLanguage == "ru"
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ const StationItem = ({
|
|||||||
className="side-menu-sight"
|
className="side-menu-sight"
|
||||||
onPointerDown={(e) => handlePointerDown(e, station.id)}
|
onPointerDown={(e) => handlePointerDown(e, station.id)}
|
||||||
onPointerUp={(e) => handlePointerUp(e, station.id, handleStationClick)}
|
onPointerUp={(e) => handlePointerUp(e, station.id, handleStationClick)}
|
||||||
title={station.name}
|
|
||||||
>
|
>
|
||||||
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
||||||
{station.name}
|
{station.name}
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ const SightItem = ({
|
|||||||
return () => window.removeEventListener("resize", checkWidth);
|
return () => window.removeEventListener("resize", checkWidth);
|
||||||
}, [sightName]);
|
}, [sightName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (localSelectedSightId !== sight.id) {
|
||||||
|
setIsExpanded(false);
|
||||||
|
}
|
||||||
|
}, [localSelectedSightId, sight.id]);
|
||||||
|
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
const newExpanded = !isExpanded;
|
const newExpanded = !isExpanded;
|
||||||
setIsExpanded(newExpanded);
|
setIsExpanded(newExpanded);
|
||||||
@@ -96,16 +102,19 @@ const SightItem = ({
|
|||||||
const stations = sightStationsCache.get(cacheKey) || [];
|
const stations = sightStationsCache.get(cacheKey) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
|
className={
|
||||||
|
localSelectedSightId === sight.id
|
||||||
|
? "side-menu-sight-selected-wrapper"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
id={`sight-${sight.id}`}
|
id={`sight-${sight.id}`}
|
||||||
onPointerDown={(e) => handlePointerDown(e, sight.id)}
|
onPointerDown={(e) => handlePointerDown(e, sight.id)}
|
||||||
onPointerUp={(e) => handlePointerUp(e, sight.id, handleClick)}
|
onPointerUp={(e) => handlePointerUp(e, sight.id, handleClick)}
|
||||||
className={`side-menu-sight pointer ${
|
className="side-menu-sight pointer"
|
||||||
localSelectedSightId === sight.id ? "selected" : ""
|
|
||||||
}`}
|
|
||||||
title={sightName}
|
|
||||||
>
|
>
|
||||||
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
||||||
{sightName}
|
{sightName}
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ const SightItem = ({
|
|||||||
className={`side-menu-sight pointer ${
|
className={`side-menu-sight pointer ${
|
||||||
localSelectedSightId === sight.id ? "selected" : ""
|
localSelectedSightId === sight.id ? "selected" : ""
|
||||||
}`}
|
}`}
|
||||||
title={sightName}
|
|
||||||
>
|
>
|
||||||
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
||||||
{sightName}
|
{sightName}
|
||||||
|
|||||||
@@ -11,6 +11,42 @@ import { apiStore } from "../../api/ApiStore/store";
|
|||||||
import { useClickDetection } from "../../hooks/useClickDetection";
|
import { useClickDetection } from "../../hooks/useClickDetection";
|
||||||
import { TouchableLayout } from "../TouchableLayout";
|
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 (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="side-menu-sight-transfer pointer"
|
||||||
|
style={style}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
>
|
||||||
|
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const StationItem = ({
|
const StationItem = ({
|
||||||
station,
|
station,
|
||||||
handlePointerDown,
|
handlePointerDown,
|
||||||
@@ -75,7 +111,13 @@ const StationItem = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
|
className={
|
||||||
|
selectedStationId === station.id
|
||||||
|
? "side-menu-sight-selected-wrapper"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="side-menu-sight"
|
className="side-menu-sight"
|
||||||
@@ -88,7 +130,6 @@ const StationItem = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title={station.name}
|
|
||||||
>
|
>
|
||||||
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
<span ref={textRef} className={shouldAnimate ? "marquee-text" : ""}>
|
||||||
{station.name}
|
{station.name}
|
||||||
@@ -101,9 +142,9 @@ const StationItem = ({
|
|||||||
>
|
>
|
||||||
{sights.length > 0 ? (
|
{sights.length > 0 ? (
|
||||||
sights.map((sight, index) => (
|
sights.map((sight, index) => (
|
||||||
<div
|
<SightTransferItem
|
||||||
key={sight.id}
|
key={sight.id}
|
||||||
className="side-menu-sight-transfer pointer"
|
name={getSightName(sight)}
|
||||||
style={{
|
style={{
|
||||||
borderBottom:
|
borderBottom:
|
||||||
index < sights.length - 1
|
index < sights.length - 1
|
||||||
@@ -115,19 +156,11 @@ const StationItem = ({
|
|||||||
onPointerUp={(e) => {
|
onPointerUp={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onSightClick) {
|
if (onSightClick) {
|
||||||
// Вычисляем позицию элемента для правильного позиционирования левого виджета
|
const elementRect = e.currentTarget.getBoundingClientRect();
|
||||||
const element = e.currentTarget;
|
onSightClick(sight.id, elementRect.top);
|
||||||
const elementRect = element.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Используем позицию элемента относительно viewport (elementRect.top)
|
|
||||||
// чтобы верхняя граница виджета совпадала с верхней границей элемента
|
|
||||||
const elementTop = elementRect.top;
|
|
||||||
onSightClick(sight.id, elementTop);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{getSightName(sight)}
|
|
||||||
</div>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="side-menu-sight-transfer-empty">
|
<div className="side-menu-sight-transfer-empty">
|
||||||
|
|||||||
@@ -1,13 +1,50 @@
|
|||||||
import '../../styles/AppealWidget.css'
|
import { useRef, useEffect } from "react";
|
||||||
|
import "../../styles/AppealWidget.css";
|
||||||
|
import { TouchableLayout } from "../TouchableLayout";
|
||||||
|
import { ReactMarkdownComponent } from "../ReactMarkdown";
|
||||||
|
|
||||||
|
function AppealWidget({
|
||||||
|
widgetImgPath,
|
||||||
|
widgetLabel,
|
||||||
|
widgetText,
|
||||||
|
style,
|
||||||
|
isOpen,
|
||||||
|
}) {
|
||||||
|
const stopProp = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
const layoutRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && layoutRef.current) {
|
||||||
|
const scrollable = layoutRef.current.querySelector(".scrollable");
|
||||||
|
if (scrollable) scrollable.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
function AppealWidget({widgetImgPath, widgetLabel, widgetText, style}) {
|
|
||||||
return (
|
return (
|
||||||
<div style={style} className='dynamic-widget'>
|
<div
|
||||||
<img className='dynamic-widget-image' src={widgetImgPath} />
|
style={style}
|
||||||
<div className='dynamic-widget-label'>{widgetLabel}</div>
|
className="dynamic-widget"
|
||||||
<div className='dynamic-widget-text'>{widgetText}</div>
|
onPointerDown={stopProp}
|
||||||
|
onPointerMove={stopProp}
|
||||||
|
onPointerUp={stopProp}
|
||||||
|
>
|
||||||
|
{widgetImgPath && (
|
||||||
|
<img className="dynamic-widget-image" src={widgetImgPath} />
|
||||||
|
)}
|
||||||
|
<div className="dynamic-widget-label">{widgetLabel}</div>
|
||||||
|
<TouchableLayout
|
||||||
|
ref={layoutRef}
|
||||||
|
className="dynamic-widget-text-scroll"
|
||||||
|
>
|
||||||
|
<div className="dynamic-widget-text">
|
||||||
|
<ReactMarkdownComponent value={widgetText} />
|
||||||
|
</div>
|
||||||
|
</TouchableLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppealWidget
|
export default AppealWidget;
|
||||||
|
|||||||
@@ -92,16 +92,14 @@ const RouteWidget = observer(() => {
|
|||||||
className={`route-widget-label ${
|
className={`route-widget-label ${
|
||||||
shouldAnimate(startStation?.name, 18) ? "marquee" : ""
|
shouldAnimate(startStation?.name, 18) ? "marquee" : ""
|
||||||
} ${getLabelSizeClass(startStation?.name)}`}
|
} ${getLabelSizeClass(startStation?.name)}`}
|
||||||
title={startStation?.name}
|
>
|
||||||
>
|
|
||||||
{startStation?.name}
|
{startStation?.name}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`route-widget-label ${
|
className={`route-widget-label ${
|
||||||
shouldAnimate(endStation?.name, 18) ? "marquee" : ""
|
shouldAnimate(endStation?.name, 18) ? "marquee" : ""
|
||||||
} ${getLabelSizeClass(endStation?.name)}`}
|
} ${getLabelSizeClass(endStation?.name)}`}
|
||||||
title={endStation?.name}
|
>
|
||||||
>
|
|
||||||
{endStation?.name}
|
{endStation?.name}
|
||||||
</div>
|
</div>
|
||||||
{(selectedLanguage === "en" || selectedLanguage === "ru") && (
|
{(selectedLanguage === "en" || selectedLanguage === "ru") && (
|
||||||
@@ -109,8 +107,7 @@ const RouteWidget = observer(() => {
|
|||||||
className={`route-widget-subtitle ${
|
className={`route-widget-subtitle ${
|
||||||
shouldAnimate(routeEnSubtitle, 50) ? "marquee" : ""
|
shouldAnimate(routeEnSubtitle, 50) ? "marquee" : ""
|
||||||
}`}
|
}`}
|
||||||
title={routeEnSubtitle}
|
>
|
||||||
>
|
|
||||||
{routeEnSubtitle}
|
{routeEnSubtitle}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -119,8 +116,7 @@ const RouteWidget = observer(() => {
|
|||||||
className={`route-widget-subtitle ${
|
className={`route-widget-subtitle ${
|
||||||
shouldAnimate(routeZhSubtitle, 50) ? "marquee" : ""
|
shouldAnimate(routeZhSubtitle, 50) ? "marquee" : ""
|
||||||
}`}
|
}`}
|
||||||
title={routeZhSubtitle}
|
>
|
||||||
>
|
|
||||||
{routeZhSubtitle}
|
{routeZhSubtitle}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Canvas, useThree } from "@react-three/fiber";
|
import { Canvas, useThree, useFrame } from "@react-three/fiber";
|
||||||
import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
|
import { OrbitControls, Center, useGLTF } from "@react-three/drei";
|
||||||
import React, { useEffect, Suspense } from "react";
|
import React, { useEffect, useRef, Suspense, useCallback } from "react";
|
||||||
import { BACKGROUND_COLOR } from "../../assets/Constants";
|
import { BACKGROUND_COLOR } from "../../assets/Constants";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
|
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
|
||||||
@@ -23,6 +23,7 @@ interface ThreeViewProps {
|
|||||||
const ZOOM_FACTOR = 1.2;
|
const ZOOM_FACTOR = 1.2;
|
||||||
const MIN_DISTANCE = 1;
|
const MIN_DISTANCE = 1;
|
||||||
const MAX_DISTANCE = 100;
|
const MAX_DISTANCE = 100;
|
||||||
|
const CAMERA_FOV = 40;
|
||||||
|
|
||||||
const TouchController = () => {
|
const TouchController = () => {
|
||||||
const { camera, controls, gl } = useThree();
|
const { camera, controls, gl } = useThree();
|
||||||
@@ -197,6 +198,47 @@ const AutoResize = () => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FitCamera = ({
|
||||||
|
groupRef,
|
||||||
|
onReady,
|
||||||
|
}: {
|
||||||
|
groupRef: React.RefObject<THREE.Group>;
|
||||||
|
onReady: () => void;
|
||||||
|
}) => {
|
||||||
|
const { camera, controls } = useThree();
|
||||||
|
const fitted = useRef(false);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (fitted.current) return;
|
||||||
|
const group = groupRef.current;
|
||||||
|
if (!group || group.children.length === 0) return;
|
||||||
|
|
||||||
|
const box = new THREE.Box3().setFromObject(group);
|
||||||
|
const sphere = new THREE.Sphere();
|
||||||
|
box.getBoundingSphere(sphere);
|
||||||
|
|
||||||
|
if (sphere.radius === 0) return;
|
||||||
|
|
||||||
|
const fov = THREE.MathUtils.degToRad(CAMERA_FOV);
|
||||||
|
const dist = sphere.radius / Math.sin(fov / 2);
|
||||||
|
|
||||||
|
camera.position.set(0, 0, dist);
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
if (controls) {
|
||||||
|
const orbit = controls as unknown as OrbitControlsImpl;
|
||||||
|
orbit.target.set(0, 0, 0);
|
||||||
|
orbit.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
fitted.current = true;
|
||||||
|
onReady();
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const Model = ({
|
const Model = ({
|
||||||
fileUrl,
|
fileUrl,
|
||||||
onLoad,
|
onLoad,
|
||||||
@@ -233,21 +275,37 @@ export const ThreeView: React.FC<ThreeViewProps> = ({
|
|||||||
onError,
|
onError,
|
||||||
controlRef,
|
controlRef,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isReady, setIsReady] = React.useState(false);
|
||||||
|
const groupRef = useRef<THREE.Group>(null!);
|
||||||
|
|
||||||
|
const handleReady = useCallback(() => {
|
||||||
|
setIsReady(true);
|
||||||
|
onLoad?.();
|
||||||
|
}, [onLoad]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width, height, position: "relative", overflow: "hidden" }}>
|
<div style={{ width, height, position: "relative", overflow: "hidden" }}>
|
||||||
|
{!isReady && (
|
||||||
|
<div style={{
|
||||||
|
position: "absolute", inset: 0,
|
||||||
|
backgroundColor: `#${BACKGROUND_COLOR.toString(16).padStart(6, "0")}`,
|
||||||
|
zIndex: 1,
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
<Canvas
|
<Canvas
|
||||||
gl={{
|
gl={{
|
||||||
antialias: true,
|
antialias: true,
|
||||||
toneMappingExposure: 1.5,
|
toneMappingExposure: 1.5,
|
||||||
outputColorSpace: THREE.SRGBColorSpace,
|
outputColorSpace: THREE.SRGBColorSpace,
|
||||||
}}
|
}}
|
||||||
camera={{ position: [0, 0, 5], fov: 40 }}
|
camera={{ position: [0, 0, 50], fov: CAMERA_FOV }}
|
||||||
style={{ width: "100%", height: "100%" }}
|
style={{ width: "100%", height: "100%" }}
|
||||||
onError={(e: any) => onError?.(e.message)}
|
onError={(e: any) => onError?.(e.message)}
|
||||||
>
|
>
|
||||||
<AutoResize />
|
<AutoResize />
|
||||||
<TouchController />
|
<TouchController />
|
||||||
{controlRef && <ZoomController controlRef={controlRef} />}
|
{controlRef && <ZoomController controlRef={controlRef} />}
|
||||||
|
<FitCamera groupRef={groupRef} onReady={handleReady} />
|
||||||
<color attach="background" args={[BACKGROUND_COLOR]} />
|
<color attach="background" args={[BACKGROUND_COLOR]} />
|
||||||
<ambientLight intensity={0.8} />
|
<ambientLight intensity={0.8} />
|
||||||
<directionalLight position={[30, 30, 30]} intensity={1.2} />
|
<directionalLight position={[30, 30, 30]} intensity={1.2} />
|
||||||
@@ -265,23 +323,18 @@ export const ThreeView: React.FC<ThreeViewProps> = ({
|
|||||||
<pointLight position={[0, 30, 0]} intensity={0.6} />
|
<pointLight position={[0, 30, 0]} intensity={0.6} />
|
||||||
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Stage
|
<Center precise>
|
||||||
environment={null}
|
<group ref={groupRef}>
|
||||||
intensity={1}
|
<Model fileUrl={fileUrl} />
|
||||||
castShadow={false}
|
</group>
|
||||||
shadows={false}
|
</Center>
|
||||||
adjustCamera={true}
|
|
||||||
center={{ precise: true }}
|
|
||||||
>
|
|
||||||
<Model fileUrl={fileUrl} onLoad={onLoad} />
|
|
||||||
</Stage>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
makeDefault
|
makeDefault
|
||||||
enableZoom={true}
|
enableZoom={true}
|
||||||
enablePan={true}
|
enablePan={true}
|
||||||
target={[50, 50, 50]}
|
target={[0, 0, 0]}
|
||||||
minDistance={1}
|
minDistance={1}
|
||||||
maxDistance={100}
|
maxDistance={100}
|
||||||
enableDamping={true}
|
enableDamping={true}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
.side-menu-sights-block {
|
.side-menu-sights-block {
|
||||||
height: calc(60%);
|
height: calc(60%);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
margin-left: 20px;
|
margin-left: 0;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-right: 5px;
|
padding-right: 5px;
|
||||||
touch-action: none; /* Отключаем стандартные действия */
|
touch-action: none; /* Отключаем стандартные действия */
|
||||||
overscroll-behavior: contain; /* Предотвращаем прокрутку родительских элементов */
|
overscroll-behavior: contain; /* Предотвращаем прокрутку родительских элементов */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,45 @@
|
|||||||
import { makeAutoObservable, runInAction } from "mobx";
|
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 TRANSITION_DURATION = 60000;
|
||||||
const TICK_INTERVAL = 100;
|
const TICK_INTERVAL = 100;
|
||||||
const TICK_STEP = TICK_INTERVAL / TRANSITION_DURATION;
|
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 {
|
interface ColorStore {
|
||||||
currentColor: string;
|
currentColor: string;
|
||||||
setCurrentColor: (color: string) => void;
|
setCurrentColor: (color: string) => void;
|
||||||
|
setMainColor: (hex: string) => void;
|
||||||
startColorAnimation: () => void;
|
startColorAnimation: () => void;
|
||||||
stopColorAnimation: () => void;
|
stopColorAnimation: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ColorStore implements ColorStore {
|
class ColorStore implements ColorStore {
|
||||||
currentColor: string = "#fff";
|
currentColor: string = "#fff";
|
||||||
|
private mainColor: { r: number; g: number; b: number } = DEFAULT_MAIN;
|
||||||
private progress: number = 0;
|
private progress: number = 0;
|
||||||
private direction: number = 1;
|
private direction: number = 1;
|
||||||
private tickInterval: ReturnType<typeof setInterval> | null = null;
|
private tickInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -28,12 +52,12 @@ class ColorStore implements ColorStore {
|
|||||||
this.currentColor = color;
|
this.currentColor = color;
|
||||||
};
|
};
|
||||||
|
|
||||||
private interpolateColor(progress: number): string {
|
setMainColor = (hex: string) => {
|
||||||
const h = Math.round(COLOR_WHITE.h + (COLOR_GREEN.h - COLOR_WHITE.h) * progress);
|
const parsed = hexToRgb(hex);
|
||||||
const s = Math.round(COLOR_WHITE.s + (COLOR_GREEN.s - COLOR_WHITE.s) * progress);
|
if (parsed) {
|
||||||
const l = Math.round(COLOR_WHITE.l + (COLOR_GREEN.l - COLOR_WHITE.l) * progress);
|
this.mainColor = parsed;
|
||||||
return `hsl(${h}, ${s}%, ${l}%)`;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
startColorAnimation = () => {
|
startColorAnimation = () => {
|
||||||
if (this.tickInterval) return;
|
if (this.tickInterval) return;
|
||||||
@@ -50,7 +74,7 @@ class ColorStore implements ColorStore {
|
|||||||
this.direction = 1;
|
this.direction = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentColor = this.interpolateColor(this.progress);
|
this.currentColor = interpolateRgb(WHITE, this.mainColor, this.progress);
|
||||||
});
|
});
|
||||||
}, TICK_INTERVAL);
|
}, TICK_INTERVAL);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,37 +5,124 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 420px;
|
width: 420px;
|
||||||
|
max-height: calc(100vh - 150px - 98px);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(
|
background:
|
||||||
|
linear-gradient(
|
||||||
114deg,
|
114deg,
|
||||||
rgba(255, 255, 255, 0) 8.71%,
|
rgba(255, 255, 255, 0) 8.71%,
|
||||||
rgba(255, 255, 255, 0.16) 69.69%
|
rgba(255, 255, 255, 0.16) 69.69%
|
||||||
),
|
),
|
||||||
#006F3A;
|
var(--carrier-left, #006f3a);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dynamic-widget-image {
|
.dynamic-widget-image {
|
||||||
border-radius-top-left: 10px;
|
padding-top: 2px;
|
||||||
border-radius-top-right: 10px;
|
margin-left: 2px;
|
||||||
padding-top: 4px;
|
margin-right: 2px;
|
||||||
margin-left: 4px;
|
width: 416px;
|
||||||
margin-right: 4px;
|
border-radius: 10px 10px 0 0;
|
||||||
width: 412px;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dynamic-widget-label {
|
.dynamic-widget-label {
|
||||||
width: 380px;
|
width: 100%;
|
||||||
margin-top: 29px;
|
padding: 10px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
border-bottom: 1px solid var(--Glass-stroke, rgba(255, 255, 255, 0.8));
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
),
|
||||||
|
rgba(var(--carrier-right-menu-rgb, 179, 165, 152), 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text-scroll.scrollable-container {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
align-self: stretch;
|
||||||
|
margin: 15px 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text-scroll .scrollable-viewport {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text-scroll .scrollable {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.dynamic-widget-text {
|
.dynamic-widget-text {
|
||||||
margin-top: 16px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
width: 380px;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
line-height: 150%;
|
line-height: 135%;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text .react-markdown-container {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 135%;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text .react-markdown-container p {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 135%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text .react-markdown-container p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text .react-markdown-container h1,
|
||||||
|
.dynamic-widget-text .react-markdown-container h2,
|
||||||
|
.dynamic-widget-text .react-markdown-container h3,
|
||||||
|
.dynamic-widget-text .react-markdown-container h4,
|
||||||
|
.dynamic-widget-text .react-markdown-container h5,
|
||||||
|
.dynamic-widget-text .react-markdown-container h6 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text .react-markdown-container ul,
|
||||||
|
.dynamic-widget-text .react-markdown-container ol {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text .react-markdown-container li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text .react-markdown-container blockquote {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 3px solid rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text .react-markdown-container img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-widget-text .react-markdown-container a {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
rgba(255, 255, 255, 0) 8.71%,
|
rgba(255, 255, 255, 0) 8.71%,
|
||||||
rgba(255, 255, 255, 0.16) 69.69%
|
rgba(255, 255, 255, 0.16) 69.69%
|
||||||
),
|
),
|
||||||
#006F3A;
|
var(--carrier-left, #006F3A);
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
}
|
}
|
||||||
@@ -67,17 +67,78 @@
|
|||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-widget-text {
|
.left-widget-text-scroll.scrollable-container {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text-scroll .scrollable-viewport {
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: "Roboto";
|
font-family: "Roboto";
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
line-height: 135%;
|
line-height: 135%;
|
||||||
max-height: 200px; /* Пример ограничения высоты */
|
padding-right: 3px;
|
||||||
overflow-y: auto;
|
}
|
||||||
touch-action: none;
|
|
||||||
overscroll-behavior: contain;
|
.left-widget-text .react-markdown-container {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 135%;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text .react-markdown-container p {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 135%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text .react-markdown-container p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text .react-markdown-container h1,
|
||||||
|
.left-widget-text .react-markdown-container h2,
|
||||||
|
.left-widget-text .react-markdown-container h3,
|
||||||
|
.left-widget-text .react-markdown-container h4,
|
||||||
|
.left-widget-text .react-markdown-container h5,
|
||||||
|
.left-widget-text .react-markdown-container h6 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text .react-markdown-container ul,
|
||||||
|
.left-widget-text .react-markdown-container ol {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text .react-markdown-container li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text .react-markdown-container blockquote {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 3px solid rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text .react-markdown-container img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-widget-text .react-markdown-container a {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-widget-image {
|
.left-widget-image {
|
||||||
@@ -93,6 +154,16 @@
|
|||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
width: 100%;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Анимация для списка пересадок */
|
/* Анимация для списка пересадок */
|
||||||
|
|||||||
@@ -1,3 +1,45 @@
|
|||||||
|
@property --fade-top {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --fade-bottom {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-chevron {
|
||||||
|
0% {
|
||||||
|
transform: rotate(var(--r, 0deg)) translateY(0px) scale(1);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: rotate(var(--r, 0deg)) translateY(-4px) scale(1.12);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: rotate(var(--r, 0deg)) translateY(-5px) scale(1.14);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(var(--r, 0deg)) translateY(0px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron-svg {
|
||||||
|
font-size: 20px;
|
||||||
|
animation: pulse-chevron 1.2s ease-in-out infinite;
|
||||||
|
animation-play-state: paused;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron-svg.is-idle {
|
||||||
|
animation-play-state: running;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron-svg.is-open {
|
||||||
|
--r: 180deg;
|
||||||
|
}
|
||||||
|
|
||||||
.right-widget {
|
.right-widget {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 32px;
|
right: 32px;
|
||||||
@@ -17,7 +59,7 @@
|
|||||||
rgba(255, 255, 255, 0) 8.71%,
|
rgba(255, 255, 255, 0) 8.71%,
|
||||||
rgba(255, 255, 255, 0.16) 69.69%
|
rgba(255, 255, 255, 0.16) 69.69%
|
||||||
),
|
),
|
||||||
#006f3a;
|
var(--carrier-right, #806c59);
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
max-height: 68px;
|
max-height: 68px;
|
||||||
@@ -63,7 +105,11 @@
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
width: 128px;
|
width: 128px;
|
||||||
|
|
||||||
background-color: #0e8953;
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--carrier-right, #806c59) 80%,
|
||||||
|
black
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-of-sights-title {
|
.list-of-sights-title {
|
||||||
@@ -90,6 +136,27 @@
|
|||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-of-sights-content .scrollable {
|
||||||
|
--fade-top: 0px;
|
||||||
|
--fade-bottom: 45px;
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0px,
|
||||||
|
black var(--fade-top),
|
||||||
|
black calc(100% - var(--fade-bottom)),
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
transition: --fade-top 0.5s ease, --fade-bottom 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-of-sights-content:not(.is-at-top) .scrollable {
|
||||||
|
--fade-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-of-sights-content.is-at-bottom .scrollable {
|
||||||
|
--fade-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.list-of-sights-grid {
|
.list-of-sights-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
@@ -103,6 +170,11 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-of-sights-content .custom-scrollbar-track {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.sight-component {
|
.sight-component {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -194,7 +266,7 @@
|
|||||||
rgba(255, 255, 255, 0) 8.71%,
|
rgba(255, 255, 255, 0) 8.71%,
|
||||||
rgba(255, 255, 255, 0.16) 69.69%
|
rgba(255, 255, 255, 0.16) 69.69%
|
||||||
),
|
),
|
||||||
#006f3a;
|
var(--carrier-right, #806c59);
|
||||||
max-height: calc(100vh - 128px);
|
max-height: calc(100vh - 128px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +309,7 @@
|
|||||||
rgba(255, 255, 255, 0.22) 0%,
|
rgba(255, 255, 255, 0.22) 0%,
|
||||||
rgba(255, 255, 255, 0.04) 100%
|
rgba(255, 255, 255, 0.04) 100%
|
||||||
),
|
),
|
||||||
rgba(0, 111, 58, 0.72);
|
rgba(var(--carrier-right-rgb, 128, 108, 89), 0.72);
|
||||||
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
|
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -246,6 +318,16 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sight-frame-title:not(.intro-title) {
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
),
|
||||||
|
rgba(var(--carrier-right-menu-rgb, 179, 165, 152), 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.sight-frame-title p {
|
.sight-frame-title p {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
@@ -304,7 +386,7 @@
|
|||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
transparent 35%,
|
transparent 35%,
|
||||||
#0e8953 50%,
|
color-mix(in srgb, var(--carrier-right, #806c59) 80%, black) 50%,
|
||||||
transparent 65%
|
transparent 65%
|
||||||
);
|
);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@@ -326,13 +408,51 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sight-frame-menu {
|
.sight-frame-menu-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 7px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sight-frame-menu-fade {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 120px;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sight-frame-menu-fade.left {
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(var(--carrier-right-menu-rgb, 179, 165, 152), 0.95),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
border-radius: 0 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sight-frame-menu-fade.right {
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
to left,
|
||||||
|
rgba(var(--carrier-right-menu-rgb, 179, 165, 152), 0.95),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
border-radius: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sight-frame-menu {
|
||||||
|
z-index: 10000;
|
||||||
|
position: relative;
|
||||||
|
padding: 7px 60px;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-evenly;
|
||||||
border-radius: 0px 0px 10px 10px;
|
border-radius: 0px 0px 10px 10px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.8);
|
border-top: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
background:
|
background:
|
||||||
@@ -341,11 +461,22 @@
|
|||||||
rgba(255, 255, 255, 0.2) 0%,
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
rgba(255, 255, 255, 0) 100%
|
rgba(255, 255, 255, 0) 100%
|
||||||
),
|
),
|
||||||
rgba(0, 111, 58, 0.4);
|
rgba(var(--carrier-right-menu-rgb, 179, 165, 152), 0.4);
|
||||||
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
|
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sight-frame-menu::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sight-frame-menu {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sight-frame-menu-point {
|
.sight-frame-menu-point {
|
||||||
@@ -356,16 +487,17 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.1s ease,
|
background-color 0.1s ease,
|
||||||
color 0.1s ease;
|
color 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sight-frame-menu-point.active {
|
.sight-frame-menu-point.active {
|
||||||
border-bottom: 2px solid #fff;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
border-bottom-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sight-frame-text-wrapper::-webkit-scrollbar-track {
|
.sight-frame-text-wrapper::-webkit-scrollbar-track {
|
||||||
@@ -519,7 +651,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alphabet {
|
.alphabet {
|
||||||
width: 100px;
|
width: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -579,8 +712,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alphabet-position {
|
.alphabet-position {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transfer-button-container {
|
.transfer-button-container {
|
||||||
@@ -607,14 +741,14 @@
|
|||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid #006f3a;
|
border: 1px solid var(--carrier-main, #006f3a);
|
||||||
background:
|
background:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgba(255, 255, 255, 0.2) 0%,
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
rgba(255, 255, 255, 0) 100%
|
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;
|
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
@@ -747,7 +881,7 @@
|
|||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
background: #006f3a;
|
background: var(--carrier-right, #806c59);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,15 +26,17 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset,
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.3) inset,
|
||||||
/* Внутренняя рамка */ 4px 4px 12px 0 rgba(255, 255, 255, 0.12) inset; /* Ваш существующий внутренний shadow */
|
/* Внутренняя рамка */ 4px 4px 12px 0 rgba(255, 255, 255, 0.12) inset; /* Ваш существующий внутренний shadow */
|
||||||
padding: 1px; /* Чтобы контент не прилипал к рамке */
|
padding: 1px; /* Чтобы контент не прилипал к рамке */
|
||||||
background: linear-gradient(
|
background:
|
||||||
|
linear-gradient(
|
||||||
to bottom right,
|
to bottom right,
|
||||||
rgba(255, 255, 255, 0.2) 0%,
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
rgba(255, 255, 255, 0) 100%
|
rgba(255, 255, 255, 0) 100%
|
||||||
),
|
),
|
||||||
rgba(0, 111, 58, 0.4);
|
rgba(179, 165, 152, 0.4);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
z-index: 10000001;
|
z-index: 10000001;
|
||||||
@@ -50,7 +52,8 @@
|
|||||||
height: 96px;
|
height: 96px;
|
||||||
background-color: #fcd500;
|
background-color: #fcd500;
|
||||||
color: black;
|
color: black;
|
||||||
border-radius: 10px;
|
border-top-left-radius: 10px;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
rgba(255, 255, 255, 0.2) 0%,
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
rgba(255, 255, 255, 0) 100%
|
rgba(255, 255, 255, 0) 100%
|
||||||
),
|
),
|
||||||
#006f3a;
|
var(--carrier-left, #006f3a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-menu-label {
|
.side-menu-label {
|
||||||
@@ -35,11 +35,13 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-top: 120px;
|
margin-top: 120px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
width: 220px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-menu-buttons {
|
.side-menu-buttons {
|
||||||
width: 220px;
|
width: 220px;
|
||||||
margin-top: 260px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-menu-button {
|
.side-menu-button {
|
||||||
@@ -51,10 +53,6 @@
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-menu-button--sights {
|
|
||||||
background-color: #fcd500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-menu-button--active {
|
.side-menu-button--active {
|
||||||
background-color: #fcd500;
|
background-color: #fcd500;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -138,10 +136,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
3.33% {
|
3.33% {
|
||||||
fill: rgb(76, 175, 75);
|
fill: var(--carrier-left, rgb(76, 175, 75));
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
fill: rgb(76, 175, 75);
|
fill: var(--carrier-left, rgb(76, 175, 75));
|
||||||
}
|
}
|
||||||
53.33% {
|
53.33% {
|
||||||
fill: #ffffff;
|
fill: #ffffff;
|
||||||
@@ -191,7 +189,7 @@
|
|||||||
top: -2px;
|
top: -2px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 7px;
|
height: 7px;
|
||||||
background-color: #0e8953;
|
background-color: color-mix(in srgb, var(--carrier-left, #006f3a) 80%, black);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +205,7 @@
|
|||||||
rgba(255, 255, 255, 0) 8.71%,
|
rgba(255, 255, 255, 0) 8.71%,
|
||||||
rgba(255, 255, 255, 0.16) 69.69%
|
rgba(255, 255, 255, 0.16) 69.69%
|
||||||
),
|
),
|
||||||
#006f3a;
|
var(--carrier-left, #006f3a);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 288px;
|
width: 288px;
|
||||||
transform: translateY(100%);
|
transform: translateY(100%);
|
||||||
@@ -231,12 +229,10 @@
|
|||||||
.side-menu-sights-block {
|
.side-menu-sights-block {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin-left: 20px;
|
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
width: auto;
|
width: 100%;
|
||||||
max-width: calc(100% - 20px);
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -244,10 +240,10 @@
|
|||||||
|
|
||||||
.side-menu-sight {
|
.side-menu-sight {
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
margin-right: 20px;
|
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
margin-top: 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-family: "Roboto";
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
@@ -257,6 +253,12 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-menu-sight-selected-wrapper {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
margin-left: -20px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.side-menu-sight > span {
|
.side-menu-sight > span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar-thumb {
|
.custom-scrollbar-thumb {
|
||||||
@@ -62,11 +63,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-menu-sights-block .scrollable-viewport {
|
.side-menu-sights-block .scrollable-viewport {
|
||||||
height: calc(92%);
|
height: calc(98%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-menu-sights-block .scrollable {
|
.side-menu-sights-block .scrollable {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-of-sights-content .scrollable-viewport {
|
.list-of-sights-content .scrollable-viewport {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
rgba(255, 255, 255, 0.2) 0%,
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
rgba(255, 255, 255, 0) 100%
|
rgba(255, 255, 255, 0) 100%
|
||||||
),
|
),
|
||||||
rgba(0, 111, 58, 0.4);
|
rgba(179, 165, 152, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather-widget-time {
|
.weather-widget-time {
|
||||||
|
|||||||
@@ -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 routePermissions = item.path ? ROUTE_REQUIRED_RESOURCES[item.path] ?? [] : [];
|
||||||
const canAccessRoute = routePermissions.every((permission) =>
|
const canAccessRoute = routePermissions.every((permission) =>
|
||||||
authStore.canAccess(permission),
|
authStore.canAccess(permission),
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
import { LanguageSwitcher } from "@widgets";
|
||||||
import { articlesStore } from "@shared";
|
import { articlesStore, selectedCityStore } from "@shared";
|
||||||
|
|
||||||
const ArticleCreatePage: React.FC = () => {
|
const ArticleCreatePage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { articleData } = articlesStore;
|
const { articleData } = articlesStore;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useEffect } from "react";
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
import { LanguageSwitcher } from "@widgets";
|
||||||
import { articlesStore, languageStore } from "@shared";
|
import { articlesStore, languageStore, selectedCityStore } from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
const ArticleEditPage: React.FC = observer(() => {
|
const ArticleEditPage: React.FC = observer(() => {
|
||||||
@@ -11,6 +11,11 @@ const ArticleEditPage: React.FC = observer(() => {
|
|||||||
|
|
||||||
const { articleData, getArticle } = articlesStore;
|
const { articleData, getArticle } = articlesStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
// Устанавливаем русский язык при загрузке страницы
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const ArticleListPage = observer(() => {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
fetchArticles();
|
fetchArticles();
|
||||||
}, [language]);
|
}, [language, selectedCityStore.cityVersion]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
@@ -55,11 +55,12 @@ export const ArticleListPage = observer(() => {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/article/${params.row.id}`)}>
|
<button title="Просмотр" onClick={() => navigate(`/article/${params.row.id}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
{canWriteArticles && (
|
{canWriteArticles && (
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(params.row.id);
|
||||||
@@ -107,7 +108,9 @@ export const ArticleListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<DataGrid
|
<DataGrid
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
languageStore,
|
languageStore,
|
||||||
isMediaIdEmpty,
|
isMediaIdEmpty,
|
||||||
useSelectedCity,
|
useSelectedCity,
|
||||||
|
selectedCityStore,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
@@ -27,6 +28,56 @@ import {
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ColorPickerField = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-center gap-3 w-full">
|
||||||
|
<div
|
||||||
|
className="w-10 h-10 rounded border border-gray-300 flex-shrink-0 cursor-pointer overflow-hidden relative"
|
||||||
|
style={{ backgroundColor: value || "#ffffff" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={value || "#ffffff"}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={label}
|
||||||
|
value={value}
|
||||||
|
placeholder="#000000"
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: value ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange("")}
|
||||||
|
className="text-gray-400 hover:text-gray-600 text-xs px-1"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
) : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const CarrierCreatePage = observer(() => {
|
export const CarrierCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { createCarrierData, setCreateCarrierData } = carrierStore;
|
const { createCarrierData, setCreateCarrierData } = carrierStore;
|
||||||
@@ -43,6 +94,11 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCities = async () => {
|
const fetchCities = async () => {
|
||||||
if (!authStore.me) {
|
if (!authStore.me) {
|
||||||
@@ -220,6 +276,69 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<ColorPickerField
|
||||||
|
label="Основной цвет"
|
||||||
|
value={createCarrierData.main_color}
|
||||||
|
onChange={(val) =>
|
||||||
|
setCreateCarrierData(
|
||||||
|
createCarrierData[language].full_name,
|
||||||
|
createCarrierData[language].short_name,
|
||||||
|
createCarrierData.city_id,
|
||||||
|
createCarrierData[language].slogan,
|
||||||
|
selectedMediaId || "",
|
||||||
|
language,
|
||||||
|
{ ...colorFields(createCarrierData), main_color: val }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 pl-1">
|
||||||
|
Используется в: виджет обращений, значки на карте, скопление достопримечательностей на карте, информационный виджет
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<ColorPickerField
|
||||||
|
label="Левый цвет"
|
||||||
|
value={createCarrierData.left_color}
|
||||||
|
onChange={(val) =>
|
||||||
|
setCreateCarrierData(
|
||||||
|
createCarrierData[language].full_name,
|
||||||
|
createCarrierData[language].short_name,
|
||||||
|
createCarrierData.city_id,
|
||||||
|
createCarrierData[language].slogan,
|
||||||
|
selectedMediaId || "",
|
||||||
|
language,
|
||||||
|
{ ...colorFields(createCarrierData), left_color: val }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 pl-1">
|
||||||
|
Используется в: боковое меню, левый виджет достопримечательности
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<ColorPickerField
|
||||||
|
label="Правый цвет"
|
||||||
|
value={createCarrierData.right_color}
|
||||||
|
onChange={(val) =>
|
||||||
|
setCreateCarrierData(
|
||||||
|
createCarrierData[language].full_name,
|
||||||
|
createCarrierData[language].short_name,
|
||||||
|
createCarrierData.city_id,
|
||||||
|
createCarrierData[language].slogan,
|
||||||
|
selectedMediaId || "",
|
||||||
|
language,
|
||||||
|
{ ...colorFields(createCarrierData), right_color: val }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 pl-1">
|
||||||
|
Используется в: список достопримечательностей, страница достопримечательности
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Логотип перевозчика"
|
title="Логотип перевозчика"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
languageStore,
|
languageStore,
|
||||||
isMediaIdEmpty,
|
isMediaIdEmpty,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { ImageUploadCard, LanguageSwitcher, DeleteModal } from "@widgets";
|
import { ImageUploadCard, LanguageSwitcher, DeleteModal } from "@widgets";
|
||||||
@@ -30,6 +31,60 @@ import {
|
|||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ColorPickerField = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-center gap-3 w-full">
|
||||||
|
<div
|
||||||
|
className="w-10 h-10 rounded border border-gray-300 flex-shrink-0 cursor-pointer overflow-hidden relative"
|
||||||
|
style={{ backgroundColor: value || "#ffffff" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={value || "#ffffff"}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={label}
|
||||||
|
value={value}
|
||||||
|
placeholder="#000000"
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: value ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange("")}
|
||||||
|
className="text-gray-400 hover:text-gray-600 text-xs px-1"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
) : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const CarrierEditPage = observer(() => {
|
export const CarrierEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@@ -49,6 +104,11 @@ export const CarrierEditPage = observer(() => {
|
|||||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -68,13 +128,19 @@ export const CarrierEditPage = observer(() => {
|
|||||||
const carrierData = await getCarrier(Number(id));
|
const carrierData = await getCarrier(Number(id));
|
||||||
|
|
||||||
if (carrierData) {
|
if (carrierData) {
|
||||||
|
const colors = {
|
||||||
|
main_color: carrierData.ru?.main_color || "",
|
||||||
|
left_color: carrierData.ru?.left_color || "",
|
||||||
|
right_color: carrierData.ru?.right_color || "",
|
||||||
|
};
|
||||||
setEditCarrierData(
|
setEditCarrierData(
|
||||||
carrierData.ru?.full_name || "",
|
carrierData.ru?.full_name || "",
|
||||||
carrierData.ru?.short_name || "",
|
carrierData.ru?.short_name || "",
|
||||||
carrierData.ru?.city_id || 0,
|
carrierData.ru?.city_id || 0,
|
||||||
carrierData.ru?.slogan || "",
|
carrierData.ru?.slogan || "",
|
||||||
carrierData.ru?.logo || "",
|
carrierData.ru?.logo || "",
|
||||||
"ru"
|
"ru",
|
||||||
|
colors,
|
||||||
);
|
);
|
||||||
setEditCarrierData(
|
setEditCarrierData(
|
||||||
carrierData.en?.full_name || "",
|
carrierData.en?.full_name || "",
|
||||||
@@ -82,7 +148,7 @@ export const CarrierEditPage = observer(() => {
|
|||||||
carrierData.en?.city_id || 0,
|
carrierData.en?.city_id || 0,
|
||||||
carrierData.en?.slogan || "",
|
carrierData.en?.slogan || "",
|
||||||
carrierData.en?.logo || "",
|
carrierData.en?.logo || "",
|
||||||
"en"
|
"en",
|
||||||
);
|
);
|
||||||
setEditCarrierData(
|
setEditCarrierData(
|
||||||
carrierData.zh?.full_name || "",
|
carrierData.zh?.full_name || "",
|
||||||
@@ -90,7 +156,7 @@ export const CarrierEditPage = observer(() => {
|
|||||||
carrierData.zh?.city_id || 0,
|
carrierData.zh?.city_id || 0,
|
||||||
carrierData.zh?.slogan || "",
|
carrierData.zh?.slogan || "",
|
||||||
carrierData.zh?.logo || "",
|
carrierData.zh?.logo || "",
|
||||||
"zh"
|
"zh",
|
||||||
);
|
);
|
||||||
setInitialCityName(carrierData.ru?.city || "");
|
setInitialCityName(carrierData.ru?.city || "");
|
||||||
}
|
}
|
||||||
@@ -129,7 +195,7 @@ export const CarrierEditPage = observer(() => {
|
|||||||
editCarrierData.city_id,
|
editCarrierData.city_id,
|
||||||
editCarrierData[language].slogan,
|
editCarrierData[language].slogan,
|
||||||
media.id,
|
media.id,
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,7 +277,7 @@ export const CarrierEditPage = observer(() => {
|
|||||||
Number(e.target.value),
|
Number(e.target.value),
|
||||||
editCarrierData[language].slogan,
|
editCarrierData[language].slogan,
|
||||||
editCarrierData.logo,
|
editCarrierData.logo,
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -235,7 +301,7 @@ export const CarrierEditPage = observer(() => {
|
|||||||
editCarrierData.city_id,
|
editCarrierData.city_id,
|
||||||
editCarrierData[language].slogan,
|
editCarrierData[language].slogan,
|
||||||
editCarrierData.logo,
|
editCarrierData.logo,
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -252,7 +318,7 @@ export const CarrierEditPage = observer(() => {
|
|||||||
editCarrierData.city_id,
|
editCarrierData.city_id,
|
||||||
editCarrierData[language].slogan,
|
editCarrierData[language].slogan,
|
||||||
editCarrierData.logo,
|
editCarrierData.logo,
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -268,11 +334,77 @@ export const CarrierEditPage = observer(() => {
|
|||||||
editCarrierData.city_id,
|
editCarrierData.city_id,
|
||||||
e.target.value,
|
e.target.value,
|
||||||
editCarrierData.logo,
|
editCarrierData.logo,
|
||||||
language
|
language,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<ColorPickerField
|
||||||
|
label="Основной цвет"
|
||||||
|
value={editCarrierData.main_color}
|
||||||
|
onChange={(val) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData[language].full_name,
|
||||||
|
editCarrierData[language].short_name,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData[language].slogan,
|
||||||
|
editCarrierData.logo,
|
||||||
|
language,
|
||||||
|
{ ...colorFields(editCarrierData), main_color: val },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 pl-1">
|
||||||
|
Используется в: значки на карте, скопление достопримечательностей
|
||||||
|
на карте, информационный виджет
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<ColorPickerField
|
||||||
|
label="Левый цвет"
|
||||||
|
value={editCarrierData.left_color}
|
||||||
|
onChange={(val) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData[language].full_name,
|
||||||
|
editCarrierData[language].short_name,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData[language].slogan,
|
||||||
|
editCarrierData.logo,
|
||||||
|
language,
|
||||||
|
{ ...colorFields(editCarrierData), left_color: val },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 pl-1">
|
||||||
|
Используется в: виджет обращений, боковое меню (фон, список
|
||||||
|
остановок), левый виджет достопримечательности
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<ColorPickerField
|
||||||
|
label="Правый цвет"
|
||||||
|
value={editCarrierData.right_color}
|
||||||
|
onChange={(val) =>
|
||||||
|
setEditCarrierData(
|
||||||
|
editCarrierData[language].full_name,
|
||||||
|
editCarrierData[language].short_name,
|
||||||
|
editCarrierData.city_id,
|
||||||
|
editCarrierData[language].slogan,
|
||||||
|
editCarrierData.logo,
|
||||||
|
language,
|
||||||
|
{ ...colorFields(editCarrierData), right_color: val },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 pl-1">
|
||||||
|
Используется в: список достопримечательностей (фон, карточки),
|
||||||
|
правый виджет достопримечательности
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Логотип перевозчика"
|
title="Логотип перевозчика"
|
||||||
@@ -346,7 +478,7 @@ export const CarrierEditPage = observer(() => {
|
|||||||
editCarrierData.city_id,
|
editCarrierData.city_id,
|
||||||
editCarrierData[language].slogan,
|
editCarrierData[language].slogan,
|
||||||
"",
|
"",
|
||||||
language
|
language,
|
||||||
);
|
);
|
||||||
setIsDeleteLogoModalOpen(false);
|
setIsDeleteLogoModalOpen(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, carrierStore, cityStore, languageStore, SearchInput } from "@shared";
|
import { authStore, carrierStore, cityStore, languageStore, selectedCityStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||||
@@ -39,7 +39,7 @@ export const CarrierListPage = observer(() => {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [language]);
|
}, [language, selectedCityStore.cityVersion]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
@@ -98,10 +98,11 @@ export const CarrierListPage = observer(() => {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
|
<button title="Редактировать" onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(params.row.id);
|
||||||
@@ -161,7 +162,9 @@ export const CarrierListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -169,6 +172,11 @@ export const CarrierListPage = observer(() => {
|
|||||||
checkboxSelection={canWriteCarriers}
|
checkboxSelection={canWriteCarriers}
|
||||||
disableRowSelectionExcludeModel
|
disableRowSelectionExcludeModel
|
||||||
disableRowSelectionOnClick
|
disableRowSelectionOnClick
|
||||||
|
onRowDoubleClick={(params) => {
|
||||||
|
if (canWriteCarriers) {
|
||||||
|
navigate(`/carrier/${params.id}/edit`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
paginationModel={paginationModel}
|
paginationModel={paginationModel}
|
||||||
onPaginationModelChange={setPaginationModel}
|
onPaginationModelChange={setPaginationModel}
|
||||||
|
|||||||
@@ -17,16 +17,24 @@ import {
|
|||||||
countryStore,
|
countryStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
mediaStore,
|
mediaStore,
|
||||||
|
snapshotStore,
|
||||||
isMediaIdEmpty,
|
isMediaIdEmpty,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
||||||
|
|
||||||
export const CityCreatePage = observer(() => {
|
export const CityCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const { createCityData, setCreateCityData, setCreateCityWeatherCode } =
|
const { createCityData, setCreateCityData, setCreateCityWeatherCode } =
|
||||||
cityStore;
|
cityStore;
|
||||||
@@ -53,7 +61,13 @@ export const CityCreatePage = observer(() => {
|
|||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
const ruCityName = createCityData.ru.name.trim();
|
||||||
await cityStore.createCity();
|
await cityStore.createCity();
|
||||||
|
try {
|
||||||
|
await snapshotStore.createEmptySnapshot(`${ruCityName}_Пустой_Экспорт`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to create empty snapshot for city:", e);
|
||||||
|
}
|
||||||
toast.success("Город успешно создан");
|
toast.success("Город успешно создан");
|
||||||
navigate("/city");
|
navigate("/city");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -23,12 +23,19 @@ import {
|
|||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
import { LanguageSwitcher, ImageUploadCard } from "@widgets";
|
||||||
|
|
||||||
export const CityEditPage = observer(() => {
|
export const CityEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||||
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
|
||||||
|
|||||||
@@ -114,10 +114,11 @@ export const CityListPage = observer(() => {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/city/${params.row.id}/edit`)}>
|
<button title="Редактировать" onClick={() => navigate(`/city/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@@ -139,7 +140,10 @@ export const CityListPage = observer(() => {
|
|||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Города</h1>
|
<h1 className="text-2xl">Города</h1>
|
||||||
{canWriteCities && (
|
{canWriteCities && (
|
||||||
<CreateButton label="Создать город" path="/city/create" />
|
<CreateButton
|
||||||
|
label="Создать город"
|
||||||
|
path="/city/create"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -155,7 +159,9 @@ export const CityListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={filteredRows}
|
rows={filteredRows}
|
||||||
@@ -163,6 +169,11 @@ export const CityListPage = observer(() => {
|
|||||||
checkboxSelection={canWriteCities}
|
checkboxSelection={canWriteCities}
|
||||||
disableRowSelectionExcludeModel
|
disableRowSelectionExcludeModel
|
||||||
disableRowSelectionOnClick
|
disableRowSelectionOnClick
|
||||||
|
onRowDoubleClick={(params) => {
|
||||||
|
if (canWriteCities) {
|
||||||
|
navigate(`/city/${params.id}/edit`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
paginationModel={paginationModel}
|
paginationModel={paginationModel}
|
||||||
onPaginationModelChange={setPaginationModel}
|
onPaginationModelChange={setPaginationModel}
|
||||||
@@ -195,7 +206,11 @@ export const CityListPage = observer(() => {
|
|||||||
slots={{
|
slots={{
|
||||||
noRowsOverlay: () => (
|
noRowsOverlay: () => (
|
||||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||||
{isLoading ? <CircularProgress size={20} /> : "Нет городов"}
|
{isLoading ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : (
|
||||||
|
"Нет городов"
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -15,11 +15,18 @@ import {
|
|||||||
RU_COUNTRIES,
|
RU_COUNTRIES,
|
||||||
EN_COUNTRIES,
|
EN_COUNTRIES,
|
||||||
ZH_COUNTRIES,
|
ZH_COUNTRIES,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
export const CountryAddPage = observer(() => {
|
export const CountryAddPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { createCountryData, setCountryData, createCountry } = countryStore;
|
const { createCountryData, setCountryData, createCountry } = countryStore;
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ import { ArrowLeft, Save } from "lucide-react";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { countryStore, languageStore } from "@shared";
|
import { countryStore, languageStore, selectedCityStore } from "@shared";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const CountryCreatePage = observer(() => {
|
export const CountryCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const { createCountryData, setCountryData, createCountry } = countryStore;
|
const { createCountryData, setCountryData, createCountry } = countryStore;
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ import { ArrowLeft, Save } from "lucide-react";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { countryStore, languageStore, LoadingSpinner } from "@shared";
|
import { countryStore, languageStore, LoadingSpinner, selectedCityStore } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LanguageSwitcher } from "@widgets";
|
import { LanguageSwitcher } from "@widgets";
|
||||||
|
|
||||||
export const CountryEditPage = observer(() => {
|
export const CountryEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|||||||
@@ -92,7 +92,10 @@ export const CountryListPage = observer(() => {
|
|||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Страны</h1>
|
<h1 className="text-2xl">Страны</h1>
|
||||||
{canWriteCountries && (
|
{canWriteCountries && (
|
||||||
<CreateButton label="Добавить страну" path="/country/add" />
|
<CreateButton
|
||||||
|
label="Добавить страну"
|
||||||
|
path="/country/add"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -108,7 +111,9 @@ export const CountryListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -148,7 +153,11 @@ export const CountryListPage = observer(() => {
|
|||||||
slots={{
|
slots={{
|
||||||
noRowsOverlay: () => (
|
noRowsOverlay: () => (
|
||||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||||
{isLoading ? <CircularProgress size={20} /> : "Нет стран"}
|
{isLoading ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : (
|
||||||
|
"Нет стран"
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
cityStore,
|
cityStore,
|
||||||
createSightStore,
|
createSightStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import {
|
import {
|
||||||
CreateInformationTab,
|
CreateInformationTab,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
} from "@widgets";
|
} from "@widgets";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { runInAction } from "mobx";
|
||||||
|
|
||||||
function a11yProps(index: number) {
|
function a11yProps(index: number) {
|
||||||
return {
|
return {
|
||||||
@@ -30,6 +32,11 @@ export const CreateSightPage = observer(() => {
|
|||||||
const { getArticles } = articlesStore;
|
const { getArticles } = articlesStore;
|
||||||
const needLeave = createSightStore.needLeaveAgree;
|
const needLeave = createSightStore.needLeaveAgree;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleChange = (_: React.SyntheticEvent, newValue: number) => {
|
const handleChange = (_: React.SyntheticEvent, newValue: number) => {
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
};
|
};
|
||||||
@@ -56,6 +63,14 @@ export const CreateSightPage = observer(() => {
|
|||||||
await authStore.fetchMeCities().catch(() => undefined);
|
await authStore.fetchMeCities().catch(() => undefined);
|
||||||
}
|
}
|
||||||
await getArticles(languageStore.language);
|
await getArticles(languageStore.language);
|
||||||
|
|
||||||
|
const { selectedCityId, selectedCity } = selectedCityStore;
|
||||||
|
if (selectedCityId && selectedCity && !createSightStore.sight.city_id) {
|
||||||
|
runInAction(() => {
|
||||||
|
createSightStore.sight.city_id = selectedCityId;
|
||||||
|
createSightStore.sight.city = selectedCity.name;
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
cityStore,
|
cityStore,
|
||||||
editSightStore,
|
editSightStore,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useBlocker, useParams } from "react-router-dom";
|
import { useBlocker, useParams } from "react-router-dom";
|
||||||
|
|
||||||
@@ -25,6 +26,11 @@ export const EditSightPage = observer(() => {
|
|||||||
const { sight, getSightInfo, needLeaveAgree, getRightArticles } = editSightStore;
|
const { sight, getSightInfo, needLeaveAgree, getRightArticles } = editSightStore;
|
||||||
const { getArticles } = articlesStore;
|
const { getArticles } = articlesStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { getCities } = cityStore;
|
const { getCities } = cityStore;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { mediaStore, MEDIA_TYPE_LABELS, selectedCityStore } from "@shared";
|
import { mediaStore, MEDIA_TYPE_LABELS, selectedCityStore } from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
@@ -20,6 +20,11 @@ export const MediaCreatePage = observer(() => {
|
|||||||
const [type, setType] = useState("");
|
const [type, setType] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
MEDIA_TYPE_LABELS,
|
MEDIA_TYPE_LABELS,
|
||||||
languageStore,
|
languageStore,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { MediaViewer } from "@widgets";
|
import { MediaViewer } from "@widgets";
|
||||||
|
|
||||||
@@ -42,6 +43,11 @@ export const MediaEditPage = observer(() => {
|
|||||||
const [mediaType, setMediaType] = useState(media?.media_type ?? 1);
|
const [mediaType, setMediaType] = useState(media?.media_type ?? 1);
|
||||||
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>([]);
|
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
mediaStore.getOneMedia(id);
|
mediaStore.getOneMedia(id);
|
||||||
|
|||||||
@@ -78,11 +78,12 @@ export const MediaListPage = observer(() => {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/media/${params.row.id}`)}>
|
<button title="Просмотр" onClick={() => navigate(`/media/${params.row.id}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
{canWriteMedia && (
|
{canWriteMedia && (
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(params.row.id);
|
||||||
@@ -112,7 +113,9 @@ export const MediaListPage = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
{canWriteMedia && ids.length > 0 && (
|
{canWriteMedia && ids.length > 0 && (
|
||||||
<div className="flex justify-end mb-5 duration-300">
|
<div className="flex justify-end mb-5 duration-300">
|
||||||
|
|||||||
@@ -39,11 +39,18 @@ import type { Route } from "@shared";
|
|||||||
|
|
||||||
export const RouteCreatePage = observer(() => {
|
export const RouteCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [carrier, setCarrier] = useState<string>("");
|
const [carrier, setCarrier] = useState<string>("");
|
||||||
const [routeNumber, setRouteNumber] = useState("");
|
const [routeNumber, setRouteNumber] = useState("");
|
||||||
const [routeCoords, setRouteCoords] = useState("");
|
const [routeCoords, setRouteCoords] = useState("");
|
||||||
const [govRouteNumber, setGovRouteNumber] = useState("");
|
const [govRouteNumber, setGovRouteNumber] = useState("");
|
||||||
const [governorAppeal, setGovernorAppeal] = useState<string>("");
|
const [governorAppeal, setGovernorAppeal] = useState<string>("");
|
||||||
|
const [buttonText, setButtonText] = useState("");
|
||||||
const [direction, setDirection] = useState("backward");
|
const [direction, setDirection] = useState("backward");
|
||||||
const [scaleMin, setScaleMin] = useState("10");
|
const [scaleMin, setScaleMin] = useState("10");
|
||||||
const [scaleMax, setScaleMax] = useState("100");
|
const [scaleMax, setScaleMax] = useState("100");
|
||||||
@@ -51,7 +58,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const [turn, setTurn] = useState("");
|
const [turn, setTurn] = useState("");
|
||||||
const [centerLat, setCenterLat] = useState("");
|
const [centerLat, setCenterLat] = useState("");
|
||||||
const [centerLng, setCenterLng] = useState("");
|
const [centerLng, setCenterLng] = useState("");
|
||||||
const [videoTimer, setVideoTimer] = useState(60);
|
const [videoTimer, setVideoTimer] = useState(420);
|
||||||
const [videoPreview, setVideoPreview] = useState<string>("");
|
const [videoPreview, setVideoPreview] = useState<string>("");
|
||||||
const [icon, setIcon] = useState<string>("");
|
const [icon, setIcon] = useState<string>("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -286,6 +293,10 @@ export const RouteCreatePage = observer(() => {
|
|||||||
newRoute.governor_appeal = governor_appeal;
|
newRoute.governor_appeal = governor_appeal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (buttonText.trim()) {
|
||||||
|
newRoute.button_text = buttonText.trim();
|
||||||
|
}
|
||||||
|
|
||||||
const newId = await routeStore.createRoute(newRoute);
|
const newId = await routeStore.createRoute(newRoute);
|
||||||
toast.success("Маршрут успешно создан");
|
toast.success("Маршрут успешно создан");
|
||||||
navigate(`/route/${newId}/edit`);
|
navigate(`/route/${newId}/edit`);
|
||||||
@@ -401,6 +412,18 @@ export const RouteCreatePage = observer(() => {
|
|||||||
onChange={(e) => setGovRouteNumber(e.target.value)}
|
onChange={(e) => setGovRouteNumber(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
|
Текст кнопки обращения
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
value={buttonText}
|
||||||
|
onChange={(e) => setButtonText(e.target.value)}
|
||||||
|
placeholder="Обращение губернатора"
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
helperText="Если пусто, будет использован текст по умолчанию"
|
||||||
|
/>
|
||||||
|
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
Обращение к пассажирам
|
Обращение к пассажирам
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -555,7 +578,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Таймер видео (сек)"
|
label="Таймер видео заставки (сек)"
|
||||||
type="number"
|
type="number"
|
||||||
value={videoTimer}
|
value={videoTimer}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -36,11 +36,18 @@ import {
|
|||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { LinkedItems } from "../LinekedStations";
|
import { LinkedItems } from "../LinekedStations";
|
||||||
|
|
||||||
export const RouteEditPage = observer(() => {
|
export const RouteEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { editRouteData, copyRouteAction } = routeStore;
|
const { editRouteData, copyRouteAction } = routeStore;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -548,9 +555,9 @@ export const RouteEditPage = observer(() => {
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Таймер видео (сек)"
|
label="Таймер видео заставки (сек)"
|
||||||
type="number"
|
type="number"
|
||||||
value={editRouteData.video_timer ?? 60}
|
value={editRouteData.video_timer ?? 420}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = Math.max(1, Math.round(Number(e.target.value)));
|
const val = Math.max(1, Math.round(Number(e.target.value)));
|
||||||
if (Number.isFinite(val)) {
|
if (Number.isFinite(val)) {
|
||||||
@@ -559,6 +566,22 @@ export const RouteEditPage = observer(() => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
|
Текст кнопки обращения
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
value={editRouteData.button_text || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
button_text: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Обращение губернатора"
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
helperText="Если пусто, будет использован текст по умолчанию"
|
||||||
|
/>
|
||||||
|
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
Обращение к пассажирам
|
Обращение к пассажирам
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const RouteListPage = observer(() => {
|
|||||||
loadCounts(routeIds);
|
loadCounts(routeIds);
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [language]);
|
}, [language, selectedCityStore.cityVersion]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
@@ -139,7 +139,7 @@ export const RouteListPage = observer(() => {
|
|||||||
headerAlign: "center" as const,
|
headerAlign: "center" as const,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
renderHeader: (params: any) => (
|
renderHeader: (params: any) => (
|
||||||
<Tooltip title="Количество привязанных достопримечательностей">
|
<Tooltip title="Отображает количество привязанных достопримечательностей">
|
||||||
<span>{params.colDef.headerName}</span>
|
<span>{params.colDef.headerName}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@@ -157,7 +157,7 @@ export const RouteListPage = observer(() => {
|
|||||||
headerAlign: "center" as const,
|
headerAlign: "center" as const,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
renderHeader: (params: any) => (
|
renderHeader: (params: any) => (
|
||||||
<Tooltip title="Количество привязанных остановок">
|
<Tooltip title="Отображает количество привязанных остановок">
|
||||||
<span>{params.colDef.headerName}</span>
|
<span>{params.colDef.headerName}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@@ -178,22 +178,23 @@ export const RouteListPage = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
{canWriteRoutes && (
|
{canWriteRoutes && (
|
||||||
<button onClick={() => navigate(`/route/${params.row.id}/edit`)}>
|
<button title="Редактировать" onClick={() => navigate(`/route/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{canShowRoutePreview && (
|
{canShowRoutePreview && (
|
||||||
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
<button title="Предпросмотр на карте" onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
||||||
<Map size={20} className="text-purple-500" />
|
<Map size={20} className="text-purple-500" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{canShowRoutePreview && (
|
{canShowRoutePreview && (
|
||||||
<button onClick={() => window.open(`/demo/${params.row.id}`, "_blank")}>
|
<button title="Демо" onClick={() => window.open(`/demo/${params.row.id}`, "_blank")}>
|
||||||
<Monitor size={20} className="text-green-500" />
|
<Monitor size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{canWriteRoutes && (
|
{canWriteRoutes && (
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(params.row.id);
|
||||||
@@ -210,6 +211,9 @@ export const RouteListPage = observer(() => {
|
|||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
const { selectedCityId } = selectedCityStore;
|
const { selectedCityId } = selectedCityStore;
|
||||||
|
if (!selectedCityId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const query = searchQuery.trim().toLowerCase();
|
const query = searchQuery.trim().toLowerCase();
|
||||||
let filtered = routes.data;
|
let filtered = routes.data;
|
||||||
if (selectedCityId) {
|
if (selectedCityId) {
|
||||||
@@ -247,7 +251,10 @@ export const RouteListPage = observer(() => {
|
|||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Маршруты</h1>
|
<h1 className="text-2xl">Маршруты</h1>
|
||||||
{canWriteRoutes && (
|
{canWriteRoutes && (
|
||||||
<CreateButton label="Создать маршрут" path="/route/create" />
|
<CreateButton
|
||||||
|
label="Создать маршрут"
|
||||||
|
path="/route/create"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -263,7 +270,9 @@ export const RouteListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -304,7 +313,13 @@ export const RouteListPage = observer(() => {
|
|||||||
slots={{
|
slots={{
|
||||||
noRowsOverlay: () => (
|
noRowsOverlay: () => (
|
||||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||||
{isLoading ? <CircularProgress size={20} /> : "Нет маршрутов"}
|
{isLoading ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : !selectedCityStore.selectedCityId ? (
|
||||||
|
"Выберите город"
|
||||||
|
) : (
|
||||||
|
"Нет маршрутов"
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { useNavigate, useNavigationType } from "react-router";
|
import { useNavigate, useNavigationType } from "react-router";
|
||||||
import { MediaViewer } from "@widgets";
|
import { MediaViewer } from "@widgets";
|
||||||
import { useMapData } from "./MapDataContext";
|
import { useMapData } from "./MapDataContext";
|
||||||
@@ -15,22 +15,24 @@ type LeftSidebarProps = {
|
|||||||
|
|
||||||
export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
|
export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const navigationType = useNavigationType(); // PUSH, POP, REPLACE
|
const navigationType = useNavigationType();
|
||||||
const { routeData } = useMapData();
|
const { routeData } = useMapData();
|
||||||
const [carrierThumbnail, setCarrierThumbnail] = useState<string | null>(null);
|
|
||||||
const [carrierLogo, setCarrierLogo] = useState<string | null>(null);
|
const [carrierLogo, setCarrierLogo] = useState<string | null>(null);
|
||||||
|
const [carrierSlogan, setCarrierSlogan] = useState<string | null>(null);
|
||||||
|
const [carrierShortName, setCarrierShortName] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchCarrierThumbnail() {
|
async function fetchCarrierData() {
|
||||||
if (routeData?.carrier_id) {
|
if (routeData?.carrier_id) {
|
||||||
const { city_id, logo } = (
|
const carrier = (
|
||||||
await authInstance.get(`/carrier/${routeData.carrier_id}`)
|
await authInstance.get(`/carrier/${routeData.carrier_id}`)
|
||||||
).data;
|
).data;
|
||||||
const { arms } = (await authInstance.get(`/city/${city_id}`)).data;
|
setCarrierLogo(carrier.logo);
|
||||||
setCarrierThumbnail(arms);
|
setCarrierSlogan(carrier.slogan ?? null);
|
||||||
setCarrierLogo(logo);
|
setCarrierShortName(carrier.short_name ?? null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchCarrierThumbnail();
|
fetchCarrierData();
|
||||||
}, [routeData?.carrier_id]);
|
}, [routeData?.carrier_id]);
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
@@ -42,131 +44,190 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<div
|
||||||
sx={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
width: 288,
|
||||||
height: "100%",
|
height: "100%",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
transition: "padding 0.3s ease",
|
|
||||||
p: open ? 2 : 0,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "stretch",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
{/* Кнопка назад — вне основного меню */}
|
||||||
direction="column"
|
<div
|
||||||
height="100%"
|
style={{
|
||||||
width="100%"
|
padding: "12px 12px 0",
|
||||||
spacing={4}
|
|
||||||
alignItems="stretch"
|
|
||||||
justifyContent="space-between"
|
|
||||||
sx={{
|
|
||||||
opacity: open ? 1 : 0,
|
opacity: open ? 1 : 0,
|
||||||
transition: "opacity 0.25s ease",
|
|
||||||
pointerEvents: open ? "auto" : "none",
|
pointerEvents: open ? "auto" : "none",
|
||||||
display: open ? "flex" : "none",
|
transition: "opacity 0.25s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleBack}
|
onClick={handleBack}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#222",
|
backgroundColor: "#222",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
px: 2,
|
px: 2,
|
||||||
py: 1,
|
py: 1,
|
||||||
marginBottom: 10,
|
"&:hover": { backgroundColor: "#2d2d2d" },
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: "#2d2d2d",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
startIcon={<ArrowBackIcon />}
|
startIcon={<ArrowBackIcon />}
|
||||||
>
|
>
|
||||||
Назад
|
Назад
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Stack
|
{/* Основное меню — повторяет .side-menu */}
|
||||||
direction="column"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
spacing={3}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxWidth: 150,
|
boxSizing: "border-box",
|
||||||
|
paddingTop: 46,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 10,
|
height: "calc(100% - 56px)",
|
||||||
|
position: "relative",
|
||||||
|
opacity: open ? 1 : 0,
|
||||||
|
transition: "opacity 0.25s ease",
|
||||||
|
pointerEvents: open ? "auto" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{carrierThumbnail && !isMediaIdEmpty(carrierThumbnail) && (
|
{/* Герб — .side-menu-crest */}
|
||||||
<MediaViewer
|
<div
|
||||||
media={{
|
style={{
|
||||||
id: carrierThumbnail,
|
width: 170,
|
||||||
media_type: 1, // Тип "Фото" для логотипа
|
height: 170,
|
||||||
filename: "route_thumbnail",
|
alignSelf: "flex-start",
|
||||||
|
marginLeft: 20,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.15)",
|
||||||
|
borderRadius: 8,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "rgba(255,255,255,0.5)",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
fullWidth
|
>
|
||||||
fullHeight
|
Герб
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
|
{/* Слоган — .side-menu-label */}
|
||||||
|
{carrierSlogan && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 10,
|
||||||
|
textAlign: "left",
|
||||||
|
fontSize: 15,
|
||||||
|
padding: "0 20px",
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: "150%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{carrierSlogan}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Typography sx={{ color: "#fff" }} textAlign="center">
|
|
||||||
При поддержке Правительства
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center justify-center gap-2 mt-10">
|
{/* Кнопки — .side-menu-buttons */}
|
||||||
<button className="bg-[#fcd500] text-black px-4 py-2 rounded-md w-full font-medium my-10">
|
<div
|
||||||
Обращение губернатора
|
style={{
|
||||||
</button>
|
width: 220,
|
||||||
<button className="bg-white text-black px-4 py-2 rounded-md w-full font-medium mx-5">
|
marginTop: routeData?.governor_appeal || 0 > 0 ? 40 : 260,
|
||||||
Достопримечательности
|
}}
|
||||||
</button>
|
|
||||||
<button className="bg-white text-black px-4 py-2 rounded-md w-full font-medium mx-5">
|
|
||||||
Остановки
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Stack
|
|
||||||
direction="column"
|
|
||||||
alignItems="center"
|
|
||||||
maxHeight={150}
|
|
||||||
justifyContent="center"
|
|
||||||
flexGrow={1}
|
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
color: "#000",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "8px 16px",
|
||||||
|
marginBottom: 16,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Достопримечательности
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
color: "#000",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "8px 16px",
|
||||||
|
marginBottom: 16,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Остановки
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Нижняя секция — .side-menu-bottom-section */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* .side-menu-carrier-block */}
|
||||||
|
<div style={{ padding: "0 20px" }}>
|
||||||
{carrierLogo && !isMediaIdEmpty(carrierLogo) && (
|
{carrierLogo && !isMediaIdEmpty(carrierLogo) && (
|
||||||
|
<div style={{ width: 170 }}>
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
id: carrierLogo,
|
id: carrierLogo,
|
||||||
media_type: 1, // Тип "Фото" для логотипа
|
media_type: 1,
|
||||||
filename: "route_thumbnail_logo",
|
filename: "carrier_logo",
|
||||||
}}
|
}}
|
||||||
fullHeight
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
{carrierShortName && (
|
||||||
|
<div
|
||||||
<Typography
|
style={{
|
||||||
variant="h6"
|
marginTop: 4,
|
||||||
textAlign="center"
|
textAlign: "left",
|
||||||
sx={{ color: "#fff", marginTop: "auto" }}
|
fontSize: 16,
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: "150%",
|
||||||
|
color: "#fff",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
#ВсемПоПути
|
{carrierShortName}
|
||||||
</Typography>
|
</div>
|
||||||
</Stack>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-[20px] -right-[520px] z-10">
|
{/* .side-menu-bottom-photo */}
|
||||||
|
<img
|
||||||
|
src="/side-menu-photo.png"
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
width: "288px",
|
||||||
|
marginTop: 32,
|
||||||
|
display: "block",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute bottom-[20px] z-10"
|
||||||
|
style={{
|
||||||
|
right: open ? -520 : -312,
|
||||||
|
transition: "right 0.3s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<LanguageSelector onBack={onToggle} isSidebarOpen={open} />
|
<LanguageSelector onBack={onToggle} isSidebarOpen={open} />
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -448,6 +448,22 @@ const StationLabel = observer(
|
|||||||
anchor={dynamicAnchor}
|
anchor={dynamicAnchor}
|
||||||
zIndex={isHovered || isControlHovered ? 1000 : 0}
|
zIndex={isHovered || isControlHovered ? 1000 : 0}
|
||||||
>
|
>
|
||||||
|
{ruLabelWidth > 0 && (
|
||||||
|
<pixiGraphics
|
||||||
|
draw={(g: Graphics) => {
|
||||||
|
g.clear();
|
||||||
|
const hasSecondLabel = !!(station.name && language !== "ru" && ruLabel);
|
||||||
|
const pad = 10 / scale;
|
||||||
|
const w = ruLabelWidth + pad * 2;
|
||||||
|
const top = -compensatedRuFontSize / 2 - pad;
|
||||||
|
const bottom = hasSecondLabel
|
||||||
|
? compensatedRuFontSize * 1.1 + compensatedNameFontSize / 2 + pad
|
||||||
|
: compensatedRuFontSize / 2 + pad;
|
||||||
|
g.rect(-w / 2, top, w, bottom - top);
|
||||||
|
g.fill({ color: 0x000000, alpha: 0.001 });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{ruLabel && (
|
{ruLabel && (
|
||||||
<pixiText
|
<pixiText
|
||||||
ref={ruLabelRef}
|
ref={ruLabelRef}
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ export const RoutePreview = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
width: isLeftSidebarOpen ? 300 : 0,
|
zIndex: 20,
|
||||||
|
width: isLeftSidebarOpen ? 288 : 0,
|
||||||
transition: "width 0.3s ease",
|
transition: "width 0.3s ease",
|
||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
@@ -145,14 +146,14 @@ export const RouteMap = observer(() => {
|
|||||||
) {
|
) {
|
||||||
const coordinates = coordinatesToLocal(
|
const coordinates = coordinatesToLocal(
|
||||||
originalRouteData?.center_latitude,
|
originalRouteData?.center_latitude,
|
||||||
originalRouteData?.center_longitude
|
originalRouteData?.center_longitude,
|
||||||
);
|
);
|
||||||
|
|
||||||
setTransform(
|
setTransform(
|
||||||
coordinates.x,
|
coordinates.x,
|
||||||
coordinates.y,
|
coordinates.y,
|
||||||
originalRouteData?.rotate,
|
originalRouteData?.rotate,
|
||||||
originalRouteData?.scale_min
|
originalRouteData?.scale_min,
|
||||||
);
|
);
|
||||||
setIsSetup(true);
|
setIsSetup(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface RouteData {
|
|||||||
icon_size?: number;
|
icon_size?: number;
|
||||||
font_size: number;
|
font_size: number;
|
||||||
governor_appeal: number;
|
governor_appeal: number;
|
||||||
|
button_text?: string;
|
||||||
id: number;
|
id: number;
|
||||||
path: [number, number][];
|
path: [number, number][];
|
||||||
rotate: number;
|
rotate: number;
|
||||||
|
|||||||
@@ -26,10 +26,12 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset,
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.3) inset,
|
||||||
/* Внутренняя рамка */ 4px 4px 12px 0 rgba(255, 255, 255, 0.12) inset; /* Ваш существующий внутренний shadow */
|
/* Внутренняя рамка */ 4px 4px 12px 0 rgba(255, 255, 255, 0.12) inset; /* Ваш существующий внутренний shadow */
|
||||||
padding: 1px; /* Чтобы контент не прилипал к рамке */
|
padding: 1px; /* Чтобы контент не прилипал к рамке */
|
||||||
background: linear-gradient(
|
background:
|
||||||
|
linear-gradient(
|
||||||
to bottom right,
|
to bottom right,
|
||||||
rgba(255, 255, 255, 0.2) 0%,
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
rgba(255, 255, 255, 0) 100%
|
rgba(255, 255, 255, 0) 100%
|
||||||
@@ -50,7 +52,8 @@
|
|||||||
height: 96px;
|
height: 96px;
|
||||||
background-color: #fcd500;
|
background-color: #fcd500;
|
||||||
color: black;
|
color: black;
|
||||||
border-radius: 10px;
|
border-top-left-radius: 10px;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -2327,9 +2327,6 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
const stationScreenY =
|
const stationScreenY =
|
||||||
rotatedY * camera.scale + camera.translation.y;
|
rotatedY * camera.scale + camera.translation.y;
|
||||||
|
|
||||||
const labelX = stationScreenX + offsetX;
|
|
||||||
const labelY = stationScreenY + offsetY;
|
|
||||||
|
|
||||||
const backendAlign = station.align;
|
const backendAlign = station.align;
|
||||||
|
|
||||||
const anchor = getAnchorFromOffset(backendAlign ?? 2);
|
const anchor = getAnchorFromOffset(backendAlign ?? 2);
|
||||||
@@ -2339,8 +2336,6 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
|
|
||||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
|
|
||||||
const cssX = labelX / dpr;
|
|
||||||
const cssY = labelY / dpr;
|
|
||||||
const rotationCss = `${rotationAngle}rad`;
|
const rotationCss = `${rotationAngle}rad`;
|
||||||
const counterRotationCss = `${-rotationAngle}rad`;
|
const counterRotationCss = `${-rotationAngle}rad`;
|
||||||
|
|
||||||
@@ -2359,6 +2354,13 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
const scaleFactor = 1 + (zoomClampedScale - 1) * 0.4;
|
const scaleFactor = 1 + (zoomClampedScale - 1) * 0.4;
|
||||||
|
|
||||||
const primaryFontSize = 16 * fontScale * scaleFactor;
|
const primaryFontSize = 16 * fontScale * scaleFactor;
|
||||||
|
|
||||||
|
const mainLabelHeight = primaryFontSize * 1.2;
|
||||||
|
const labelX = stationScreenX + offsetX;
|
||||||
|
const labelY = stationScreenY + offsetY + mainLabelHeight / 2;
|
||||||
|
|
||||||
|
const cssX = labelX / dpr;
|
||||||
|
const cssY = labelY / dpr;
|
||||||
const secondaryFontSize = 13 * fontScale * scaleFactor;
|
const secondaryFontSize = 13 * fontScale * scaleFactor;
|
||||||
|
|
||||||
const secondaryMarginTop = 5 * fontScale * scaleFactor;
|
const secondaryMarginTop = 5 * fontScale * scaleFactor;
|
||||||
@@ -2404,13 +2406,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
hoveredStationIconId === station.id ||
|
hoveredStationIconId === station.id ||
|
||||||
resizingStationIconId === station.id;
|
resizingStationIconId === station.id;
|
||||||
|
|
||||||
const secondaryLineHeight = 1.2;
|
const secondaryLineHeight = 1.2 * scaleFactor;
|
||||||
const secondaryHeight = showSecondary
|
|
||||||
? secondaryFontSize * secondaryLineHeight
|
|
||||||
: 0;
|
|
||||||
const menuPaddingTop = showSecondary
|
|
||||||
? Math.max(0, secondaryHeight - secondaryMarginTop) + 3
|
|
||||||
: 3;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={station.id}>
|
<div key={station.id}>
|
||||||
@@ -2440,7 +2436,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
color: "#fff",
|
color: "#fff",
|
||||||
fontFamily: "Roboto, sans-serif",
|
fontFamily: "Roboto, sans-serif",
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
pointerEvents: "auto",
|
pointerEvents: "none",
|
||||||
cursor: "grab",
|
cursor: "grab",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
@@ -2448,14 +2444,16 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: "auto",
|
display: "inline-block",
|
||||||
|
pointerEvents: "none",
|
||||||
transformOrigin: "left center",
|
transformOrigin: "left center",
|
||||||
transform: `rotate(${rotationCss})`,
|
transform: `rotate(${rotationCss})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: "auto",
|
display: "inline-block",
|
||||||
|
pointerEvents: "none",
|
||||||
transformOrigin: "left center",
|
transformOrigin: "left center",
|
||||||
transform: `rotate(${counterRotationCss})`,
|
transform: `rotate(${counterRotationCss})`,
|
||||||
}}
|
}}
|
||||||
@@ -2549,6 +2547,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
position: "relative",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
fontSize: primaryFontSize,
|
fontSize: primaryFontSize,
|
||||||
textShadow: "0 0 4px rgba(0,0,0,0.6)",
|
textShadow: "0 0 4px rgba(0,0,0,0.6)",
|
||||||
@@ -2557,7 +2556,6 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{station.name}
|
{station.name}
|
||||||
</div>
|
|
||||||
{showSecondary ? (
|
{showSecondary ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -2580,6 +2578,7 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{hoveredStationId === station.id && (
|
{hoveredStationId === station.id && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -2587,9 +2586,9 @@ export const WebGLRouteMapPrototype = observer(() => {
|
|||||||
top: "100%",
|
top: "100%",
|
||||||
left: "50%",
|
left: "50%",
|
||||||
transform: "translateX(-50%)",
|
transform: "translateX(-50%)",
|
||||||
paddingTop: menuPaddingTop,
|
paddingTop: "8px",
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
zIndex: 10,
|
zIndex: 1000000,
|
||||||
cursor: "default",
|
cursor: "default",
|
||||||
}}
|
}}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const SightListPage = observer(() => {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
fetchSights();
|
fetchSights();
|
||||||
}, [language]);
|
}, [language, selectedCityStore.cityVersion]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
@@ -105,10 +105,11 @@ export const SightListPage = observer(() => {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/sight/${params.row.id}/edit`)}>
|
<button title="Редактировать" onClick={() => navigate(`/sight/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(params.row.id);
|
||||||
@@ -121,8 +122,11 @@ export const SightListPage = observer(() => {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredSights = useMemo(() => {
|
|
||||||
const { selectedCityId } = selectedCityStore;
|
const { selectedCityId } = selectedCityStore;
|
||||||
|
const filteredSights = useMemo(() => {
|
||||||
|
if (!selectedCityId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const allowedCityIds = canReadCities
|
const allowedCityIds = canReadCities
|
||||||
? null
|
? null
|
||||||
: authStore.meCities["ru"].map((c) => c.city_id);
|
: authStore.meCities["ru"].map((c) => c.city_id);
|
||||||
@@ -131,12 +135,12 @@ export const SightListPage = observer(() => {
|
|||||||
if (allowedCityIds && !allowedCityIds.includes(sight.city_id)) {
|
if (allowedCityIds && !allowedCityIds.includes(sight.city_id)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (selectedCityId && sight.city_id !== selectedCityId) {
|
if (sight.city_id !== selectedCityId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [sights, selectedCityStore.selectedCityId, canReadCities, authStore.meCities]);
|
}, [sights, selectedCityId, canReadCities, authStore.meCities]);
|
||||||
|
|
||||||
const query = searchQuery.trim().toLowerCase();
|
const query = searchQuery.trim().toLowerCase();
|
||||||
const rows = filteredSights
|
const rows = filteredSights
|
||||||
@@ -177,7 +181,9 @@ export const SightListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -216,6 +222,8 @@ export const SightListPage = observer(() => {
|
|||||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<CircularProgress size={20} />
|
<CircularProgress size={20} />
|
||||||
|
) : !selectedCityId ? (
|
||||||
|
"Выберите город"
|
||||||
) : (
|
) : (
|
||||||
"Нет достопримечательностей"
|
"Нет достопримечательностей"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,46 +1,58 @@
|
|||||||
import { Button, TextField } from "@mui/material";
|
import {
|
||||||
import { snapshotStore } from "@shared";
|
Button,
|
||||||
|
TextField,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { snapshotStore, authStore, routeStore, selectedCityStore, cityStore, carrierStore } from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
|
|
||||||
|
function escapeRegex(s: string) {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExportNameRegex(cityNames: string[]): RegExp {
|
||||||
|
if (!cityNames.length) return /.+/;
|
||||||
|
const pattern = cityNames.map(escapeRegex).join("|");
|
||||||
|
return new RegExp(`^(${pattern})_.+$`);
|
||||||
|
}
|
||||||
|
|
||||||
export const SnapshotCreatePage = observer(() => {
|
export const SnapshotCreatePage = observer(() => {
|
||||||
const { createSnapshot, getSnapshotStatus, getStorageInfo, snapshotStatus } = snapshotStore;
|
const { createSnapshot, getSnapshotStatus, getStorageInfo, snapshotStatus } = snapshotStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
|
const [nameError, setNameError] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [duplicateWarningOpen, setDuplicateWarningOpen] = useState(false);
|
||||||
|
const [duplicateRouteNumbers, setDuplicateRouteNumbers] = useState<string[]>([]);
|
||||||
|
|
||||||
return (
|
const exportNameRegex = useMemo(() => {
|
||||||
<div className="w-full h-[400px] flex justify-center items-center">
|
const names = cityStore.cities["ru"].data.map((c) => c.name.trim());
|
||||||
<div className="w-full h-full p-3 flex flex-col gap-10">
|
return buildExportNameRegex(names);
|
||||||
<div className="flex justify-between items-center">
|
}, [cityStore.cities["ru"].data.length]);
|
||||||
<button
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
>
|
|
||||||
<ArrowLeft size={20} />
|
|
||||||
Назад
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold">Создание экспорта медиа</h1>
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
|
||||||
<TextField
|
|
||||||
className="w-full"
|
|
||||||
label="Название"
|
|
||||||
required
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
useEffect(() => {
|
||||||
variant="contained"
|
if (!cityStore.cities["ru"].loaded) {
|
||||||
color="primary"
|
cityStore.getCities("ru");
|
||||||
className="w-min flex gap-2 items-center"
|
}
|
||||||
startIcon={<Save size={20} />}
|
}, []);
|
||||||
onClick={async () => {
|
|
||||||
|
const canReadRoutes = authStore.canRead("routes");
|
||||||
|
|
||||||
|
const startExport = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const id = await createSnapshot(name);
|
const id = await createSnapshot(name);
|
||||||
@@ -68,8 +80,118 @@ export const SnapshotCreatePage = observer(() => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!canReadRoutes) {
|
||||||
|
await startExport();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
routeStore.routes.loaded = false;
|
||||||
|
});
|
||||||
|
await routeStore.getRoutes();
|
||||||
|
await carrierStore.getCarriers("ru");
|
||||||
|
|
||||||
|
const routes = routeStore.routes.data;
|
||||||
|
const carriers = carrierStore.carriers.ru.data;
|
||||||
|
const carrierCityMap = new Map<number, number>();
|
||||||
|
for (const c of carriers) {
|
||||||
|
carrierCityMap.set(c.id, c.city_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateMessages: string[] = [];
|
||||||
|
|
||||||
|
const directionKey = new Map<string, number>();
|
||||||
|
for (const route of routes) {
|
||||||
|
const num = (route.route_sys_number ?? "").trim();
|
||||||
|
if (!num) continue;
|
||||||
|
const cityId = carrierCityMap.get(route.carrier_id) ?? 0;
|
||||||
|
const key = `${num}|${route.route_direction}|${cityId}`;
|
||||||
|
directionKey.set(key, (directionKey.get(key) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
for (const [key, count] of directionKey) {
|
||||||
|
if (count > 1) {
|
||||||
|
const [num, dir] = key.split("|");
|
||||||
|
const dirLabel = dir === "true" ? "прямой" : "обратный";
|
||||||
|
duplicateMessages.push(
|
||||||
|
`Дублируется маршрут №${num} (${dirLabel})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cityPerNumber = new Map<string, Set<number>>();
|
||||||
|
for (const route of routes) {
|
||||||
|
const num = (route.route_sys_number ?? "").trim();
|
||||||
|
if (!num) continue;
|
||||||
|
const cityId = carrierCityMap.get(route.carrier_id) ?? 0;
|
||||||
|
if (!cityPerNumber.has(num)) {
|
||||||
|
cityPerNumber.set(num, new Set());
|
||||||
|
}
|
||||||
|
cityPerNumber.get(num)!.add(cityId);
|
||||||
|
}
|
||||||
|
for (const [num, cities] of cityPerNumber) {
|
||||||
|
if (cities.size > 1) {
|
||||||
|
duplicateMessages.push(
|
||||||
|
`Маршрут №${num} присутствует в нескольких городах`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateMessages.length > 0) {
|
||||||
|
setDuplicateRouteNumbers(duplicateMessages);
|
||||||
|
setDuplicateWarningOpen(true);
|
||||||
|
} else {
|
||||||
|
await startExport();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await startExport();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-[400px] flex justify-center items-center">
|
||||||
|
<div className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold">Создание экспорта медиа</h1>
|
||||||
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Название"
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
error={!!nameError}
|
||||||
|
helperText={nameError ?? " "}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setName(val);
|
||||||
|
const trimmed = val.trim();
|
||||||
|
const hasFullFormat = trimmed.includes("_") && trimmed.split("_").slice(1).join("_").length > 0;
|
||||||
|
if (hasFullFormat && !exportNameRegex.test(trimmed)) {
|
||||||
|
setNameError("Название должно начинаться с названия существующего города");
|
||||||
|
} else {
|
||||||
|
setNameError(null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isLoading || !name.trim()}
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className="w-min flex gap-2 items-center"
|
||||||
|
startIcon={<Save size={20} />}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isLoading || !exportNameRegex.test(name.trim())}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -87,6 +209,45 @@ export const SnapshotCreatePage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={duplicateWarningOpen}
|
||||||
|
onClose={() => !isLoading && setDuplicateWarningOpen(false)}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>Найдены повторяющиеся маршруты</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<p className="mb-3">
|
||||||
|
Обнаружены маршруты с одинаковыми номерами трассы. Это может привести к
|
||||||
|
некорректным данным в экспорте.
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5">
|
||||||
|
{duplicateRouteNumbers.map((msg, i) => (
|
||||||
|
<li key={i}>{msg}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
onClick={() => setDuplicateWarningOpen(false)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={{ backgroundColor: "#795548", "&:hover": { backgroundColor: "#5D4037" } }}
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
setDuplicateWarningOpen(false);
|
||||||
|
await startExport();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Продолжить экспорт
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, languageStore, snapshotStore, SearchInput } from "@shared";
|
import { authStore, languageStore, snapshotStore, cityStore, vehicleStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { DatabaseBackup, Trash2 } from "lucide-react";
|
import { DatabaseBackup, Trash2 } from "lucide-react";
|
||||||
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
|
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
|
||||||
import { Alert, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from "@mui/material";
|
import { Alert, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, TextField, Typography } from "@mui/material";
|
||||||
|
|
||||||
const LOW_STORAGE_THRESHOLD_GB = 10;
|
const LOW_STORAGE_THRESHOLD_GB = 10;
|
||||||
|
|
||||||
|
function escapeRegex(s: string) {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExportNameRegex(cityNames: string[]): RegExp {
|
||||||
|
if (!cityNames.length) return /.+/;
|
||||||
|
const pattern = cityNames.map(escapeRegex).join("|");
|
||||||
|
return new RegExp(`^(${pattern})_.+$`);
|
||||||
|
}
|
||||||
|
|
||||||
const SEGMENT_COLORS = [
|
const SEGMENT_COLORS = [
|
||||||
"#FF3B30",
|
"#FF3B30",
|
||||||
"#FF9500",
|
"#FF9500",
|
||||||
@@ -33,11 +43,14 @@ export const SnapshotListPage = observer(() => {
|
|||||||
createEmptySnapshot,
|
createEmptySnapshot,
|
||||||
} = snapshotStore;
|
} = snapshotStore;
|
||||||
const canWriteDevices = authStore.canWrite("devices");
|
const canWriteDevices = authStore.canWrite("devices");
|
||||||
|
const canReadDevices = authStore.canRead("devices");
|
||||||
const canCreateSnapshot =
|
const canCreateSnapshot =
|
||||||
authStore.hasRole("snapshot_create") && canWriteDevices;
|
authStore.hasRole("snapshot_create") && canWriteDevices;
|
||||||
const canManageSnapshots = authStore.canWrite("snapshot") && canWriteDevices;
|
const canManageSnapshots = authStore.canWrite("snapshot") && canWriteDevices;
|
||||||
|
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isSnapshotOnDeviceWarning, setIsSnapshotOnDeviceWarning] = useState(false);
|
||||||
|
const [devicesWithSnapshot, setDevicesWithSnapshot] = useState<string[]>([]);
|
||||||
const [rowId, setRowId] = useState<string | null>(null);
|
const [rowId, setRowId] = useState<string | null>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
||||||
@@ -45,6 +58,12 @@ export const SnapshotListPage = observer(() => {
|
|||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [isEmptySnapshotModalOpen, setIsEmptySnapshotModalOpen] = useState(false);
|
const [isEmptySnapshotModalOpen, setIsEmptySnapshotModalOpen] = useState(false);
|
||||||
const [emptySnapshotName, setEmptySnapshotName] = useState("");
|
const [emptySnapshotName, setEmptySnapshotName] = useState("");
|
||||||
|
const [emptySnapshotNameError, setEmptySnapshotNameError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const exportNameRegex = useMemo(() => {
|
||||||
|
const names = cityStore.cities["ru"].data.map((c) => c.name.trim());
|
||||||
|
return buildExportNameRegex(names);
|
||||||
|
}, [cityStore.cities["ru"].data.length]);
|
||||||
const [isCreatingEmpty, setIsCreatingEmpty] = useState(false);
|
const [isCreatingEmpty, setIsCreatingEmpty] = useState(false);
|
||||||
const [paginationModel, setPaginationModel] = useState({
|
const [paginationModel, setPaginationModel] = useState({
|
||||||
page: 0,
|
page: 0,
|
||||||
@@ -61,7 +80,11 @@ export const SnapshotListPage = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSnapshots = async () => {
|
const fetchSnapshots = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await Promise.all([getSnapshots(), getStorageInfo()]);
|
const promises: Promise<void>[] = [getSnapshots(), getStorageInfo()];
|
||||||
|
if (canReadDevices && !vehicleStore.vehicles.loaded) {
|
||||||
|
promises.push(vehicleStore.getVehicles());
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
fetchSnapshots();
|
fetchSnapshots();
|
||||||
@@ -76,6 +99,26 @@ export const SnapshotListPage = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
|
{
|
||||||
|
field: "color",
|
||||||
|
headerName: "",
|
||||||
|
width: 28,
|
||||||
|
sortable: false,
|
||||||
|
disableColumnMenu: true,
|
||||||
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
|
<div className="flex items-center justify-center h-full w-full">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
backgroundColor: params.value,
|
||||||
|
borderRadius: "50%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: "name",
|
field: "name",
|
||||||
headerName: "Название",
|
headerName: "Название",
|
||||||
@@ -117,6 +160,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button
|
<button
|
||||||
|
title="Восстановить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsRestoreModalOpen(true);
|
setIsRestoreModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(params.row.id);
|
||||||
@@ -125,9 +169,22 @@ export const SnapshotListPage = observer(() => {
|
|||||||
<DatabaseBackup size={20} className="text-blue-500" />
|
<DatabaseBackup size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const snapshotId = params.row.id;
|
||||||
|
if (canReadDevices) {
|
||||||
|
const devicesUsing = vehicleStore.vehicles.data
|
||||||
|
.filter(v => v.vehicle.current_snapshot_uuid === snapshotId)
|
||||||
|
.map(v => v.vehicle.tail_number || v.vehicle.uuid || `ID ${v.vehicle.id}`);
|
||||||
|
if (devicesUsing.length > 0) {
|
||||||
|
setDevicesWithSnapshot(devicesUsing);
|
||||||
|
setIsSnapshotOnDeviceWarning(true);
|
||||||
|
setRowId(snapshotId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(snapshotId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 size={20} className="text-red-500" />
|
<Trash2 size={20} className="text-red-500" />
|
||||||
@@ -150,12 +207,13 @@ export const SnapshotListPage = observer(() => {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(query),
|
.includes(query),
|
||||||
)
|
)
|
||||||
.map((snapshot) => ({
|
.map((snapshot, index) => ({
|
||||||
id: snapshot.ID,
|
id: snapshot.ID,
|
||||||
name: snapshot.Name,
|
name: snapshot.Name,
|
||||||
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
||||||
created_at: formatCreationTime(snapshot.CreationTime),
|
created_at: formatCreationTime(snapshot.CreationTime),
|
||||||
occupied_disk_space_gb: snapshot.occupied_disk_space_gb,
|
occupied_disk_space_gb: snapshot.occupied_disk_space_gb,
|
||||||
|
color: SEGMENT_COLORS[index % SEGMENT_COLORS.length],
|
||||||
}));
|
}));
|
||||||
}, [snapshots, searchQuery]);
|
}, [snapshots, searchQuery]);
|
||||||
|
|
||||||
@@ -178,10 +236,11 @@ export const SnapshotListPage = observer(() => {
|
|||||||
disabled={isLowStorage}
|
disabled={isLowStorage}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEmptySnapshotName("");
|
setEmptySnapshotName("");
|
||||||
|
setEmptySnapshotNameError(null);
|
||||||
setIsEmptySnapshotModalOpen(true);
|
setIsEmptySnapshotModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Создать пустой снапшот
|
Создать пустой экспорт
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canCreateSnapshot && (
|
{canCreateSnapshot && (
|
||||||
@@ -203,7 +262,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full h-3 rounded-lg overflow-hidden bg-gray-100">
|
<div className="flex w-full h-3 rounded-lg overflow-hidden bg-gray-100">
|
||||||
{rows.map((row, i) => {
|
{rows.map((row) => {
|
||||||
const pct =
|
const pct =
|
||||||
row.occupied_disk_space_gb != null && totalGB > 0
|
row.occupied_disk_space_gb != null && totalGB > 0
|
||||||
? (row.occupied_disk_space_gb / totalGB) * 100
|
? (row.occupied_disk_space_gb / totalGB) * 100
|
||||||
@@ -214,8 +273,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
key={row.id}
|
key={row.id}
|
||||||
style={{
|
style={{
|
||||||
width: `${pct}%`,
|
width: `${pct}%`,
|
||||||
backgroundColor:
|
backgroundColor: row.color,
|
||||||
SEGMENT_COLORS[i % SEGMENT_COLORS.length],
|
|
||||||
}}
|
}}
|
||||||
title={`${row.name}: ${row.occupied_disk_space_gb?.toFixed(1)} ГБ`}
|
title={`${row.name}: ${row.occupied_disk_space_gb?.toFixed(1)} ГБ`}
|
||||||
/>
|
/>
|
||||||
@@ -233,7 +291,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-x-5 gap-y-1 mt-3">
|
<div className="flex flex-wrap gap-x-5 gap-y-1 mt-3">
|
||||||
{rows.map((row, i) => {
|
{rows.map((row) => {
|
||||||
if (row.occupied_disk_space_gb == null || row.occupied_disk_space_gb <= 0)
|
if (row.occupied_disk_space_gb == null || row.occupied_disk_space_gb <= 0)
|
||||||
return null;
|
return null;
|
||||||
return (
|
return (
|
||||||
@@ -243,10 +301,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="inline-block w-2.5 h-2.5 rounded-full"
|
className="inline-block w-2.5 h-2.5 rounded-full"
|
||||||
style={{
|
style={{ backgroundColor: row.color }}
|
||||||
backgroundColor:
|
|
||||||
SEGMENT_COLORS[i % SEGMENT_COLORS.length],
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{row.name}
|
{row.name}
|
||||||
</div>
|
</div>
|
||||||
@@ -280,7 +335,9 @@ export const SnapshotListPage = observer(() => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -325,14 +382,26 @@ export const SnapshotListPage = observer(() => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
maxWidth="xs"
|
maxWidth="xs"
|
||||||
>
|
>
|
||||||
<DialogTitle>Создать пустой снапшот</DialogTitle>
|
<DialogTitle>Создать пустой экспорт</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Название"
|
label="Название"
|
||||||
value={emptySnapshotName}
|
value={emptySnapshotName}
|
||||||
onChange={(e) => setEmptySnapshotName(e.target.value)}
|
error={!!emptySnapshotNameError}
|
||||||
|
helperText={emptySnapshotNameError ?? " "}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setEmptySnapshotName(val);
|
||||||
|
const trimmed = val.trim();
|
||||||
|
const hasFullFormat = trimmed.includes("_") && trimmed.split("_").slice(1).join("_").length > 0;
|
||||||
|
if (hasFullFormat && !exportNameRegex.test(trimmed)) {
|
||||||
|
setEmptySnapshotNameError("Название должно начинаться с названия существующего города");
|
||||||
|
} else {
|
||||||
|
setEmptySnapshotNameError(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -342,7 +411,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={!emptySnapshotName.trim() || isCreatingEmpty}
|
disabled={!exportNameRegex.test(emptySnapshotName.trim()) || isCreatingEmpty}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsCreatingEmpty(true);
|
setIsCreatingEmpty(true);
|
||||||
try {
|
try {
|
||||||
@@ -359,6 +428,36 @@ export const SnapshotListPage = observer(() => {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isSnapshotOnDeviceWarning}
|
||||||
|
onClose={() => setIsSnapshotOnDeviceWarning(false)}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="xs"
|
||||||
|
>
|
||||||
|
<DialogTitle>Удаление невозможно</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Alert severity="warning" sx={{ mt: 1 }}>
|
||||||
|
Этот экспорт загружен на устройства. Удалите или замените экспорт на
|
||||||
|
устройствах перед удалением.
|
||||||
|
</Alert>
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Typography variant="body2" fontWeight={600} gutterBottom>
|
||||||
|
Устройства:
|
||||||
|
</Typography>
|
||||||
|
{devicesWithSnapshot.map((name, i) => (
|
||||||
|
<Typography key={i} variant="body2">
|
||||||
|
• {name}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setIsSnapshotOnDeviceWarning(false)}>
|
||||||
|
Закрыть
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<SnapshotRestore
|
<SnapshotRestore
|
||||||
open={isRestoreModalOpen}
|
open={isRestoreModalOpen}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
mediaStore,
|
mediaStore,
|
||||||
isMediaIdEmpty,
|
isMediaIdEmpty,
|
||||||
useSelectedCity,
|
useSelectedCity,
|
||||||
|
selectedCityStore,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
@@ -32,6 +33,12 @@ import {
|
|||||||
|
|
||||||
export const StationCreatePage = observer(() => {
|
export const StationCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
mediaStore,
|
mediaStore,
|
||||||
isMediaIdEmpty,
|
isMediaIdEmpty,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
|
selectedCityStore,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
@@ -34,6 +35,12 @@ import { LinkedSights } from "../LinkedSights";
|
|||||||
|
|
||||||
export const StationEditPage = observer(() => {
|
export const StationEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const StationListPage = observer(() => {
|
|||||||
loadSightCounts(stationIds);
|
loadSightCounts(stationIds);
|
||||||
};
|
};
|
||||||
fetchStations();
|
fetchStations();
|
||||||
}, [language]);
|
}, [language, selectedCityStore.cityVersion]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
@@ -86,13 +86,13 @@ export const StationListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "sightCount",
|
field: "sightCount",
|
||||||
headerName: "Достопримечательности",
|
headerName: "Привязки",
|
||||||
width: 180,
|
width: 180,
|
||||||
align: "center" as const,
|
align: "center" as const,
|
||||||
headerAlign: "center" as const,
|
headerAlign: "center" as const,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
renderHeader: (params) => (
|
renderHeader: (params) => (
|
||||||
<Tooltip title="Количество привязанных достопримечательностей">
|
<Tooltip title="Отображает количество привязок">
|
||||||
<span>{params.colDef.headerName}</span>
|
<span>{params.colDef.headerName}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@@ -114,7 +114,7 @@ export const StationListPage = observer(() => {
|
|||||||
headerAlign: "center" as const,
|
headerAlign: "center" as const,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
renderHeader: (params) => (
|
renderHeader: (params) => (
|
||||||
<Tooltip title="Подтверждение добавленных пересадок">
|
<Tooltip title="Отображает подтверждение добавленных пересадок">
|
||||||
<span>{params.colDef.headerName}</span>
|
<span>{params.colDef.headerName}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@@ -141,7 +141,7 @@ export const StationListPage = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
{canWriteStations && (
|
{canWriteStations && (
|
||||||
<button onClick={() => navigate(`/station/${params.row.id}/edit`)}>
|
<button title="Редактировать" onClick={() => navigate(`/station/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -151,13 +151,14 @@ export const StationListPage = observer(() => {
|
|||||||
setSelectedStationId(params.row.id);
|
setSelectedStationId(params.row.id);
|
||||||
setIsTransfersModalOpen(true);
|
setIsTransfersModalOpen(true);
|
||||||
}}
|
}}
|
||||||
title="Редактировать пересадки"
|
title="Управление пересадками"
|
||||||
>
|
>
|
||||||
<Route size={20} className="text-purple-500" />
|
<Route size={20} className="text-purple-500" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{canWriteStations && (
|
{canWriteStations && (
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(params.row.id);
|
||||||
@@ -174,9 +175,12 @@ export const StationListPage = observer(() => {
|
|||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
const { selectedCityId } = selectedCityStore;
|
const { selectedCityId } = selectedCityStore;
|
||||||
|
if (!selectedCityId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const query = searchQuery.trim().toLowerCase();
|
const query = searchQuery.trim().toLowerCase();
|
||||||
return stationLists[language].data
|
return stationLists[language].data
|
||||||
.filter((station: any) => !selectedCityId || station.city_id === selectedCityId)
|
.filter((station: any) => station.city_id === selectedCityId)
|
||||||
.filter(
|
.filter(
|
||||||
(station: any) =>
|
(station: any) =>
|
||||||
!query ||
|
!query ||
|
||||||
@@ -202,7 +206,10 @@ export const StationListPage = observer(() => {
|
|||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Остановки</h1>
|
<h1 className="text-2xl">Остановки</h1>
|
||||||
{canWriteStations && (
|
{canWriteStations && (
|
||||||
<CreateButton label="Создать остановку" path="/station/create" />
|
<CreateButton
|
||||||
|
label="Создать остановку"
|
||||||
|
path="/station/create"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -218,7 +225,9 @@ export const StationListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -277,7 +286,13 @@ export const StationListPage = observer(() => {
|
|||||||
slots={{
|
slots={{
|
||||||
noRowsOverlay: () => (
|
noRowsOverlay: () => (
|
||||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||||
{isLoading ? <CircularProgress size={20} /> : "Нет остановок"}
|
{isLoading ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : !selectedCityStore.selectedCityId ? (
|
||||||
|
"Выберите город"
|
||||||
|
) : (
|
||||||
|
"Нет остановок"
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Button, Paper, TextField } from "@mui/material";
|
import {
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -10,9 +17,10 @@ import {
|
|||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
PreviewMediaDialog,
|
PreviewMediaDialog,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { ImageUploadCard } from "@widgets";
|
import { ImageUploadCard, PermissionsTable, RolesHintTable, ROLE_RESOURCES } from "@widgets";
|
||||||
|
|
||||||
export const UserCreatePage = observer(() => {
|
export const UserCreatePage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -26,13 +34,38 @@ export const UserCreatePage = observer(() => {
|
|||||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
const [localRoles, setLocalRoles] = useState<string[]>(
|
||||||
|
createUserData.roles ?? ["articles_ro", "articles_rw", "media_ro", "media_rw"]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mediaStore.getMedia();
|
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 () => {
|
const handleCreate = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
// Убеждаемся, что роли в сторе обновлены перед созданием
|
||||||
|
userStore.createUserData.roles = localRoles;
|
||||||
await createUser();
|
await createUser();
|
||||||
toast.success("Пользователь успешно создан");
|
toast.success("Пользователь успешно создан");
|
||||||
navigate("/user");
|
navigate("/user");
|
||||||
@@ -67,18 +100,15 @@ export const UserCreatePage = observer(() => {
|
|||||||
: selectedMedia?.id ?? createUserData.icon ?? null;
|
: selectedMedia?.id ?? createUserData.icon ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<Paper className="w-full p-6 flex flex-col gap-8">
|
||||||
<div className="flex items-center gap-4">
|
<button className="flex items-center gap-2 self-start" onClick={() => navigate(-1)}>
|
||||||
<button
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
>
|
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
Назад
|
Назад
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<section className="flex flex-col gap-6">
|
||||||
|
<Typography variant="h6">Основные данные</Typography>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Имя"
|
label="Имя"
|
||||||
@@ -116,6 +146,7 @@ export const UserCreatePage = observer(() => {
|
|||||||
label="Пароль"
|
label="Пароль"
|
||||||
value={createUserData.password || ""}
|
value={createUserData.password || ""}
|
||||||
required
|
required
|
||||||
|
type="password"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setCreateUserData(
|
setCreateUserData(
|
||||||
createUserData.name || "",
|
createUserData.name || "",
|
||||||
@@ -127,7 +158,7 @@ export const UserCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
|
<div className="w-full flex flex-col gap-4 max-w-[300px]">
|
||||||
<ImageUploadCard
|
<ImageUploadCard
|
||||||
title="Аватар"
|
title="Аватар"
|
||||||
imageKey="thumbnail"
|
imageKey="thumbnail"
|
||||||
@@ -156,14 +187,64 @@ export const UserCreatePage = observer(() => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<Typography variant="h6">Права доступа</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setCreateUserData(
|
||||||
|
createUserData.name || "",
|
||||||
|
createUserData.email || "",
|
||||||
|
createUserData.password || "",
|
||||||
|
true,
|
||||||
|
createUserData.icon
|
||||||
|
);
|
||||||
|
const next: string[] = [];
|
||||||
|
for (const { key } of ROLE_RESOURCES) {
|
||||||
|
next.push(`${key}_rw`);
|
||||||
|
}
|
||||||
|
next.push("snapshot_create");
|
||||||
|
setLocalRoles(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Полный доступ (admin)
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setCreateUserData(
|
||||||
|
createUserData.name || "",
|
||||||
|
createUserData.email || "",
|
||||||
|
createUserData.password || "",
|
||||||
|
false,
|
||||||
|
createUserData.icon
|
||||||
|
);
|
||||||
|
setLocalRoles(["devices_ro", "vehicles_ro", "devices_maintenance_rw"]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Администратор ТО
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<PermissionsTable localRoles={localRoles} setLocalRoles={setLocalRoles} />
|
||||||
|
<RolesHintTable />
|
||||||
|
</section>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
className="w-min flex gap-2 items-center"
|
className="self-end w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={
|
disabled={
|
||||||
isLoading || !createUserData.name || !createUserData.password
|
isLoading || !createUserData.name || !createUserData.password || !createUserData.email
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -172,7 +253,6 @@ export const UserCreatePage = observer(() => {
|
|||||||
"Создать"
|
"Создать"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<SelectMediaDialog
|
<SelectMediaDialog
|
||||||
open={isSelectMediaOpen}
|
open={isSelectMediaOpen}
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControlLabel,
|
|
||||||
Checkbox,
|
|
||||||
Paper,
|
Paper,
|
||||||
TextField,
|
TextField,
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Radio,
|
|
||||||
RadioGroup,
|
|
||||||
Divider,
|
Divider,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
@@ -31,45 +22,12 @@ import {
|
|||||||
authStore,
|
authStore,
|
||||||
cityStore,
|
cityStore,
|
||||||
MultiSelect,
|
MultiSelect,
|
||||||
|
selectedCityStore,
|
||||||
type User,
|
type User,
|
||||||
type UserCity,
|
type UserCity,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ImageUploadCard, DeleteModal } from "@widgets";
|
import { ImageUploadCard, DeleteModal, PermissionsTable, RolesHintTable, ROLE_RESOURCES } 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 UserEditPage = observer(() => {
|
export const UserEditPage = observer(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -93,10 +51,29 @@ export const UserEditPage = observer(() => {
|
|||||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
languageStore.setLanguage("ru");
|
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(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -311,175 +288,36 @@ export const UserEditPage = observer(() => {
|
|||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<Typography variant="h6">Права доступа</Typography>
|
<Typography variant="h6">Права доступа</Typography>
|
||||||
|
|
||||||
<FormControlLabel
|
<Box sx={{ display: "flex", gap: 1 }}>
|
||||||
control={
|
<Button
|
||||||
<Checkbox
|
variant="outlined"
|
||||||
checked={localRoles.includes("admin")}
|
size="small"
|
||||||
onChange={(e) => {
|
onClick={() => {
|
||||||
if (e.target.checked) {
|
setEditUserData(editUserData.name || "", editUserData.email || "", editUserData.password || "", true, editUserData.icon || "");
|
||||||
setLocalRoles((prev) => {
|
const next: string[] = [];
|
||||||
let next = prev.filter((r) => r !== "admin");
|
|
||||||
for (const { key } of ROLE_RESOURCES) {
|
for (const { key } of ROLE_RESOURCES) {
|
||||||
next = next.filter((r) => r !== `${key}_ro` && r !== `${key}_rw`);
|
|
||||||
next.push(`${key}_rw`);
|
next.push(`${key}_rw`);
|
||||||
}
|
}
|
||||||
if (!next.includes("snapshot_create")) {
|
|
||||||
next.push("snapshot_create");
|
next.push("snapshot_create");
|
||||||
}
|
setLocalRoles(next);
|
||||||
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)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box sx={{ border: "1px solid", borderColor: "divider", borderRadius: 1 }}>
|
|
||||||
<Table size="small">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow sx={{ bgcolor: "action.hover" }}>
|
|
||||||
<TableCell sx={{ fontWeight: 600, width: 220 }}>Ресурс</TableCell>
|
|
||||||
<TableCell align="center" sx={{ fontWeight: 600 }}>Нет доступа</TableCell>
|
|
||||||
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение</TableCell>
|
|
||||||
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение/Запись</TableCell>
|
|
||||||
<TableCell align="center" sx={{ fontWeight: 600 }}>
|
|
||||||
Доп. права
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<TableRow key={key} hover>
|
|
||||||
<TableCell>{label}</TableCell>
|
|
||||||
<TableCell align="center" padding="checkbox">
|
|
||||||
<RadioGroup
|
|
||||||
row
|
|
||||||
value={level}
|
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
|
||||||
sx={{ justifyContent: "center", flexWrap: "nowrap" }}
|
|
||||||
>
|
>
|
||||||
<Radio value="none" size="small" />
|
Полный доступ (admin)
|
||||||
</RadioGroup>
|
</Button>
|
||||||
</TableCell>
|
<Button
|
||||||
<TableCell align="center" padding="checkbox">
|
variant="outlined"
|
||||||
{isSnapshotResource ? (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
-
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<RadioGroup
|
|
||||||
row
|
|
||||||
value={level}
|
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
|
||||||
sx={{ justifyContent: "center", flexWrap: "nowrap" }}
|
|
||||||
>
|
|
||||||
<Radio value="ro" size="small" />
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center" padding="checkbox">
|
|
||||||
<RadioGroup
|
|
||||||
row
|
|
||||||
value={level}
|
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
|
||||||
sx={{ justifyContent: "center", flexWrap: "nowrap" }}
|
|
||||||
>
|
|
||||||
<Radio value="rw" size="small" />
|
|
||||||
</RadioGroup>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center" padding="checkbox">
|
|
||||||
{isSnapshotResource ? (
|
|
||||||
<Checkbox
|
|
||||||
checked={localRoles.includes("snapshot_create")}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleSnapshotCreateChange(e.target.checked)
|
|
||||||
}
|
|
||||||
size="small"
|
size="small"
|
||||||
title="Разрешает создавать новые снапшоты"
|
onClick={() => {
|
||||||
/>
|
setEditUserData(editUserData.name || "", editUserData.email || "", editUserData.password || "", false, editUserData.icon || "");
|
||||||
) : isDevicesResource ? (
|
setLocalRoles(["devices_ro", "vehicles_ro", "devices_maintenance_rw"]);
|
||||||
<Checkbox
|
}}
|
||||||
checked={localRoles.includes("devices_maintenance_rw")}
|
>
|
||||||
onChange={(e) => handleMaintenanceChange(e.target.checked)}
|
Администратор ТО
|
||||||
size="small"
|
</Button>
|
||||||
title="Разрешает переводить устройства в режим технического обслуживания"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
-
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<PermissionsTable localRoles={localRoles} setLocalRoles={setLocalRoles} />
|
||||||
|
<RolesHintTable />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@@ -92,10 +92,11 @@ export const UserListPage = observer(() => {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params: GridRenderCellParams) => (
|
renderCell: (params: GridRenderCellParams) => (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
<button onClick={() => navigate(`/user/${params.row.id}/edit`)}>
|
<button title="Редактировать" onClick={() => navigate(`/user/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(params.row.id);
|
||||||
@@ -146,7 +147,9 @@ export const UserListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
@@ -154,6 +157,11 @@ export const UserListPage = observer(() => {
|
|||||||
checkboxSelection={canWriteUsers}
|
checkboxSelection={canWriteUsers}
|
||||||
disableRowSelectionExcludeModel
|
disableRowSelectionExcludeModel
|
||||||
disableRowSelectionOnClick
|
disableRowSelectionOnClick
|
||||||
|
onRowDoubleClick={(params) => {
|
||||||
|
if (canWriteUsers) {
|
||||||
|
navigate(`/user/${params.row.id}/edit`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
paginationModel={paginationModel}
|
paginationModel={paginationModel}
|
||||||
onPaginationModelChange={setPaginationModel}
|
onPaginationModelChange={setPaginationModel}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
VEHICLE_TYPES,
|
VEHICLE_TYPES,
|
||||||
carrierStore,
|
carrierStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
|
cityStore,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Save } from "lucide-react";
|
import { ArrowLeft, Save } from "lucide-react";
|
||||||
@@ -26,11 +28,18 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
const [type, setType] = useState("");
|
const [type, setType] = useState("");
|
||||||
const [carrierId, setCarrierId] = useState<number | null>(null);
|
const [carrierId, setCarrierId] = useState<number | null>(null);
|
||||||
const [model, setModel] = useState("");
|
const [model, setModel] = useState("");
|
||||||
|
const [cityId, setCityId] = useState<number | null>(selectedCityStore.selectedCityId);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
carrierStore.getCarriers(language);
|
carrierStore.getCarriers(language);
|
||||||
|
cityStore.getCities("ru");
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
@@ -43,6 +52,7 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
?.full_name as string,
|
?.full_name as string,
|
||||||
carrierId!,
|
carrierId!,
|
||||||
model || undefined,
|
model || undefined,
|
||||||
|
cityId ?? undefined,
|
||||||
);
|
);
|
||||||
toast.success("Транспорт успешно создан");
|
toast.success("Транспорт успешно создан");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -73,12 +83,11 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
onChange={(e) => setTailNumber(e.target.value)}
|
onChange={(e) => setTailNumber(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Тип</InputLabel>
|
<InputLabel>Тип</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={type}
|
value={type}
|
||||||
label="Тип"
|
label="Тип"
|
||||||
required
|
|
||||||
onChange={(e) => setType(e.target.value)}
|
onChange={(e) => setType(e.target.value)}
|
||||||
>
|
>
|
||||||
{VEHICLE_TYPES.map((type) => (
|
{VEHICLE_TYPES.map((type) => (
|
||||||
@@ -89,12 +98,11 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Перевозчик</InputLabel>
|
<InputLabel>Перевозчик</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={carrierId || ""}
|
value={carrierId || ""}
|
||||||
label="Перевозчик"
|
label="Перевозчик"
|
||||||
required
|
|
||||||
onChange={(e) => setCarrierId(e.target.value as number)}
|
onChange={(e) => setCarrierId(e.target.value as number)}
|
||||||
>
|
>
|
||||||
{carrierStore.carriers[language].data?.map((carrier) => (
|
{carrierStore.carriers[language].data?.map((carrier) => (
|
||||||
@@ -113,12 +121,27 @@ export const VehicleCreatePage = observer(() => {
|
|||||||
placeholder="Произвольное название модели"
|
placeholder="Произвольное название модели"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Город</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={cityId ?? ""}
|
||||||
|
label="Город"
|
||||||
|
onChange={(e) => setCityId(e.target.value ? Number(e.target.value) : null)}
|
||||||
|
>
|
||||||
|
{cityStore.cities.ru.data.map((city) => (
|
||||||
|
<MenuItem key={city.id} value={city.id}>
|
||||||
|
{city.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={isLoading || !tailNumber || !type || !carrierId}
|
disabled={isLoading || !tailNumber || !type || !carrierId || !cityId}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={20} className="animate-spin" />
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
VEHICLE_TYPES,
|
VEHICLE_TYPES,
|
||||||
vehicleStore,
|
vehicleStore,
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
|
cityStore,
|
||||||
|
selectedCityStore,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
@@ -38,6 +40,11 @@ export const VehicleEditPage = observer(() => {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedCityStore.setIsLocked(true);
|
||||||
|
return () => selectedCityStore.setIsLocked(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
// Устанавливаем русский язык при загрузке страницы
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
@@ -54,6 +61,7 @@ export const VehicleEditPage = observer(() => {
|
|||||||
try {
|
try {
|
||||||
await getVehicle(Number(id));
|
await getVehicle(Number(id));
|
||||||
await getCarriers(language);
|
await getCarriers(language);
|
||||||
|
await cityStore.getCities("ru");
|
||||||
|
|
||||||
setEditVehicleData({
|
setEditVehicleData({
|
||||||
tail_number: vehicle[Number(id)]?.vehicle.tail_number ?? "",
|
tail_number: vehicle[Number(id)]?.vehicle.tail_number ?? "",
|
||||||
@@ -63,6 +71,7 @@ export const VehicleEditPage = observer(() => {
|
|||||||
model: vehicle[Number(id)]?.vehicle.model ?? "",
|
model: vehicle[Number(id)]?.vehicle.model ?? "",
|
||||||
snapshot_update_blocked:
|
snapshot_update_blocked:
|
||||||
vehicle[Number(id)]?.vehicle.snapshot_update_blocked ?? false,
|
vehicle[Number(id)]?.vehicle.snapshot_update_blocked ?? false,
|
||||||
|
city_id: vehicle[Number(id)]?.vehicle.city_id ?? selectedCityStore.selectedCityId ?? undefined,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingData(false);
|
setIsLoadingData(false);
|
||||||
@@ -125,12 +134,11 @@ export const VehicleEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Тип</InputLabel>
|
<InputLabel>Тип</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={editVehicleData.type}
|
value={editVehicleData.type}
|
||||||
label="Тип"
|
label="Тип"
|
||||||
required
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditVehicleData({ ...editVehicleData, type: e.target.value })
|
setEditVehicleData({ ...editVehicleData, type: e.target.value })
|
||||||
}
|
}
|
||||||
@@ -143,12 +151,11 @@ export const VehicleEditPage = observer(() => {
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Перевозчик</InputLabel>
|
<InputLabel>Перевозчик</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={editVehicleData.carrier_id}
|
value={editVehicleData.carrier_id}
|
||||||
label="Перевозчик"
|
label="Перевозчик"
|
||||||
required
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditVehicleData({
|
setEditVehicleData({
|
||||||
...editVehicleData,
|
...editVehicleData,
|
||||||
@@ -177,6 +184,26 @@ export const VehicleEditPage = observer(() => {
|
|||||||
placeholder="Произвольное название модели"
|
placeholder="Произвольное название модели"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth required>
|
||||||
|
<InputLabel>Город</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={editVehicleData.city_id ?? ""}
|
||||||
|
label="Город"
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditVehicleData({
|
||||||
|
...editVehicleData,
|
||||||
|
city_id: e.target.value ? Number(e.target.value) : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{cityStore.cities.ru.data.map((city) => (
|
||||||
|
<MenuItem key={city.id} value={city.id}>
|
||||||
|
{city.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -202,7 +229,8 @@ export const VehicleEditPage = observer(() => {
|
|||||||
isLoading ||
|
isLoading ||
|
||||||
!editVehicleData.tail_number ||
|
!editVehicleData.tail_number ||
|
||||||
!editVehicleData.type ||
|
!editVehicleData.type ||
|
||||||
!editVehicleData.carrier_id
|
!editVehicleData.carrier_id ||
|
||||||
|
!editVehicleData.city_id
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
import { ruRU } from "@mui/x-data-grid/locales";
|
import { ruRU } from "@mui/x-data-grid/locales";
|
||||||
import { authStore, carrierStore, languageStore, vehicleStore, SearchInput } from "@shared";
|
import { authStore, carrierStore, languageStore, vehicleStore, SearchInput, selectedCityStore, cityStore } from "@shared";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
|
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
|
||||||
@@ -11,7 +11,7 @@ import { Box, CircularProgress } from "@mui/material";
|
|||||||
|
|
||||||
export const VehicleListPage = observer(() => {
|
export const VehicleListPage = observer(() => {
|
||||||
const { vehicles, getVehicles, deleteVehicle } = vehicleStore;
|
const { vehicles, getVehicles, deleteVehicle } = vehicleStore;
|
||||||
const { carriers, getCarriers } = carrierStore;
|
const { getCarriers } = carrierStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
|
||||||
@@ -31,10 +31,11 @@ export const VehicleListPage = observer(() => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await getVehicles();
|
await getVehicles();
|
||||||
await getCarriers(language);
|
await getCarriers(language);
|
||||||
|
await cityStore.getCities("ru");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [language]);
|
}, [language, selectedCityStore.cityVersion]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
@@ -114,15 +115,16 @@ export const VehicleListPage = observer(() => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full gap-7 justify-center items-center">
|
<div className="flex h-full gap-7 justify-center items-center">
|
||||||
{canWrite && (
|
{canWrite && (
|
||||||
<button onClick={() => navigate(`/vehicle/${params.row.id}/edit`)}>
|
<button title="Редактировать" onClick={() => navigate(`/vehicle/${params.row.id}/edit`)}>
|
||||||
<Pencil size={20} className="text-blue-500" />
|
<Pencil size={20} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => navigate(`/vehicle/${params.row.id}`)}>
|
<button title="Просмотр" onClick={() => navigate(`/vehicle/${params.row.id}`)}>
|
||||||
<Eye size={20} className="text-green-500" />
|
<Eye size={20} className="text-green-500" />
|
||||||
</button>
|
</button>
|
||||||
{canWrite && (
|
{canWrite && (
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
setRowId(params.row.id);
|
setRowId(params.row.id);
|
||||||
@@ -137,9 +139,13 @@ export const VehicleListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const { selectedCityId } = selectedCityStore;
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
|
if (!selectedCityId) return [];
|
||||||
const query = searchQuery.trim().toLowerCase();
|
const query = searchQuery.trim().toLowerCase();
|
||||||
return (vehicles.data ?? [])
|
return (vehicles.data ?? [])
|
||||||
|
.filter((vehicle) => vehicle.vehicle.city_id === selectedCityId)
|
||||||
.filter(
|
.filter(
|
||||||
(vehicle) =>
|
(vehicle) =>
|
||||||
!query ||
|
!query ||
|
||||||
@@ -151,11 +157,9 @@ export const VehicleListPage = observer(() => {
|
|||||||
tail_number: vehicle.vehicle.tail_number,
|
tail_number: vehicle.vehicle.tail_number,
|
||||||
type: vehicle.vehicle.type,
|
type: vehicle.vehicle.type,
|
||||||
carrier: vehicle.vehicle.carrier,
|
carrier: vehicle.vehicle.carrier,
|
||||||
city: carriers[language].data?.find(
|
city: cityStore.cities.ru.data.find((c) => c.id === vehicle.vehicle.city_id)?.name,
|
||||||
(carrier) => carrier.id === vehicle.vehicle.carrier_id
|
|
||||||
)?.city,
|
|
||||||
}));
|
}));
|
||||||
}, [vehicles.data, carriers[language].data, searchQuery]);
|
}, [vehicles.data, selectedCityId, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -169,7 +173,9 @@ export const VehicleListPage = observer(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(rows.length > 0 || searchQuery) && (
|
||||||
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
)}
|
||||||
|
|
||||||
{canWriteVehicles && ids.length > 0 && (
|
{canWriteVehicles && ids.length > 0 && (
|
||||||
<div className="flex justify-end mb-5 duration-300">
|
<div className="flex justify-end mb-5 duration-300">
|
||||||
@@ -223,6 +229,8 @@ export const VehicleListPage = observer(() => {
|
|||||||
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<CircularProgress size={20} />
|
<CircularProgress size={20} />
|
||||||
|
) : !selectedCityId ? (
|
||||||
|
"Выберите город"
|
||||||
) : (
|
) : (
|
||||||
"Нет транспортных средств"
|
"Нет транспортных средств"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -103,8 +103,26 @@ export const UploadMediaDialog = observer(
|
|||||||
|
|
||||||
setMediaFile(initialFile);
|
setMediaFile(initialFile);
|
||||||
setMediaFilename(initialFile.name);
|
setMediaFilename(initialFile.name);
|
||||||
|
|
||||||
|
const extension = initialFile.name.split(".").pop()?.toLowerCase();
|
||||||
|
if (extension) {
|
||||||
|
if (["glb", "gltf"].includes(extension)) {
|
||||||
|
setAvailableMediaTypes([6]);
|
||||||
|
setMediaType(6);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
||||||
|
extension
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setAvailableMediaTypes([1, 3, 4, 5]);
|
||||||
|
setMediaType(1);
|
||||||
|
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||||
setAvailableMediaTypes([2]);
|
setAvailableMediaTypes([2]);
|
||||||
setMediaType(2);
|
setMediaType(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newBlobUrl = URL.createObjectURL(initialFile);
|
const newBlobUrl = URL.createObjectURL(initialFile);
|
||||||
setMediaUrl(newBlobUrl);
|
setMediaUrl(newBlobUrl);
|
||||||
previousMediaUrlRef.current = newBlobUrl;
|
previousMediaUrlRef.current = newBlobUrl;
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ export type Carrier = {
|
|||||||
city: string;
|
city: string;
|
||||||
city_id: number;
|
city_id: number;
|
||||||
logo: string;
|
logo: string;
|
||||||
// main_color: string;
|
main_color: string;
|
||||||
// left_color: string;
|
left_color: string;
|
||||||
// right_color: string;
|
right_color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CarrierData = {
|
type CarrierData = {
|
||||||
@@ -112,6 +112,9 @@ class CarrierStore {
|
|||||||
createCarrierData = {
|
createCarrierData = {
|
||||||
city_id: 0,
|
city_id: 0,
|
||||||
logo: "",
|
logo: "",
|
||||||
|
main_color: "",
|
||||||
|
left_color: "",
|
||||||
|
right_color: "",
|
||||||
ru: {
|
ru: {
|
||||||
full_name: "",
|
full_name: "",
|
||||||
short_name: "",
|
short_name: "",
|
||||||
@@ -135,10 +138,16 @@ class CarrierStore {
|
|||||||
cityId: number,
|
cityId: number,
|
||||||
slogan: string,
|
slogan: string,
|
||||||
logoId: string,
|
logoId: string,
|
||||||
language: Language
|
language: Language,
|
||||||
|
colors?: { main_color?: string; left_color?: string; right_color?: string }
|
||||||
) => {
|
) => {
|
||||||
this.createCarrierData.city_id = cityId;
|
this.createCarrierData.city_id = cityId;
|
||||||
this.createCarrierData.logo = logoId;
|
this.createCarrierData.logo = logoId;
|
||||||
|
if (colors) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
this.createCarrierData[language] = {
|
this.createCarrierData[language] = {
|
||||||
full_name: fullName,
|
full_name: fullName,
|
||||||
short_name: shortName,
|
short_name: shortName,
|
||||||
@@ -198,9 +207,10 @@ class CarrierStore {
|
|||||||
city: cityName,
|
city: cityName,
|
||||||
city_id: this.createCarrierData.city_id,
|
city_id: this.createCarrierData.city_id,
|
||||||
slogan: (this.createCarrierData[language].slogan || "").trim(),
|
slogan: (this.createCarrierData[language].slogan || "").trim(),
|
||||||
...(this.createCarrierData.logo
|
...(this.createCarrierData.logo ? { logo: this.createCarrierData.logo } : {}),
|
||||||
? { logo: this.createCarrierData.logo }
|
...(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 } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await languageInstance(language).post("/carrier", payload);
|
const response = await languageInstance(language).post("/carrier", payload);
|
||||||
@@ -243,6 +253,9 @@ class CarrierStore {
|
|||||||
this.createCarrierData = {
|
this.createCarrierData = {
|
||||||
city_id: 0,
|
city_id: 0,
|
||||||
logo: "",
|
logo: "",
|
||||||
|
main_color: "",
|
||||||
|
left_color: "",
|
||||||
|
right_color: "",
|
||||||
ru: {
|
ru: {
|
||||||
full_name: "",
|
full_name: "",
|
||||||
short_name: "",
|
short_name: "",
|
||||||
@@ -265,53 +278,44 @@ class CarrierStore {
|
|||||||
ru: {
|
ru: {
|
||||||
full_name: "",
|
full_name: "",
|
||||||
short_name: "",
|
short_name: "",
|
||||||
|
|
||||||
// main_color: "",
|
|
||||||
// left_color: "",
|
|
||||||
// right_color: "",
|
|
||||||
slogan: "",
|
slogan: "",
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
full_name: "",
|
full_name: "",
|
||||||
short_name: "",
|
short_name: "",
|
||||||
|
slogan: "",
|
||||||
// main_color: "",
|
},
|
||||||
// left_color: "",
|
zh: {
|
||||||
// right_color: "",
|
full_name: "",
|
||||||
|
short_name: "",
|
||||||
slogan: "",
|
slogan: "",
|
||||||
},
|
},
|
||||||
city_id: 0,
|
city_id: 0,
|
||||||
logo: "",
|
logo: "",
|
||||||
zh: {
|
main_color: "",
|
||||||
full_name: "",
|
left_color: "",
|
||||||
short_name: "",
|
right_color: "",
|
||||||
|
|
||||||
// main_color: "",
|
|
||||||
// left_color: "",
|
|
||||||
// right_color: "",
|
|
||||||
slogan: "",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setEditCarrierData = (
|
setEditCarrierData = (
|
||||||
fullName: string,
|
fullName: string,
|
||||||
shortName: string,
|
shortName: string,
|
||||||
cityId: number,
|
cityId: number,
|
||||||
// main_color: string,
|
|
||||||
// left_color: string,
|
|
||||||
// right_color: string,
|
|
||||||
slogan: string,
|
slogan: string,
|
||||||
logoId: string,
|
logoId: string,
|
||||||
language: Language
|
language: Language,
|
||||||
|
colors?: { main_color?: string; left_color?: string; right_color?: string }
|
||||||
) => {
|
) => {
|
||||||
this.editCarrierData.city_id = cityId;
|
this.editCarrierData.city_id = cityId;
|
||||||
this.editCarrierData.logo = logoId;
|
this.editCarrierData.logo = logoId;
|
||||||
|
if (colors) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
this.editCarrierData[language] = {
|
this.editCarrierData[language] = {
|
||||||
full_name: fullName,
|
full_name: fullName,
|
||||||
short_name: shortName,
|
short_name: shortName,
|
||||||
// main_color: main_color,
|
|
||||||
// left_color: left_color,
|
|
||||||
// right_color: right_color,
|
|
||||||
slogan: slogan,
|
slogan: slogan,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -326,9 +330,10 @@ class CarrierStore {
|
|||||||
slogan: (this.editCarrierData[lang].slogan || "").trim(),
|
slogan: (this.editCarrierData[lang].slogan || "").trim(),
|
||||||
city: cityName,
|
city: cityName,
|
||||||
city_id: this.editCarrierData.city_id,
|
city_id: this.editCarrierData.city_id,
|
||||||
...(this.editCarrierData.logo
|
...(this.editCarrierData.logo ? { logo: this.editCarrierData.logo } : {}),
|
||||||
? { logo: this.editCarrierData.logo }
|
...(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 } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type Route = {
|
|||||||
center_latitude: number;
|
center_latitude: number;
|
||||||
center_longitude: number;
|
center_longitude: number;
|
||||||
governor_appeal: number;
|
governor_appeal: number;
|
||||||
|
button_text?: string;
|
||||||
id: number;
|
id: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
path: number[][];
|
path: number[][];
|
||||||
@@ -143,6 +144,7 @@ class RouteStore {
|
|||||||
center_latitude: "",
|
center_latitude: "",
|
||||||
center_longitude: "",
|
center_longitude: "",
|
||||||
governor_appeal: 0,
|
governor_appeal: 0,
|
||||||
|
button_text: "" as string | undefined,
|
||||||
id: 0,
|
id: 0,
|
||||||
icon: "",
|
icon: "",
|
||||||
path: [] as number[][],
|
path: [] as number[][],
|
||||||
@@ -153,7 +155,7 @@ class RouteStore {
|
|||||||
scale_max: 0,
|
scale_max: 0,
|
||||||
scale_min: 0,
|
scale_min: 0,
|
||||||
video_preview: "" as string | undefined,
|
video_preview: "" as string | undefined,
|
||||||
video_timer: 60,
|
video_timer: 420,
|
||||||
};
|
};
|
||||||
|
|
||||||
setEditRouteData = (data: any) => {
|
setEditRouteData = (data: any) => {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { City } from "../CityStore";
|
|||||||
|
|
||||||
class SelectedCityStore {
|
class SelectedCityStore {
|
||||||
selectedCity: City | null = null;
|
selectedCity: City | null = null;
|
||||||
|
isLocked: boolean = false;
|
||||||
|
cityVersion: number = 0;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
@@ -24,6 +26,7 @@ class SelectedCityStore {
|
|||||||
setSelectedCity = (city: City | null) => {
|
setSelectedCity = (city: City | null) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.selectedCity = city;
|
this.selectedCity = city;
|
||||||
|
this.cityVersion += 1;
|
||||||
if (city) {
|
if (city) {
|
||||||
localStorage.setItem("selectedCity", JSON.stringify(city));
|
localStorage.setItem("selectedCity", JSON.stringify(city));
|
||||||
} else {
|
} else {
|
||||||
@@ -32,6 +35,12 @@ class SelectedCityStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setIsLocked = (locked: boolean) => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.isLocked = locked;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
clearSelectedCity = () => {
|
clearSelectedCity = () => {
|
||||||
this.setSelectedCity(null);
|
this.setSelectedCity(null);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,12 +60,6 @@ class SnapshotStore {
|
|||||||
articlesStore.articleData = null;
|
articlesStore.articleData = null;
|
||||||
articlesStore.articleMedia = null;
|
articlesStore.articleMedia = null;
|
||||||
|
|
||||||
countryStore.countries = {
|
|
||||||
ru: { data: [], loaded: false },
|
|
||||||
en: { data: [], loaded: false },
|
|
||||||
zh: { data: [], loaded: false },
|
|
||||||
};
|
|
||||||
|
|
||||||
carrierStore.carriers = {
|
carrierStore.carriers = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class VehicleStore {
|
|||||||
carrier: string,
|
carrier: string,
|
||||||
carrierId: number,
|
carrierId: number,
|
||||||
model?: string,
|
model?: string,
|
||||||
|
cityId?: number,
|
||||||
) => {
|
) => {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
tail_number: tailNumber.trim(),
|
tail_number: tailNumber.trim(),
|
||||||
@@ -129,6 +130,7 @@ class VehicleStore {
|
|||||||
};
|
};
|
||||||
// TODO: когда будет бекенд — добавить model в payload и в ответ
|
// TODO: когда будет бекенд — добавить model в payload и в ответ
|
||||||
if (model != null && model !== "") payload.model = model;
|
if (model != null && model !== "") payload.model = model;
|
||||||
|
if (cityId != null) payload.city_id = cityId;
|
||||||
const response = await languageInstance("ru").post("/vehicle", payload);
|
const response = await languageInstance("ru").post("/vehicle", payload);
|
||||||
const normalizedVehicle = this.normalizeVehicleItem(response.data);
|
const normalizedVehicle = this.normalizeVehicleItem(response.data);
|
||||||
|
|
||||||
@@ -147,6 +149,7 @@ class VehicleStore {
|
|||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
model: string;
|
model: string;
|
||||||
snapshot_update_blocked: boolean;
|
snapshot_update_blocked: boolean;
|
||||||
|
city_id?: number;
|
||||||
} = {
|
} = {
|
||||||
tail_number: "",
|
tail_number: "",
|
||||||
type: 0,
|
type: 0,
|
||||||
@@ -154,6 +157,7 @@ class VehicleStore {
|
|||||||
carrier_id: 0,
|
carrier_id: 0,
|
||||||
model: "",
|
model: "",
|
||||||
snapshot_update_blocked: false,
|
snapshot_update_blocked: false,
|
||||||
|
city_id: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
setEditVehicleData = (data: {
|
setEditVehicleData = (data: {
|
||||||
@@ -163,6 +167,7 @@ class VehicleStore {
|
|||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
model?: string;
|
model?: string;
|
||||||
snapshot_update_blocked?: boolean;
|
snapshot_update_blocked?: boolean;
|
||||||
|
city_id?: number;
|
||||||
}) => {
|
}) => {
|
||||||
this.editVehicleData = {
|
this.editVehicleData = {
|
||||||
...this.editVehicleData,
|
...this.editVehicleData,
|
||||||
@@ -179,6 +184,7 @@ class VehicleStore {
|
|||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
model?: string;
|
model?: string;
|
||||||
snapshot_update_blocked?: boolean;
|
snapshot_update_blocked?: boolean;
|
||||||
|
city_id?: number;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
@@ -190,6 +196,7 @@ class VehicleStore {
|
|||||||
if (data.model != null && data.model !== "") payload.model = data.model;
|
if (data.model != null && data.model !== "") payload.model = data.model;
|
||||||
if (data.snapshot_update_blocked != null)
|
if (data.snapshot_update_blocked != null)
|
||||||
payload.snapshot_update_blocked = data.snapshot_update_blocked;
|
payload.snapshot_update_blocked = data.snapshot_update_blocked;
|
||||||
|
if (data.city_id != null) payload.city_id = data.city_id;
|
||||||
const response = await languageInstance("ru").patch(
|
const response = await languageInstance("ru").patch(
|
||||||
`/vehicle/${id}`,
|
`/vehicle/${id}`,
|
||||||
payload,
|
payload,
|
||||||
|
|||||||
@@ -6,13 +6,23 @@ import {
|
|||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
Typography,
|
Typography,
|
||||||
Box,
|
Box,
|
||||||
|
keyframes,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { authStore, cityStore, selectedCityStore, type City } from "@shared";
|
import { authStore, cityStore, selectedCityStore, snapshotStore, type City } from "@shared";
|
||||||
import { MapPin } from "lucide-react";
|
import { MapPin } from "lucide-react";
|
||||||
|
|
||||||
|
const borderSpin = keyframes`
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 50%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const CitySelector: React.FC = observer(() => {
|
export const CitySelector: React.FC = observer(() => {
|
||||||
const { selectedCity, setSelectedCity } = selectedCityStore;
|
const { selectedCity, setSelectedCity, isLocked } = selectedCityStore;
|
||||||
const canLoadAllCities = authStore.isAdmin && authStore.canRead("cities");
|
const canLoadAllCities = authStore.isAdmin && authStore.canRead("cities");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -43,41 +53,59 @@ export const CitySelector: React.FC = observer(() => {
|
|||||||
})()
|
})()
|
||||||
: baseCities;
|
: baseCities;
|
||||||
|
|
||||||
|
const noCitySelected = !selectedCity?.id;
|
||||||
|
|
||||||
const handleCityChange = (event: SelectChangeEvent<string>) => {
|
const handleCityChange = (event: SelectChangeEvent<string>) => {
|
||||||
const cityId = event.target.value;
|
const cityId = event.target.value;
|
||||||
if (cityId === "") {
|
if (cityId === "") {
|
||||||
|
snapshotStore.clearStoreCache();
|
||||||
setSelectedCity(null);
|
setSelectedCity(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const city = currentCities.find((c) => c.id === Number(cityId));
|
const city = currentCities.find((c) => c.id === Number(cityId));
|
||||||
if (city) {
|
if (city) {
|
||||||
|
snapshotStore.clearStoreCache();
|
||||||
setSelectedCity(city);
|
setSelectedCity(city);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const selectElement = (
|
||||||
<Box className="flex items-center gap-2">
|
|
||||||
<MapPin size={16} className="text-white" />
|
|
||||||
<FormControl size="medium" sx={{ minWidth: 120 }}>
|
|
||||||
<Select
|
<Select
|
||||||
value={selectedCity?.id?.toString() || ""}
|
value={selectedCity?.id?.toString() || ""}
|
||||||
onChange={handleCityChange}
|
onChange={handleCityChange}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
|
disabled={isLocked}
|
||||||
sx={{
|
sx={{
|
||||||
height: "40px",
|
height: "40px",
|
||||||
color: "white",
|
color: "white",
|
||||||
|
borderRadius: "4px",
|
||||||
|
...(noCitySelected && !isLocked
|
||||||
|
? {
|
||||||
|
backgroundColor: "#48989f",
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": { border: "none" },
|
||||||
|
}
|
||||||
|
: {
|
||||||
"& .MuiOutlinedInput-notchedOutline": {
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
borderColor: "rgba(255, 255, 255, 0.3)",
|
borderColor: isLocked
|
||||||
|
? "rgba(255, 255, 255, 0.1)"
|
||||||
|
: "rgba(255, 255, 255, 0.3)",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "rgba(255, 255, 255, 0.5)",
|
||||||
|
WebkitTextFillColor: "rgba(255, 255, 255, 0.5)",
|
||||||
},
|
},
|
||||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
borderColor: isLocked
|
||||||
|
? "rgba(255, 255, 255, 0.1)"
|
||||||
|
: "rgba(255, 255, 255, 0.5)",
|
||||||
},
|
},
|
||||||
"&.Mui.focused .MuiOutlinedInput-notchedOutline": {
|
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||||
borderColor: "white",
|
borderColor: "white",
|
||||||
},
|
},
|
||||||
"& .MuiSvgIcon-root": {
|
"& .MuiSvgIcon-root": {
|
||||||
color: "white",
|
color: isLocked ? "rgba(255, 255, 255, 0.3)" : "white",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -90,6 +118,28 @@ export const CitySelector: React.FC = observer(() => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="flex items-center gap-2">
|
||||||
|
<MapPin size={16} className={isLocked ? "text-gray-400" : "text-white"} />
|
||||||
|
<FormControl size="medium" sx={{ minWidth: 120 }}>
|
||||||
|
{noCitySelected && !isLocked ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "2px",
|
||||||
|
background: "linear-gradient(90deg, rgba(255,255,255,0.1), rgba(255,255,255,0.7), rgba(255,255,255,0.1), rgba(255,255,255,0.7), rgba(255,255,255,0.1))",
|
||||||
|
backgroundSize: "200% 100%",
|
||||||
|
animation: `${borderSpin} 2.5s linear infinite`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectElement}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
selectElement
|
||||||
|
)}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,30 +24,51 @@ const shiftYYYYMMDD = (value: string, days: number) => {
|
|||||||
|
|
||||||
type LogLevel = "info" | "warn" | "error" | "debug" | "fatal" | "unknown";
|
type LogLevel = "info" | "warn" | "error" | "debug" | "fatal" | "unknown";
|
||||||
|
|
||||||
const LOG_LEVEL_STYLES: Record<LogLevel, { badge: string; text: string }> = {
|
const LOG_LEVEL_STYLES: Record<
|
||||||
|
LogLevel,
|
||||||
|
{ badge: string; text: string; bg: string; color: string; borderColor: string }
|
||||||
|
> = {
|
||||||
info: {
|
info: {
|
||||||
badge: "bg-blue-100 text-blue-700",
|
badge: "bg-blue-100 text-blue-700",
|
||||||
text: "text-[#000000BF]",
|
text: "text-[#000000BF]",
|
||||||
|
bg: "#DBEAFE",
|
||||||
|
color: "#1D4ED8",
|
||||||
|
borderColor: "#93C5FD",
|
||||||
},
|
},
|
||||||
debug: {
|
debug: {
|
||||||
badge: "bg-gray-100 text-gray-600",
|
badge: "bg-gray-100 text-gray-600",
|
||||||
text: "text-gray-600",
|
text: "text-gray-600",
|
||||||
|
bg: "#F3F4F6",
|
||||||
|
color: "#4B5563",
|
||||||
|
borderColor: "#D1D5DB",
|
||||||
},
|
},
|
||||||
warn: {
|
warn: {
|
||||||
badge: "bg-amber-100 text-amber-700",
|
badge: "bg-amber-100 text-amber-700",
|
||||||
text: "text-amber-800",
|
text: "text-amber-800",
|
||||||
|
bg: "#FEF3C7",
|
||||||
|
color: "#B45309",
|
||||||
|
borderColor: "#FCD34D",
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
badge: "bg-red-100 text-red-700",
|
badge: "bg-red-100 text-red-700",
|
||||||
text: "text-red-700",
|
text: "text-red-700",
|
||||||
|
bg: "#FEE2E2",
|
||||||
|
color: "#B91C1C",
|
||||||
|
borderColor: "#FCA5A5",
|
||||||
},
|
},
|
||||||
fatal: {
|
fatal: {
|
||||||
badge: "bg-red-200 text-red-900",
|
badge: "bg-red-200 text-red-900",
|
||||||
text: "text-red-900 font-semibold",
|
text: "text-red-900 font-semibold",
|
||||||
|
bg: "#FECACA",
|
||||||
|
color: "#7F1D1D",
|
||||||
|
borderColor: "#F87171",
|
||||||
},
|
},
|
||||||
unknown: {
|
unknown: {
|
||||||
badge: "bg-gray-100 text-gray-500",
|
badge: "bg-gray-100 text-gray-500",
|
||||||
text: "text-[#000000BF]",
|
text: "text-[#000000BF]",
|
||||||
|
bg: "#F3F4F6",
|
||||||
|
color: "#6B7280",
|
||||||
|
borderColor: "#D1D5DB",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,6 +160,23 @@ export const DeviceLogsModal = ({
|
|||||||
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
||||||
const [dateFrom, setDateFrom] = useState(toYYYYMMDD(yesterday));
|
const [dateFrom, setDateFrom] = useState(toYYYYMMDD(yesterday));
|
||||||
const [dateTo, setDateTo] = useState(toYYYYMMDD(today));
|
const [dateTo, setDateTo] = useState(toYYYYMMDD(today));
|
||||||
|
|
||||||
|
const ALL_LEVELS: LogLevel[] = ["debug", "info", "warn", "error", "fatal"];
|
||||||
|
const [activeLevels, setActiveLevels] = useState<Set<LogLevel>>(
|
||||||
|
new Set(ALL_LEVELS)
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleLevel = (level: LogLevel) => {
|
||||||
|
setActiveLevels((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(level)) {
|
||||||
|
next.delete(level);
|
||||||
|
} else {
|
||||||
|
next.add(level);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
const dateToMin = shiftYYYYMMDD(dateFrom, 1);
|
const dateToMin = shiftYYYYMMDD(dateFrom, 1);
|
||||||
const dateFromMax = shiftYYYYMMDD(dateTo, -1);
|
const dateFromMax = shiftYYYYMMDD(dateTo, -1);
|
||||||
|
|
||||||
@@ -205,16 +243,21 @@ export const DeviceLogsModal = ({
|
|||||||
return parsed;
|
return parsed;
|
||||||
}, [chunks]);
|
}, [chunks]);
|
||||||
|
|
||||||
|
const filteredLogs = useMemo(
|
||||||
|
() => logs.filter((log) => activeLevels.has(log.level)),
|
||||||
|
[logs, activeLevels]
|
||||||
|
);
|
||||||
|
|
||||||
const logsText = useMemo(
|
const logsText = useMemo(
|
||||||
() =>
|
() =>
|
||||||
logs
|
filteredLogs
|
||||||
.map((log) => {
|
.map((log) => {
|
||||||
const level = log.level === "unknown" ? "LOG" : log.level.toUpperCase();
|
const level = log.level === "unknown" ? "LOG" : log.level.toUpperCase();
|
||||||
const time = log.time ? `[${log.time}] ` : "";
|
const time = log.time ? `[${log.time}] ` : "";
|
||||||
return `${time}${level}: ${log.text}`;
|
return `${time}${level}: ${log.text}`;
|
||||||
})
|
})
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
[logs]
|
[filteredLogs]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDownloadLogs = () => {
|
const handleDownloadLogs = () => {
|
||||||
@@ -253,6 +296,28 @@ export const DeviceLogsModal = ({
|
|||||||
<div className="flex flex-col gap-6 h-[85vh]">
|
<div className="flex flex-col gap-6 h-[85vh]">
|
||||||
<div className="flex gap-4 items-center justify-between w-full flex-wrap">
|
<div className="flex gap-4 items-center justify-between w-full flex-wrap">
|
||||||
<h2 className="text-2xl font-semibold text-[#000000BF]">Логи</h2>
|
<h2 className="text-2xl font-semibold text-[#000000BF]">Логи</h2>
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
{ALL_LEVELS.map((level) => {
|
||||||
|
const active = activeLevels.has(level);
|
||||||
|
const s = LOG_LEVEL_STYLES[level];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={level}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleLevel(level)}
|
||||||
|
className="cursor-pointer select-none rounded-md px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wide transition-all duration-150"
|
||||||
|
style={{
|
||||||
|
backgroundColor: active ? s.bg : "transparent",
|
||||||
|
color: active ? s.color : "#9CA3AF",
|
||||||
|
border: `1.5px solid ${active ? s.borderColor : "#E5E7EB"}`,
|
||||||
|
opacity: active ? 1 : 0.55,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{level}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<TextField
|
<TextField
|
||||||
type="date"
|
type="date"
|
||||||
@@ -280,7 +345,7 @@ export const DeviceLogsModal = ({
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={handleDownloadLogs}
|
onClick={handleDownloadLogs}
|
||||||
disabled={isLoading || Boolean(error) || logs.length === 0}
|
disabled={isLoading || Boolean(error) || filteredLogs.length === 0}
|
||||||
>
|
>
|
||||||
Скачать .txt
|
Скачать .txt
|
||||||
</Button>
|
</Button>
|
||||||
@@ -303,8 +368,8 @@ export const DeviceLogsModal = ({
|
|||||||
{!isLoading && !error && (
|
{!isLoading && !error && (
|
||||||
<div className="w-full h-full overflow-y-auto rounded-xl">
|
<div className="w-full h-full overflow-y-auto rounded-xl">
|
||||||
<div className="flex flex-col gap-0.5 font-mono text-[13px]">
|
<div className="flex flex-col gap-0.5 font-mono text-[13px]">
|
||||||
{logs.length > 0 ? (
|
{filteredLogs.length > 0 ? (
|
||||||
logs.map((log) => {
|
filteredLogs.map((log) => {
|
||||||
const style = LOG_LEVEL_STYLES[log.level];
|
const style = LOG_LEVEL_STYLES[log.level];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
vehicleStore,
|
vehicleStore,
|
||||||
routeStore,
|
routeStore,
|
||||||
Vehicle,
|
Vehicle,
|
||||||
carrierStore,
|
|
||||||
selectedCityStore,
|
selectedCityStore,
|
||||||
menuStore,
|
menuStore,
|
||||||
VEHICLE_TYPES,
|
VEHICLE_TYPES,
|
||||||
@@ -184,31 +183,14 @@ export const DevicesTable = observer(() => {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterVehiclesBySelectedCity = (vehiclesList: Vehicle[]): Vehicle[] => {
|
const { selectedCityId } = selectedCityStore;
|
||||||
const selectedCityId = selectedCityStore.selectedCityId;
|
|
||||||
|
|
||||||
if (!selectedCityId) {
|
const filteredVehicles = useMemo((): Vehicle[] => {
|
||||||
return vehiclesList;
|
if (!selectedCityId) return [];
|
||||||
}
|
return (vehicles.data as Vehicle[]).filter(
|
||||||
|
(vehicle) => vehicle.vehicle.city_id === selectedCityId,
|
||||||
const carriersInSelectedCityIds = new Set(
|
|
||||||
carrierStore.carriers.ru.data
|
|
||||||
.filter((carrier) => carrier.city_id === selectedCityId)
|
|
||||||
.map((carrier) => carrier.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (carriersInSelectedCityIds.size === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return vehiclesList.filter((vehicle) =>
|
|
||||||
carriersInSelectedCityIds.has(vehicle.vehicle.carrier_id),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredVehicles = filterVehiclesBySelectedCity(
|
|
||||||
vehicles.data as Vehicle[],
|
|
||||||
);
|
);
|
||||||
|
}, [selectedCityId, vehicles.data]);
|
||||||
|
|
||||||
const rows = useMemo(
|
const rows = useMemo(
|
||||||
() => transformToRows(filteredVehicles),
|
() => transformToRows(filteredVehicles),
|
||||||
@@ -628,13 +610,15 @@ export const DevicesTable = observer(() => {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!isMaintenanceOnly && (
|
||||||
|
<>
|
||||||
{canWriteDevices && (
|
{canWriteDevices && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate(`/vehicle/${row.vehicle_id}/edit`);
|
navigate(`/vehicle/${row.vehicle_id}/edit`);
|
||||||
}}
|
}}
|
||||||
title="Редактировать транспорт"
|
title="Редактировать"
|
||||||
>
|
>
|
||||||
<Pencil size={16} />
|
<Pencil size={16} />
|
||||||
</button>
|
</button>
|
||||||
@@ -644,7 +628,7 @@ export const DevicesTable = observer(() => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleReloadStatus();
|
handleReloadStatus();
|
||||||
}}
|
}}
|
||||||
title="Перезапросить статус"
|
title="Обновить статус"
|
||||||
disabled={
|
disabled={
|
||||||
!row.device_uuid || !devices.includes(row.device_uuid)
|
!row.device_uuid || !devices.includes(row.device_uuid)
|
||||||
}
|
}
|
||||||
@@ -663,17 +647,6 @@ export const DevicesTable = observer(() => {
|
|||||||
>
|
>
|
||||||
<Copy size={16} />
|
<Copy size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setSessionsModalVehicleId(row.vehicle_id);
|
|
||||||
setSessionsModalVehicleTailNumber(row.tail_number);
|
|
||||||
setSessionsModalOpen(true);
|
|
||||||
}}
|
|
||||||
title="Сессии ТО"
|
|
||||||
>
|
|
||||||
<Wrench size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -682,10 +655,23 @@ export const DevicesTable = observer(() => {
|
|||||||
setLogsModalOpen(true);
|
setLogsModalOpen(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="Логи устройства"
|
title="Логи"
|
||||||
>
|
>
|
||||||
<ScrollText size={16} />
|
<ScrollText size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSessionsModalVehicleId(row.vehicle_id);
|
||||||
|
setSessionsModalVehicleTailNumber(row.tail_number);
|
||||||
|
setSessionsModalOpen(true);
|
||||||
|
}}
|
||||||
|
title="Сессии обслуживания"
|
||||||
|
>
|
||||||
|
<Wrench size={16} />
|
||||||
|
</button>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -714,9 +700,11 @@ export const DevicesTable = observer(() => {
|
|||||||
|
|
||||||
const visibleColumns = useMemo(() => {
|
const visibleColumns = useMemo(() => {
|
||||||
if (isMaintenanceOnly) {
|
if (isMaintenanceOnly) {
|
||||||
return columns.filter((c) =>
|
return columns
|
||||||
["model", "tail_number", "maintenance_mode_on"].includes(c.field),
|
.filter((c) =>
|
||||||
);
|
["model", "tail_number", "maintenance_mode_on", "actions"].includes(c.field),
|
||||||
|
)
|
||||||
|
.map((c) => ({ ...c, flex: 1, width: undefined, minWidth: undefined }));
|
||||||
}
|
}
|
||||||
if (!canWriteDevices) {
|
if (!canWriteDevices) {
|
||||||
return columns.filter(
|
return columns.filter(
|
||||||
@@ -729,20 +717,21 @@ export const DevicesTable = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
if (isMaintenanceOnly) {
|
||||||
|
await Promise.all([getVehicles(), getDevices()]);
|
||||||
|
} else {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
getVehicles(),
|
getVehicles(),
|
||||||
getDevices(),
|
getDevices(),
|
||||||
getSnapshots(),
|
getSnapshots(),
|
||||||
getRoutes(),
|
getRoutes(),
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [getDevices, getSnapshots, getVehicles, getRoutes]);
|
}, [getDevices, getSnapshots, getVehicles, getRoutes, isMaintenanceOnly, selectedCityStore.cityVersion]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
carrierStore.getCarriers("ru");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOpenSendSnapshotModal = () => {
|
const handleOpenSendSnapshotModal = () => {
|
||||||
if (!canWriteDevices) {
|
if (!canWriteDevices) {
|
||||||
@@ -876,6 +865,8 @@ export const DevicesTable = observer(() => {
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<CircularProgress size={20} />
|
<CircularProgress size={20} />
|
||||||
|
) : !selectedCityId ? (
|
||||||
|
"Выберите город"
|
||||||
) : (
|
) : (
|
||||||
"Нет устройств для отображения"
|
"Нет устройств для отображения"
|
||||||
)}
|
)}
|
||||||
@@ -921,8 +912,12 @@ export const DevicesTable = observer(() => {
|
|||||||
checkboxSelection={canWriteDevices}
|
checkboxSelection={canWriteDevices}
|
||||||
disableRowSelectionExcludeModel
|
disableRowSelectionExcludeModel
|
||||||
disableRowSelectionOnClick
|
disableRowSelectionOnClick
|
||||||
loading={isLoading}
|
onRowDoubleClick={(params) => {
|
||||||
paginationModel={paginationModel}
|
if (canWriteDevices) {
|
||||||
|
navigate(`/vehicle/${params.row.vehicle_id}/edit`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
loading={isLoading} paginationModel={paginationModel}
|
||||||
onPaginationModelChange={setPaginationModel}
|
onPaginationModelChange={setPaginationModel}
|
||||||
pageSizeOptions={[50]}
|
pageSizeOptions={[50]}
|
||||||
onRowSelectionModelChange={
|
onRowSelectionModelChange={
|
||||||
|
|||||||
128
src/widgets/PermissionsTable/PermissionsTable.tsx
Normal file
128
src/widgets/PermissionsTable/PermissionsTable.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { ROLE_RESOURCES, getPermissionLevel, applyPermissionChange, type PermissionLevel } from "./constants";
|
||||||
|
|
||||||
|
interface PermissionsTableProps {
|
||||||
|
localRoles: string[];
|
||||||
|
setLocalRoles: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PermissionsTable({ localRoles, setLocalRoles }: PermissionsTableProps) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ border: "1px solid", borderColor: "divider", borderRadius: 1 }}>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow sx={{ bgcolor: "action.hover" }}>
|
||||||
|
<TableCell sx={{ fontWeight: 600, width: 220 }}>Ресурс</TableCell>
|
||||||
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Нет доступа</TableCell>
|
||||||
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение</TableCell>
|
||||||
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Чтение/Запись</TableCell>
|
||||||
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Доп. права</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{ROLE_RESOURCES.map(({ key, label }) => {
|
||||||
|
const level = getPermissionLevel(localRoles, key);
|
||||||
|
const isSnapshotResource = key === "snapshot";
|
||||||
|
const isDevicesResource = key === "devices";
|
||||||
|
|
||||||
|
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 handleSnapshotCreateChange = (checked: boolean) => {
|
||||||
|
setLocalRoles((prev) => {
|
||||||
|
const without = prev.filter((role) => role !== "snapshot_create");
|
||||||
|
return checked ? [...without, "snapshot_create"] : without;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMaintenanceChange = (checked: boolean) => {
|
||||||
|
setLocalRoles((prev) => {
|
||||||
|
const without = prev.filter((r) => r !== "devices_maintenance_rw");
|
||||||
|
return checked ? [...without, "devices_maintenance_rw"] : without;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={key} hover>
|
||||||
|
<TableCell>{label}</TableCell>
|
||||||
|
<TableCell align="center" padding="checkbox">
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={level}
|
||||||
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
|
sx={{ justifyContent: "center", flexWrap: "nowrap" }}
|
||||||
|
>
|
||||||
|
<Radio value="none" size="small" />
|
||||||
|
</RadioGroup>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" padding="checkbox">
|
||||||
|
{isSnapshotResource ? (
|
||||||
|
<Typography variant="body2" color="text.secondary">-</Typography>
|
||||||
|
) : (
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={level}
|
||||||
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
|
sx={{ justifyContent: "center", flexWrap: "nowrap" }}
|
||||||
|
>
|
||||||
|
<Radio value="ro" size="small" />
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" padding="checkbox">
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={level}
|
||||||
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
|
sx={{ justifyContent: "center", flexWrap: "nowrap" }}
|
||||||
|
>
|
||||||
|
<Radio value="rw" size="small" />
|
||||||
|
</RadioGroup>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" padding="checkbox">
|
||||||
|
{isSnapshotResource ? (
|
||||||
|
<Checkbox
|
||||||
|
checked={localRoles.includes("snapshot_create")}
|
||||||
|
onChange={(e) => handleSnapshotCreateChange(e.target.checked)}
|
||||||
|
size="small"
|
||||||
|
title="Разрешает создавать новые снапшоты"
|
||||||
|
/>
|
||||||
|
) : isDevicesResource ? (
|
||||||
|
<Box sx={{ display: "flex", gap: 0.5, justifyContent: "center" }}>
|
||||||
|
<Checkbox
|
||||||
|
checked={localRoles.includes("devices_maintenance_rw")}
|
||||||
|
onChange={(e) => handleMaintenanceChange(e.target.checked)}
|
||||||
|
size="small"
|
||||||
|
title="Техническое обслуживание (ТО)"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary">-</Typography>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/widgets/PermissionsTable/RolesHintTable.tsx
Normal file
49
src/widgets/PermissionsTable/RolesHintTable.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
const ROLE_HINTS = [
|
||||||
|
{ tab: "Экспорт", roles: "Экспорт (Ч/З)" },
|
||||||
|
{ tab: "Создание экспорта", roles: "Экспорт (доп. права) + Устройства (Ч/З)" },
|
||||||
|
{ tab: "Устройства", roles: "Устройства + Транспорт + Маршруты + Перевозчики + Экспорт (Ч/З)" },
|
||||||
|
{ tab: "Карта", roles: "Маршруты (Ч/З) или Остановки (Ч/З) или Достопримечательности (Ч/З)" },
|
||||||
|
{ tab: "Пользователи", roles: "Пользователи" },
|
||||||
|
{ tab: "Достопримечательности", roles: "Достопримечательности" },
|
||||||
|
{ tab: "Остановки", roles: "Остановки" },
|
||||||
|
{ tab: "Маршруты", roles: "Маршруты + Перевозчики" },
|
||||||
|
{ tab: "Страны", roles: "Страны" },
|
||||||
|
{ tab: "Города", roles: "Города + Страны" },
|
||||||
|
{ tab: "Перевозчики", roles: "Перевозчики" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function RolesHintTable() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 2, p: 2, bgcolor: "grey.50", borderRadius: 1, border: "1px solid", borderColor: "divider" }}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Какие роли нужны для вкладок
|
||||||
|
</Typography>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Вкладка</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Необходимые роли</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{ROLE_HINTS.map(({ tab, roles }) => (
|
||||||
|
<TableRow key={tab}>
|
||||||
|
<TableCell sx={{ py: 0.5 }}>{tab}</TableCell>
|
||||||
|
<TableCell sx={{ py: 0.5 }}>{roles}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/widgets/PermissionsTable/constants.ts
Normal file
33
src/widgets/PermissionsTable/constants.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export 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;
|
||||||
|
|
||||||
|
export type PermissionLevel = "none" | "ro" | "rw";
|
||||||
|
|
||||||
|
export function getPermissionLevel(roles: string[], resource: string): PermissionLevel {
|
||||||
|
if (roles.includes(`${resource}_rw`)) return "rw";
|
||||||
|
if (roles.includes(`${resource}_ro`)) return "ro";
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
3
src/widgets/PermissionsTable/index.ts
Normal file
3
src/widgets/PermissionsTable/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { PermissionsTable } from "./PermissionsTable";
|
||||||
|
export { RolesHintTable } from "./RolesHintTable";
|
||||||
|
export { ROLE_RESOURCES, type PermissionLevel, getPermissionLevel, applyPermissionChange } from "./constants";
|
||||||
@@ -120,7 +120,6 @@ export const ReactMarkdownEditor = ({
|
|||||||
"table",
|
"table",
|
||||||
"horizontal-rule",
|
"horizontal-rule",
|
||||||
"preview",
|
"preview",
|
||||||
"fullscreen",
|
|
||||||
"guide",
|
"guide",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -243,46 +243,39 @@ export const LeftWidgetTab = observer(
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
maxWidth: "320px",
|
maxWidth: "316px",
|
||||||
gap: 0.5,
|
gap: 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<Paper
|
|
||||||
elevation={3}
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
minWidth: 320,
|
|
||||||
background:
|
|
||||||
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
|
||||||
overflowY: "auto",
|
|
||||||
display: "flex",
|
|
||||||
|
|
||||||
flexDirection: "column",
|
|
||||||
borderRadius: "10px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
overflow: "hidden",
|
width: "316px",
|
||||||
width: "100%",
|
|
||||||
minHeight: 100,
|
|
||||||
padding: "3px",
|
|
||||||
position: "relative",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
borderRadius: "10px",
|
||||||
|
background:
|
||||||
"& img": {
|
"linear-gradient(114deg, rgba(255,255,255,0) 8.71%, rgba(255,255,255,0.16) 69.69%), #006F3A",
|
||||||
borderTopLeftRadius: "10px",
|
|
||||||
borderTopRightRadius: "10px",
|
|
||||||
width: "100%",
|
|
||||||
height: "auto",
|
|
||||||
objectFit: "contain",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data.left.media.length > 0 ? (
|
{data.left.media.length > 0 ? (
|
||||||
<>
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
width: "312px",
|
||||||
|
height: "175px",
|
||||||
|
margin: "2px 0px 2px 0px",
|
||||||
|
borderRadius: "10px 10px 0 0",
|
||||||
|
overflow: "hidden",
|
||||||
|
flexShrink: 0,
|
||||||
|
"& img, & video": {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: "10px 10px 0 0",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
media={{
|
media={{
|
||||||
id: data.left.media[0].id,
|
id: data.left.media[0].id,
|
||||||
@@ -293,99 +286,80 @@ export const LeftWidgetTab = observer(
|
|||||||
/>
|
/>
|
||||||
{sight.common.watermark_lu && (
|
{sight.common.watermark_lu && (
|
||||||
<img
|
<img
|
||||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${sight.common.watermark_lu}/download?token=${token}`}
|
||||||
sight.common.watermark_lu
|
|
||||||
}/download?token=${token}`}
|
|
||||||
alt="preview"
|
alt="preview"
|
||||||
className="absolute top-4 left-4 z-10"
|
className="absolute top-4 left-4 z-10"
|
||||||
style={{
|
style={{ width: "30px", height: "30px", objectFit: "contain" }}
|
||||||
width: "30px",
|
|
||||||
height: "30px",
|
|
||||||
objectFit: "contain",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sight.common.watermark_rd && (
|
{sight.common.watermark_rd && (
|
||||||
<img
|
<img
|
||||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
src={`${import.meta.env.VITE_KRBL_MEDIA}${sight.common.watermark_rd}/download?token=${token}`}
|
||||||
sight.common.watermark_rd
|
|
||||||
}/download?token=${token}`}
|
|
||||||
alt="preview"
|
alt="preview"
|
||||||
className="absolute bottom-4 right-4 z-10"
|
className="absolute bottom-4 right-4 z-10"
|
||||||
style={{
|
style={{ width: "30px", height: "30px", objectFit: "contain" }}
|
||||||
width: "30px",
|
|
||||||
height: "30px",
|
|
||||||
objectFit: "contain",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<ImagePlus size={48} color="white" />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center", height: "175px" }}>
|
||||||
|
<ImagePlus size={48} color="white" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ padding: "0px 10px 20px 10px", width: "100%" }}>
|
||||||
<Box
|
<Box
|
||||||
|
component="div"
|
||||||
sx={{
|
sx={{
|
||||||
background:
|
color: "#fff",
|
||||||
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
fontFamily: "Roboto",
|
||||||
color: "white",
|
fontSize: "20px",
|
||||||
margin: "5px 0px 5px 0px",
|
fontWeight: 600,
|
||||||
display: "flex",
|
lineHeight: "150%",
|
||||||
flexDirection: "column",
|
|
||||||
gap: 1,
|
|
||||||
padding: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="h5"
|
|
||||||
component="h2"
|
|
||||||
sx={{
|
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
fontSize: "24px",
|
|
||||||
fontWeight: 700,
|
|
||||||
lineHeight: "120%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data?.left?.heading || "Название информации"}
|
{data?.left?.heading || "Название информации"}
|
||||||
</Typography>
|
</Box>
|
||||||
<Typography
|
<Box
|
||||||
variant="h6"
|
component="div"
|
||||||
component="h2"
|
|
||||||
sx={{
|
sx={{
|
||||||
|
marginTop: "2px",
|
||||||
|
color: "#fff",
|
||||||
|
fontFamily: "Roboto",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: "150%",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
fontSize: "18px",
|
|
||||||
|
|
||||||
lineHeight: "120%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sight[language as Language].address}
|
{sight[language as Language].address}
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{data?.left?.body && (
|
{data?.left?.body && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
padding: 1,
|
marginTop: "15px",
|
||||||
maxHeight: "300px",
|
maxHeight: "200px",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
width: "100%",
|
"&::-webkit-scrollbar": { display: "none" },
|
||||||
"&::-webkit-scrollbar": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
"&": {
|
|
||||||
scrollbarWidth: "none",
|
scrollbarWidth: "none",
|
||||||
|
"& p, & li, & h1, & h2, & h3": {
|
||||||
|
color: "#fff !important",
|
||||||
|
fontFamily: "Roboto, sans-serif !important",
|
||||||
|
fontSize: "16px !important",
|
||||||
|
fontWeight: "300 !important",
|
||||||
|
lineHeight: "135% !important",
|
||||||
|
marginTop: "0 !important",
|
||||||
|
marginBottom: "0 !important",
|
||||||
},
|
},
|
||||||
background:
|
|
||||||
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
|
||||||
flexGrow: 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ReactMarkdownComponent value={data?.left?.body} />
|
<ReactMarkdownComponent value={data?.left?.body} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
rgba(255, 255, 255, 0) 8.71%,
|
rgba(255, 255, 255, 0) 8.71%,
|
||||||
rgba(255, 255, 255, 0.16) 69.69%
|
rgba(255, 255, 255, 0.16) 69.69%
|
||||||
),
|
),
|
||||||
#806c59;
|
#006f3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sfp-sight-frame-media-stack {
|
.sfp-sight-frame-media-stack {
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
width: calc(100% - 4px);
|
width: calc(100% - 4px);
|
||||||
height: 300px;
|
height: 300px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #111;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sfp-sight-frame-media-item {
|
.sfp-sight-frame-media-item {
|
||||||
@@ -67,21 +66,21 @@
|
|||||||
.sfp-sight-frame-title {
|
.sfp-sight-frame-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 7px 16px;
|
padding: 10px 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-family: "Roboto";
|
font-family: "Roboto";
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 120%;
|
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:
|
background:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgba(255, 255, 255, 0.22) 0%,
|
rgba(255, 255, 255, 0.22) 0%,
|
||||||
rgba(255, 255, 255, 0.04) 100%
|
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-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -118,7 +117,7 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-height: calc(80vh - 354px);
|
max-height: calc(80vh - 354px);
|
||||||
min-height: 80px;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -156,12 +155,16 @@
|
|||||||
.sfp-sight-frame-text h3,
|
.sfp-sight-frame-text h3,
|
||||||
.sfp-sight-frame-text li {
|
.sfp-sight-frame-text li {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 150%;
|
||||||
|
font-family: "Roboto";
|
||||||
}
|
}
|
||||||
|
|
||||||
.sfp-sight-frame-menu {
|
.sfp-sight-frame-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
@@ -173,7 +176,7 @@
|
|||||||
rgba(255, 255, 255, 0.2) 0%,
|
rgba(255, 255, 255, 0.2) 0%,
|
||||||
rgba(255, 255, 255, 0) 100%
|
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;
|
box-shadow: 4px 4px 12px 0px rgba(255, 255, 255, 0.12) inset;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -265,3 +268,4 @@
|
|||||||
.sfp-sight-frame-media-stack.three-d-view {
|
.sfp-sight-frame-media-stack.three-d-view {
|
||||||
background-color: #111 !important;
|
background-color: #111 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,12 +87,14 @@ export const SightsTable = observer(() => {
|
|||||||
<TableCell align="center" className="py-3">
|
<TableCell align="center" className="py-3">
|
||||||
<div className="flex justify-center items-center gap-3">
|
<div className="flex justify-center items-center gap-3">
|
||||||
<button
|
<button
|
||||||
|
title="Редактировать"
|
||||||
className="rounded-md px-3 py-1.5 transition-transform transform hover:scale-105"
|
className="rounded-md px-3 py-1.5 transition-transform transform hover:scale-105"
|
||||||
onClick={() => navigate(`/sight/${row?.id}`)}
|
onClick={() => navigate(`/sight/${row?.id}`)}
|
||||||
>
|
>
|
||||||
<Pencil size={18} className="text-blue-500" />
|
<Pencil size={18} className="text-blue-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
title="Удалить"
|
||||||
className="rounded-md px-3 py-1.5 transition-transform transform hover:scale-105"
|
className="rounded-md px-3 py-1.5 transition-transform transform hover:scale-105"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ export * from "./SaveWithoutCityAgree";
|
|||||||
export * from "./CitySelector";
|
export * from "./CitySelector";
|
||||||
export * from "./modals";
|
export * from "./modals";
|
||||||
export * from "./TestingModeBanner";
|
export * from "./TestingModeBanner";
|
||||||
|
export * from "./PermissionsTable";
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
474
yarn.lock
474
yarn.lock
@@ -16,7 +16,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz"
|
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz"
|
||||||
integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==
|
integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==
|
||||||
|
|
||||||
"@babel/core@^7.21.3", "@babel/core@^7.28.0":
|
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.21.3", "@babel/core@^7.28.0":
|
||||||
version "7.28.5"
|
version "7.28.5"
|
||||||
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz"
|
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz"
|
||||||
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
|
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
|
||||||
@@ -170,28 +170,6 @@
|
|||||||
resolved "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz"
|
resolved "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz"
|
||||||
integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==
|
integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==
|
||||||
|
|
||||||
"@emnapi/core@^1.5.0":
|
|
||||||
version "1.10.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467"
|
|
||||||
integrity sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==
|
|
||||||
dependencies:
|
|
||||||
"@emnapi/wasi-threads" "1.2.1"
|
|
||||||
tslib "^2.4.0"
|
|
||||||
|
|
||||||
"@emnapi/runtime@^1.5.0":
|
|
||||||
version "1.10.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c"
|
|
||||||
integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==
|
|
||||||
dependencies:
|
|
||||||
tslib "^2.4.0"
|
|
||||||
|
|
||||||
"@emnapi/wasi-threads@1.2.1", "@emnapi/wasi-threads@^1.1.0":
|
|
||||||
version "1.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548"
|
|
||||||
integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==
|
|
||||||
dependencies:
|
|
||||||
tslib "^2.4.0"
|
|
||||||
|
|
||||||
"@emotion/babel-plugin@^11.13.5":
|
"@emotion/babel-plugin@^11.13.5":
|
||||||
version "11.13.5"
|
version "11.13.5"
|
||||||
resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz"
|
resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz"
|
||||||
@@ -237,7 +215,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz"
|
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz"
|
||||||
integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
|
integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==
|
||||||
|
|
||||||
"@emotion/react@^11.14.0":
|
"@emotion/react@^11.0.0-rc.0", "@emotion/react@^11.14.0", "@emotion/react@^11.4.1", "@emotion/react@^11.5.0", "@emotion/react@^11.9.0":
|
||||||
version "11.14.0"
|
version "11.14.0"
|
||||||
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz"
|
resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz"
|
||||||
integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==
|
integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==
|
||||||
@@ -267,7 +245,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz"
|
resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz"
|
||||||
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
|
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
|
||||||
|
|
||||||
"@emotion/styled@^11.14.0":
|
"@emotion/styled@^11.14.0", "@emotion/styled@^11.3.0", "@emotion/styled@^11.8.1":
|
||||||
version "11.14.1"
|
version "11.14.1"
|
||||||
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz"
|
resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz"
|
||||||
integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==
|
integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==
|
||||||
@@ -299,136 +277,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz"
|
resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz"
|
||||||
integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
|
integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
|
||||||
|
|
||||||
"@esbuild/aix-ppc64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49"
|
|
||||||
integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==
|
|
||||||
|
|
||||||
"@esbuild/android-arm64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03"
|
|
||||||
integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==
|
|
||||||
|
|
||||||
"@esbuild/android-arm@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae"
|
|
||||||
integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==
|
|
||||||
|
|
||||||
"@esbuild/android-x64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6"
|
|
||||||
integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==
|
|
||||||
|
|
||||||
"@esbuild/darwin-arm64@0.25.11":
|
"@esbuild/darwin-arm64@0.25.11":
|
||||||
version "0.25.11"
|
version "0.25.11"
|
||||||
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz"
|
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz"
|
||||||
integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==
|
integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==
|
||||||
|
|
||||||
"@esbuild/darwin-x64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe"
|
|
||||||
integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==
|
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a"
|
|
||||||
integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==
|
|
||||||
|
|
||||||
"@esbuild/freebsd-x64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb"
|
|
||||||
integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==
|
|
||||||
|
|
||||||
"@esbuild/linux-arm64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5"
|
|
||||||
integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==
|
|
||||||
|
|
||||||
"@esbuild/linux-arm@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f"
|
|
||||||
integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==
|
|
||||||
|
|
||||||
"@esbuild/linux-ia32@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b"
|
|
||||||
integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==
|
|
||||||
|
|
||||||
"@esbuild/linux-loong64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb"
|
|
||||||
integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==
|
|
||||||
|
|
||||||
"@esbuild/linux-mips64el@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5"
|
|
||||||
integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==
|
|
||||||
|
|
||||||
"@esbuild/linux-ppc64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74"
|
|
||||||
integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==
|
|
||||||
|
|
||||||
"@esbuild/linux-riscv64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273"
|
|
||||||
integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==
|
|
||||||
|
|
||||||
"@esbuild/linux-s390x@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263"
|
|
||||||
integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==
|
|
||||||
|
|
||||||
"@esbuild/linux-x64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910"
|
|
||||||
integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==
|
|
||||||
|
|
||||||
"@esbuild/netbsd-arm64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077"
|
|
||||||
integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==
|
|
||||||
|
|
||||||
"@esbuild/netbsd-x64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034"
|
|
||||||
integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==
|
|
||||||
|
|
||||||
"@esbuild/openbsd-arm64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad"
|
|
||||||
integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==
|
|
||||||
|
|
||||||
"@esbuild/openbsd-x64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2"
|
|
||||||
integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==
|
|
||||||
|
|
||||||
"@esbuild/openharmony-arm64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1"
|
|
||||||
integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==
|
|
||||||
|
|
||||||
"@esbuild/sunos-x64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244"
|
|
||||||
integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==
|
|
||||||
|
|
||||||
"@esbuild/win32-arm64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935"
|
|
||||||
integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==
|
|
||||||
|
|
||||||
"@esbuild/win32-ia32@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343"
|
|
||||||
integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==
|
|
||||||
|
|
||||||
"@esbuild/win32-x64@0.25.11":
|
|
||||||
version "0.25.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz#5ecda6f3fe138b7e456f4e429edde33c823f392f"
|
|
||||||
integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==
|
|
||||||
|
|
||||||
"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0":
|
"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0":
|
||||||
version "4.9.0"
|
version "4.9.0"
|
||||||
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz"
|
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz"
|
||||||
@@ -479,7 +332,7 @@
|
|||||||
minimatch "^3.1.2"
|
minimatch "^3.1.2"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
"@eslint/js@9.38.0", "@eslint/js@^9.25.0":
|
"@eslint/js@^9.25.0", "@eslint/js@9.38.0":
|
||||||
version "9.38.0"
|
version "9.38.0"
|
||||||
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz"
|
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz"
|
||||||
integrity sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==
|
integrity sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==
|
||||||
@@ -589,7 +442,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.28.4"
|
"@babel/runtime" "^7.28.4"
|
||||||
|
|
||||||
"@mui/material@^7.1.0":
|
"@mui/material@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/material@^7.1.0", "@mui/material@^7.3.4":
|
||||||
version "7.3.4"
|
version "7.3.4"
|
||||||
resolved "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz"
|
resolved "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz"
|
||||||
integrity sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==
|
integrity sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==
|
||||||
@@ -628,7 +481,7 @@
|
|||||||
csstype "^3.1.3"
|
csstype "^3.1.3"
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
"@mui/system@^7.3.3":
|
"@mui/system@^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system@^7.3.3":
|
||||||
version "7.3.3"
|
version "7.3.3"
|
||||||
resolved "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz"
|
resolved "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz"
|
||||||
integrity sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==
|
integrity sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==
|
||||||
@@ -693,13 +546,6 @@
|
|||||||
"@mui/utils" "^7.3.3"
|
"@mui/utils" "^7.3.3"
|
||||||
"@mui/x-internals" "8.14.0"
|
"@mui/x-internals" "8.14.0"
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime@^1.0.7":
|
|
||||||
version "1.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz#a46bbfedc29751b7170c5d23bc1d8ee8c7e3c1e1"
|
|
||||||
integrity sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==
|
|
||||||
dependencies:
|
|
||||||
"@tybys/wasm-util" "^0.10.1"
|
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||||
@@ -708,7 +554,7 @@
|
|||||||
"@nodelib/fs.stat" "2.0.5"
|
"@nodelib/fs.stat" "2.0.5"
|
||||||
run-parallel "^1.1.9"
|
run-parallel "^1.1.9"
|
||||||
|
|
||||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||||
@@ -726,7 +572,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz"
|
resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz"
|
||||||
integrity sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==
|
integrity sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==
|
||||||
|
|
||||||
"@photo-sphere-viewer/core@^5.13.2":
|
"@photo-sphere-viewer/core@^5.13.2", "@photo-sphere-viewer/core@>=5.13.1":
|
||||||
version "5.14.0"
|
version "5.14.0"
|
||||||
resolved "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.14.0.tgz"
|
resolved "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.14.0.tgz"
|
||||||
integrity sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==
|
integrity sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==
|
||||||
@@ -778,7 +624,7 @@
|
|||||||
utility-types "^3.11.0"
|
utility-types "^3.11.0"
|
||||||
zustand "^5.0.1"
|
zustand "^5.0.1"
|
||||||
|
|
||||||
"@react-three/fiber@^9.1.2":
|
"@react-three/fiber@^9.0.0", "@react-three/fiber@^9.1.2":
|
||||||
version "9.4.0"
|
version "9.4.0"
|
||||||
resolved "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz"
|
resolved "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz"
|
||||||
integrity sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==
|
integrity sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==
|
||||||
@@ -810,116 +656,11 @@
|
|||||||
estree-walker "^2.0.2"
|
estree-walker "^2.0.2"
|
||||||
picomatch "^4.0.2"
|
picomatch "^4.0.2"
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz#0f44a2f8668ed87b040b6fe659358ac9239da4db"
|
|
||||||
integrity sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz#25b9a01deef6518a948431564c987bcb205274f5"
|
|
||||||
integrity sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64@4.52.5":
|
"@rollup/rollup-darwin-arm64@4.52.5":
|
||||||
version "4.52.5"
|
version "4.52.5"
|
||||||
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz"
|
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz"
|
||||||
integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==
|
integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz#8e526417cd6f54daf1d0c04cf361160216581956"
|
|
||||||
integrity sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz#0e7027054493f3409b1f219a3eac5efd128ef899"
|
|
||||||
integrity sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz#72b204a920139e9ec3d331bd9cfd9a0c248ccb10"
|
|
||||||
integrity sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz#ab1b522ebe5b7e06c99504cc38f6cd8b808ba41c"
|
|
||||||
integrity sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz#f8cc30b638f1ee7e3d18eac24af47ea29d9beb00"
|
|
||||||
integrity sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz#7af37a9e85f25db59dc8214172907b7e146c12cc"
|
|
||||||
integrity sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz#a623eb0d3617c03b7a73716eb85c6e37b776f7e0"
|
|
||||||
integrity sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz#76ea038b549c5c6c5f0d062942627c4066642ee2"
|
|
||||||
integrity sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz#d9a4c3f0a3492bc78f6fdfe8131ac61c7359ccd5"
|
|
||||||
integrity sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz#87ab033eebd1a9a1dd7b60509f6333ec1f82d994"
|
|
||||||
integrity sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz#bda3eb67e1c993c1ba12bc9c2f694e7703958d9f"
|
|
||||||
integrity sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz#f7bc10fbe096ab44694233dc42a2291ed5453d4b"
|
|
||||||
integrity sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz#a151cb1234cc9b2cf5e8cfc02aa91436b8f9e278"
|
|
||||||
integrity sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz#7859e196501cc3b3062d45d2776cfb4d2f3a9350"
|
|
||||||
integrity sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==
|
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz#85d0df7233734df31e547c1e647d2a5300b3bf30"
|
|
||||||
integrity sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz#e62357d00458db17277b88adbf690bb855cac937"
|
|
||||||
integrity sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz#fc7cd40f44834a703c1f1c3fe8bcc27ce476cd50"
|
|
||||||
integrity sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz#1a22acfc93c64a64a48c42672e857ee51774d0d3"
|
|
||||||
integrity sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc@4.52.5":
|
|
||||||
version "4.52.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz#1657f56326bbe0ac80eedc9f9c18fc1ddd24e107"
|
|
||||||
integrity sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==
|
|
||||||
|
|
||||||
"@svgr/babel-plugin-add-jsx-attribute@8.0.0":
|
"@svgr/babel-plugin-add-jsx-attribute@8.0.0":
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz"
|
resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz"
|
||||||
@@ -974,7 +715,7 @@
|
|||||||
"@svgr/babel-plugin-transform-react-native-svg" "8.1.0"
|
"@svgr/babel-plugin-transform-react-native-svg" "8.1.0"
|
||||||
"@svgr/babel-plugin-transform-svg-component" "8.0.0"
|
"@svgr/babel-plugin-transform-svg-component" "8.0.0"
|
||||||
|
|
||||||
"@svgr/core@^8.1.0":
|
"@svgr/core@*", "@svgr/core@^8.1.0":
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz"
|
resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz"
|
||||||
integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==
|
integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==
|
||||||
@@ -1016,73 +757,11 @@
|
|||||||
source-map-js "^1.2.1"
|
source-map-js "^1.2.1"
|
||||||
tailwindcss "4.1.16"
|
tailwindcss "4.1.16"
|
||||||
|
|
||||||
"@tailwindcss/oxide-android-arm64@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz#9bd16c0a08db20d7c93907a9bd1564e0255307eb"
|
|
||||||
integrity sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64@4.1.16":
|
"@tailwindcss/oxide-darwin-arm64@4.1.16":
|
||||||
version "4.1.16"
|
version "4.1.16"
|
||||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz"
|
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz"
|
||||||
integrity sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==
|
integrity sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-x64@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz#6193bafbb1a885795702f12bbef9cc5eb4cc550b"
|
|
||||||
integrity sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-freebsd-x64@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz#0e2b064d71ba87a9001ac963be2752a8ddb64349"
|
|
||||||
integrity sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz#8e80c959eeda81a08ed955e23eb6d228287b9672"
|
|
||||||
integrity sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz#d5f54910920fc5808122515f5208c5ecc1a40545"
|
|
||||||
integrity sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-musl@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz#67cdb932230ac47bf3bf5415ccc92417b27020ee"
|
|
||||||
integrity sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz#80ae0cfd8ebc970f239060ecdfdd07f6f6b14dce"
|
|
||||||
integrity sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-musl@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz#524e5b87e8e79a712de3d9bbb94d2fc2fa44391c"
|
|
||||||
integrity sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz#dc31d6bc1f6c1e8119a335ae3f28deb4d7c560f2"
|
|
||||||
integrity sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==
|
|
||||||
dependencies:
|
|
||||||
"@emnapi/core" "^1.5.0"
|
|
||||||
"@emnapi/runtime" "^1.5.0"
|
|
||||||
"@emnapi/wasi-threads" "^1.1.0"
|
|
||||||
"@napi-rs/wasm-runtime" "^1.0.7"
|
|
||||||
"@tybys/wasm-util" "^0.10.1"
|
|
||||||
tslib "^2.4.0"
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz#f1f810cdb49dae8071d5edf0db5cc0da2ec6a7e8"
|
|
||||||
integrity sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc@4.1.16":
|
|
||||||
version "4.1.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz#76dcda613578f06569c0a6015f39f12746a24dce"
|
|
||||||
integrity sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==
|
|
||||||
|
|
||||||
"@tailwindcss/oxide@4.1.16":
|
"@tailwindcss/oxide@4.1.16":
|
||||||
version "4.1.16"
|
version "4.1.16"
|
||||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz"
|
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz"
|
||||||
@@ -1122,13 +801,6 @@
|
|||||||
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz"
|
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz"
|
||||||
integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==
|
integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==
|
||||||
|
|
||||||
"@tybys/wasm-util@^0.10.1":
|
|
||||||
version "0.10.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
|
|
||||||
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
|
|
||||||
dependencies:
|
|
||||||
tslib "^2.4.0"
|
|
||||||
|
|
||||||
"@types/babel__core@^7.20.5":
|
"@types/babel__core@^7.20.5":
|
||||||
version "7.20.5"
|
version "7.20.5"
|
||||||
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
||||||
@@ -1198,7 +870,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree" "*"
|
"@types/estree" "*"
|
||||||
|
|
||||||
"@types/estree@*", "@types/estree@1.0.8", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
|
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@1.0.8":
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
|
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
|
||||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||||
@@ -1237,7 +909,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz"
|
||||||
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
||||||
|
|
||||||
"@types/node@^22.15.24":
|
"@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.15.24":
|
||||||
version "22.18.13"
|
version "22.18.13"
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz"
|
||||||
integrity sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==
|
integrity sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==
|
||||||
@@ -1284,7 +956,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
|
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
|
||||||
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
||||||
|
|
||||||
"@types/react@^19.1.2":
|
"@types/react@*", "@types/react@^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react@^18.2.25 || ^19", "@types/react@^19.1.2", "@types/react@^19.2.0", "@types/react@>=16.8", "@types/react@>=18", "@types/react@>=18.0.0":
|
||||||
version "19.2.2"
|
version "19.2.2"
|
||||||
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz"
|
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz"
|
||||||
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
|
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
|
||||||
@@ -1303,7 +975,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree" "*"
|
"@types/estree" "*"
|
||||||
|
|
||||||
"@types/three@*":
|
"@types/three@*", "@types/three@>=0.134.0":
|
||||||
version "0.180.0"
|
version "0.180.0"
|
||||||
resolved "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz"
|
resolved "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz"
|
||||||
integrity sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==
|
integrity sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==
|
||||||
@@ -1351,7 +1023,7 @@
|
|||||||
natural-compare "^1.4.0"
|
natural-compare "^1.4.0"
|
||||||
ts-api-utils "^2.1.0"
|
ts-api-utils "^2.1.0"
|
||||||
|
|
||||||
"@typescript-eslint/parser@8.46.2":
|
"@typescript-eslint/parser@^8.46.2", "@typescript-eslint/parser@8.46.2":
|
||||||
version "8.46.2"
|
version "8.46.2"
|
||||||
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz"
|
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz"
|
||||||
integrity sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==
|
integrity sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==
|
||||||
@@ -1379,7 +1051,7 @@
|
|||||||
"@typescript-eslint/types" "8.46.2"
|
"@typescript-eslint/types" "8.46.2"
|
||||||
"@typescript-eslint/visitor-keys" "8.46.2"
|
"@typescript-eslint/visitor-keys" "8.46.2"
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils@8.46.2", "@typescript-eslint/tsconfig-utils@^8.46.2":
|
"@typescript-eslint/tsconfig-utils@^8.46.2", "@typescript-eslint/tsconfig-utils@8.46.2":
|
||||||
version "8.46.2"
|
version "8.46.2"
|
||||||
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz"
|
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz"
|
||||||
integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==
|
integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==
|
||||||
@@ -1395,7 +1067,7 @@
|
|||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
ts-api-utils "^2.1.0"
|
ts-api-utils "^2.1.0"
|
||||||
|
|
||||||
"@typescript-eslint/types@8.46.2", "@typescript-eslint/types@^8.46.2":
|
"@typescript-eslint/types@^8.46.2", "@typescript-eslint/types@8.46.2":
|
||||||
version "8.46.2"
|
version "8.46.2"
|
||||||
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz"
|
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz"
|
||||||
integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==
|
integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==
|
||||||
@@ -1478,7 +1150,7 @@ acorn-jsx@^5.3.2:
|
|||||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||||
|
|
||||||
acorn@^8.15.0:
|
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0:
|
||||||
version "8.15.0"
|
version "8.15.0"
|
||||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
||||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||||
@@ -1582,7 +1254,7 @@ braces@^3.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.1.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
browserslist@^4.24.0:
|
browserslist@^4.24.0, "browserslist@>= 4.21.0":
|
||||||
version "4.27.0"
|
version "4.27.0"
|
||||||
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz"
|
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz"
|
||||||
integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==
|
integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==
|
||||||
@@ -1879,7 +1551,7 @@ earcut@^3.0.0, earcut@^3.0.2:
|
|||||||
resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz"
|
resolved "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz"
|
||||||
integrity sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==
|
integrity sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==
|
||||||
|
|
||||||
easymde@^2.20.0:
|
easymde@^2.20.0, "easymde@>= 2.0.0 < 3.0.0":
|
||||||
version "2.20.0"
|
version "2.20.0"
|
||||||
resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz"
|
resolved "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz"
|
||||||
integrity sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==
|
integrity sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==
|
||||||
@@ -2022,7 +1694,7 @@ eslint-visitor-keys@^4.2.1:
|
|||||||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
|
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
|
||||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||||
|
|
||||||
eslint@^9.25.0:
|
"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.25.0, eslint@>=8.40:
|
||||||
version "9.38.0"
|
version "9.38.0"
|
||||||
resolved "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz"
|
resolved "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz"
|
||||||
integrity sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==
|
integrity sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==
|
||||||
@@ -2148,7 +1820,12 @@ fastq@^1.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
reusify "^1.0.4"
|
reusify "^1.0.4"
|
||||||
|
|
||||||
fdir@^6.4.4, fdir@^6.5.0:
|
fdir@^6.4.4:
|
||||||
|
version "6.5.0"
|
||||||
|
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
||||||
|
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||||
|
|
||||||
|
fdir@^6.5.0:
|
||||||
version "6.5.0"
|
version "6.5.0"
|
||||||
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
||||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||||
@@ -2612,7 +2289,7 @@ its-fine@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react-reconciler" "^0.28.9"
|
"@types/react-reconciler" "^0.28.9"
|
||||||
|
|
||||||
jiti@^2.6.1:
|
jiti@*, jiti@^2.6.1, jiti@>=1.21.0:
|
||||||
version "2.6.1"
|
version "2.6.1"
|
||||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz"
|
resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz"
|
||||||
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
||||||
@@ -2691,62 +2368,12 @@ lie@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
immediate "~3.0.5"
|
immediate "~3.0.5"
|
||||||
|
|
||||||
lightningcss-android-arm64@1.30.2:
|
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz#6966b7024d39c94994008b548b71ab360eb3a307"
|
|
||||||
integrity sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==
|
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.2:
|
lightningcss-darwin-arm64@1.30.2:
|
||||||
version "1.30.2"
|
version "1.30.2"
|
||||||
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz"
|
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz"
|
||||||
integrity sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==
|
integrity sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==
|
||||||
|
|
||||||
lightningcss-darwin-x64@1.30.2:
|
lightningcss@^1.21.0, lightningcss@1.30.2:
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz#5ce87e9cd7c4f2dcc1b713f5e8ee185c88d9b7cd"
|
|
||||||
integrity sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==
|
|
||||||
|
|
||||||
lightningcss-freebsd-x64@1.30.2:
|
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz#6ae1d5e773c97961df5cff57b851807ef33692a5"
|
|
||||||
integrity sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==
|
|
||||||
|
|
||||||
lightningcss-linux-arm-gnueabihf@1.30.2:
|
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz#62c489610c0424151a6121fa99d77731536cdaeb"
|
|
||||||
integrity sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-gnu@1.30.2:
|
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz#2a3661b56fe95a0cafae90be026fe0590d089298"
|
|
||||||
integrity sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.30.2:
|
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz#d7ddd6b26959245e026bc1ad9eb6aa983aa90e6b"
|
|
||||||
integrity sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==
|
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.30.2:
|
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz#5a89814c8e63213a5965c3d166dff83c36152b1a"
|
|
||||||
integrity sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==
|
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.30.2:
|
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz#808c2e91ce0bf5d0af0e867c6152e5378c049728"
|
|
||||||
integrity sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==
|
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.30.2:
|
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz#ab4a8a8a2e6a82a4531e8bbb6bf0ff161ee6625a"
|
|
||||||
integrity sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==
|
|
||||||
|
|
||||||
lightningcss-win32-x64-msvc@1.30.2:
|
|
||||||
version "1.30.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz#f01f382c8e0a27e1c018b0bee316d210eac43b6e"
|
|
||||||
integrity sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==
|
|
||||||
|
|
||||||
lightningcss@1.30.2:
|
|
||||||
version "1.30.2"
|
version "1.30.2"
|
||||||
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz"
|
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz"
|
||||||
integrity sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==
|
integrity sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==
|
||||||
@@ -3195,7 +2822,7 @@ mobx-react-lite@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
use-sync-external-store "^1.4.0"
|
use-sync-external-store "^1.4.0"
|
||||||
|
|
||||||
mobx@^6.13.7:
|
mobx@^6.13.7, mobx@^6.9.0:
|
||||||
version "6.15.0"
|
version "6.15.0"
|
||||||
resolved "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz"
|
resolved "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz"
|
||||||
integrity sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==
|
integrity sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==
|
||||||
@@ -3270,7 +2897,7 @@ overlayscrollbars-react@^0.5.6:
|
|||||||
resolved "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz"
|
resolved "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz"
|
||||||
integrity sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==
|
integrity sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==
|
||||||
|
|
||||||
overlayscrollbars@^2.15.1:
|
overlayscrollbars@^2.0.0, overlayscrollbars@^2.15.1:
|
||||||
version "2.15.1"
|
version "2.15.1"
|
||||||
resolved "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.15.1.tgz"
|
resolved "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.15.1.tgz"
|
||||||
integrity sha512-glX26JwjL+Tkzv0JNOWdW4VozP5dGXO+Wx8+TPrdTEJTSYT/8eJS8yXM+fewjU0nFq/JeCa+X+BqABNjC4YZSA==
|
integrity sha512-glX26JwjL+Tkzv0JNOWdW4VozP5dGXO+Wx8+TPrdTEJTSYT/8eJS8yXM+fewjU0nFq/JeCa+X+BqABNjC4YZSA==
|
||||||
@@ -3386,12 +3013,12 @@ picomatch@^2.3.1:
|
|||||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||||
|
|
||||||
picomatch@^4.0.2, picomatch@^4.0.3:
|
"picomatch@^3 || ^4", picomatch@^4.0.2, picomatch@^4.0.3:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
||||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||||
|
|
||||||
pixi.js@^8.10.1:
|
pixi.js@^8.10.1, pixi.js@^8.2.6:
|
||||||
version "8.14.0"
|
version "8.14.0"
|
||||||
resolved "https://registry.npmjs.org/pixi.js/-/pixi.js-8.14.0.tgz"
|
resolved "https://registry.npmjs.org/pixi.js/-/pixi.js-8.14.0.tgz"
|
||||||
integrity sha512-ituDiEBb1Oqx56RYwTtC6MjPUhPfF/i15fpUv5oEqmzC/ce3SaSumulJcOjKG7+y0J0Ekl9Rl4XTxaUw+MVFZw==
|
integrity sha512-ituDiEBb1Oqx56RYwTtC6MjPUhPfF/i15fpUv5oEqmzC/ce3SaSumulJcOjKG7+y0J0Ekl9Rl4XTxaUw+MVFZw==
|
||||||
@@ -3448,7 +3075,7 @@ promise-worker-transferable@^1.0.4:
|
|||||||
is-promise "^2.1.0"
|
is-promise "^2.1.0"
|
||||||
lie "^3.0.2"
|
lie "^3.0.2"
|
||||||
|
|
||||||
prop-types@^15.6.2, prop-types@^15.8.1:
|
prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.8.1:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@@ -3509,14 +3136,19 @@ rbush@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
quickselect "^3.0.0"
|
quickselect "^3.0.0"
|
||||||
|
|
||||||
react-dom@^19.1.0:
|
"react-dom@^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18 || ^19", "react-dom@^18.0.0 || ^19.0.0", react-dom@^19, react-dom@^19.0.0, react-dom@^19.1.0, react-dom@>=16.0.0, react-dom@>=16.13, react-dom@>=16.6.0, react-dom@>=16.8.2, react-dom@>=18:
|
||||||
version "19.2.0"
|
version "19.2.0"
|
||||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz"
|
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz"
|
||||||
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
|
integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
scheduler "^0.27.0"
|
scheduler "^0.27.0"
|
||||||
|
|
||||||
react-is@^16.13.1, react-is@^16.7.0:
|
react-is@^16.13.1:
|
||||||
|
version "16.13.1"
|
||||||
|
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||||
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
|
react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
@@ -3550,7 +3182,7 @@ react-photo-sphere-viewer@^6.2.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
eventemitter3 "^5.0.1"
|
eventemitter3 "^5.0.1"
|
||||||
|
|
||||||
react-reconciler@0.31.0, react-reconciler@^0.31.0:
|
react-reconciler@^0.31.0, react-reconciler@0.31.0:
|
||||||
version "0.31.0"
|
version "0.31.0"
|
||||||
resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz"
|
resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz"
|
||||||
integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==
|
integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==
|
||||||
@@ -3577,7 +3209,7 @@ react-router-dom@^7.6.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react-router "7.9.4"
|
react-router "7.9.4"
|
||||||
|
|
||||||
react-router@7.9.4, react-router@^7.9.4:
|
react-router@^7.9.4, react-router@7.9.4:
|
||||||
version "7.9.4"
|
version "7.9.4"
|
||||||
resolved "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz"
|
resolved "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz"
|
||||||
integrity sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==
|
integrity sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==
|
||||||
@@ -3614,12 +3246,12 @@ react-use-measure@^2.1.7:
|
|||||||
resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz"
|
resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz"
|
||||||
integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
|
integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
|
||||||
|
|
||||||
react@^19.1.0:
|
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18 || ^19", "react@^18.0 || ^19", "react@^18.0.0 || ^19.0.0", react@^19, react@^19.0.0, react@^19.1.0, react@^19.2.0, "react@>= 16.8.0", react@>=16.0.0, react@>=16.13, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=16.8.2, react@>=17.0, react@>=18, react@>=18.0.0, react@>=19.0.0:
|
||||||
version "19.2.0"
|
version "19.2.0"
|
||||||
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz"
|
resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz"
|
||||||
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
|
integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
|
||||||
|
|
||||||
redux@^5.0.1:
|
redux@^5.0.0, redux@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz"
|
resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz"
|
||||||
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
|
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
|
||||||
@@ -3705,7 +3337,7 @@ rollup-plugin-visualizer@^6.0.5:
|
|||||||
source-map "^0.7.4"
|
source-map "^0.7.4"
|
||||||
yargs "^17.5.1"
|
yargs "^17.5.1"
|
||||||
|
|
||||||
rollup@^4.34.9:
|
rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^4.34.9, "rollup@2.x || 3.x || 4.x":
|
||||||
version "4.52.5"
|
version "4.52.5"
|
||||||
resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz"
|
resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz"
|
||||||
integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==
|
integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==
|
||||||
@@ -3891,7 +3523,7 @@ svg-parser@^2.0.4:
|
|||||||
resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz"
|
resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz"
|
||||||
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
||||||
|
|
||||||
tailwindcss@4.1.16, tailwindcss@^4.1.8:
|
tailwindcss@^4.1.8, "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1", tailwindcss@4.1.16:
|
||||||
version "4.1.16"
|
version "4.1.16"
|
||||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz"
|
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz"
|
||||||
integrity sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==
|
integrity sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==
|
||||||
@@ -3923,7 +3555,7 @@ three@^0.170.0:
|
|||||||
resolved "https://registry.npmjs.org/three/-/three-0.170.0.tgz"
|
resolved "https://registry.npmjs.org/three/-/three-0.170.0.tgz"
|
||||||
integrity sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==
|
integrity sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==
|
||||||
|
|
||||||
three@^0.177.0:
|
three@^0.177.0, "three@>= 0.159.0", three@>=0.125.0, three@>=0.126.1, three@>=0.128.0, three@>=0.134.0, three@>=0.137, three@>=0.156, three@>=0.159:
|
||||||
version "0.177.0"
|
version "0.177.0"
|
||||||
resolved "https://registry.npmjs.org/three/-/three-0.177.0.tgz"
|
resolved "https://registry.npmjs.org/three/-/three-0.177.0.tgz"
|
||||||
integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==
|
integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==
|
||||||
@@ -3993,9 +3625,9 @@ ts-api-utils@^2.1.0:
|
|||||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
|
||||||
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
||||||
|
|
||||||
tslib@^2.0.3, tslib@^2.4.0:
|
tslib@^2.0.3:
|
||||||
version "2.8.1"
|
version "2.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||||
|
|
||||||
tunnel-rat@^0.1.2:
|
tunnel-rat@^0.1.2:
|
||||||
@@ -4022,7 +3654,7 @@ typescript-eslint@^8.30.1:
|
|||||||
"@typescript-eslint/typescript-estree" "8.46.2"
|
"@typescript-eslint/typescript-estree" "8.46.2"
|
||||||
"@typescript-eslint/utils" "8.46.2"
|
"@typescript-eslint/utils" "8.46.2"
|
||||||
|
|
||||||
typescript@~5.8.3:
|
typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@>=4.9.5, typescript@~5.8.3:
|
||||||
version "5.8.3"
|
version "5.8.3"
|
||||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
|
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
|
||||||
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
||||||
@@ -4103,7 +3735,7 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0:
|
use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0, use-sync-external-store@^1.6.0, use-sync-external-store@>=1.2.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz"
|
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz"
|
||||||
integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
|
integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
|
||||||
@@ -4163,7 +3795,7 @@ vite-plugin-svgr@^4.5.0:
|
|||||||
"@svgr/core" "^8.1.0"
|
"@svgr/core" "^8.1.0"
|
||||||
"@svgr/plugin-jsx" "^8.1.0"
|
"@svgr/plugin-jsx" "^8.1.0"
|
||||||
|
|
||||||
vite@^6.3.5:
|
"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^6.3.5, vite@>=2.6.0:
|
||||||
version "6.4.1"
|
version "6.4.1"
|
||||||
resolved "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz"
|
resolved "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz"
|
||||||
integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
|
integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
|
||||||
|
|||||||
Reference in New Issue
Block a user