diff --git a/src/pages/Carrier/CarrierCreatePage/index.tsx b/src/pages/Carrier/CarrierCreatePage/index.tsx index a61daae..4fc1435 100644 --- a/src/pages/Carrier/CarrierCreatePage/index.tsx +++ b/src/pages/Carrier/CarrierCreatePage/index.tsx @@ -8,8 +8,8 @@ import { InputLabel, } from "@mui/material"; import { observer } from "mobx-react-lite"; -import { ArrowLeft, Save } from "lucide-react"; -import { Loader2 } from "lucide-react"; +import { ArrowLeft, Loader2, Save } from "lucide-react"; + import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { @@ -17,15 +17,14 @@ import { cityStore, mediaStore, languageStore, + isMediaIdEmpty, useSelectedCity, -} from "@shared"; -import { useState, useEffect } from "react"; -import { ImageUploadCard, LanguageSwitcher } from "@widgets"; -import { SelectMediaDialog, UploadMediaDialog, PreviewMediaDialog, } from "@shared"; +import { useState, useEffect } from "react"; +import { ImageUploadCard, LanguageSwitcher } from "@widgets"; export const CarrierCreatePage = observer(() => { const navigate = useNavigate(); @@ -56,7 +55,7 @@ export const CarrierCreatePage = observer(() => { selectedCityId, createCarrierData[language].slogan, selectedMediaId || "", - language + language, ); } }, [selectedCityId, createCarrierData.city_id]); @@ -88,13 +87,17 @@ export const CarrierCreatePage = observer(() => { createCarrierData.city_id, createCarrierData[language].slogan, media.id, - language + language, ); }; - const selectedMedia = selectedMediaId - ? mediaStore.media.find((m) => m.id === selectedMediaId) - : null; + const selectedMedia = + selectedMediaId && !isMediaIdEmpty(selectedMediaId) + ? mediaStore.media.find((m) => m.id === selectedMediaId) + : null; + const effectiveLogoUrl = isMediaIdEmpty(selectedMediaId) + ? null + : selectedMedia?.id ?? selectedMediaId ?? null; return ( @@ -127,7 +130,7 @@ export const CarrierCreatePage = observer(() => { e.target.value as number, createCarrierData[language].slogan, selectedMediaId || "", - language + language, ) } > @@ -151,7 +154,7 @@ export const CarrierCreatePage = observer(() => { createCarrierData.city_id, createCarrierData[language].slogan, selectedMediaId || "", - language + language, ) } /> @@ -168,7 +171,7 @@ export const CarrierCreatePage = observer(() => { createCarrierData.city_id, createCarrierData[language].slogan, selectedMediaId || "", - language + language, ) } /> @@ -184,7 +187,7 @@ export const CarrierCreatePage = observer(() => { createCarrierData.city_id, e.target.value, selectedMediaId || "", - language + language, ) } /> @@ -193,10 +196,10 @@ export const CarrierCreatePage = observer(() => { { setIsPreviewMediaOpen(true); - setMediaId(selectedMedia?.id ?? ""); + setMediaId(effectiveLogoUrl ?? ""); }} onDeleteImageClick={() => { setSelectedMediaId(null); @@ -207,7 +210,7 @@ export const CarrierCreatePage = observer(() => { createCarrierData.city_id, createCarrierData[language].slogan, "", - language + language, ); }} onSelectFileClick={() => { diff --git a/src/pages/Carrier/CarrierEditPage/index.tsx b/src/pages/Carrier/CarrierEditPage/index.tsx index 0b17050..ebe50f7 100644 --- a/src/pages/Carrier/CarrierEditPage/index.tsx +++ b/src/pages/Carrier/CarrierEditPage/index.tsx @@ -18,6 +18,7 @@ import { cityStore, mediaStore, languageStore, + isMediaIdEmpty, LoadingSpinner, } from "@shared"; import { useState, useEffect } from "react"; @@ -123,9 +124,13 @@ export const CarrierEditPage = observer(() => { ); }; - const selectedMedia = editCarrierData.logo - ? mediaStore.media.find((m) => m.id === editCarrierData.logo) - : null; + const selectedMedia = + editCarrierData.logo && !isMediaIdEmpty(editCarrierData.logo) + ? mediaStore.media.find((m) => m.id === editCarrierData.logo) + : null; + const effectiveLogoUrl = isMediaIdEmpty(editCarrierData.logo) + ? null + : (selectedMedia?.id ?? editCarrierData.logo); if (isLoadingData) { return ( @@ -238,10 +243,10 @@ export const CarrierEditPage = observer(() => { { setIsPreviewMediaOpen(true); - setMediaId(selectedMedia?.id ?? ""); + setMediaId(effectiveLogoUrl ?? ""); }} onDeleteImageClick={() => { setIsDeleteLogoModalOpen(true); diff --git a/src/pages/City/CityCreatePage/index.tsx b/src/pages/City/CityCreatePage/index.tsx index 0f26a89..76f5548 100644 --- a/src/pages/City/CityCreatePage/index.tsx +++ b/src/pages/City/CityCreatePage/index.tsx @@ -12,14 +12,18 @@ import { ArrowLeft, Save } from "lucide-react"; import { Loader2 } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; -import { cityStore, countryStore, languageStore, mediaStore } from "@shared"; -import { useState, useEffect } from "react"; -import { LanguageSwitcher, ImageUploadCard } from "@widgets"; import { + cityStore, + countryStore, + languageStore, + mediaStore, + isMediaIdEmpty, SelectMediaDialog, UploadMediaDialog, PreviewMediaDialog, } from "@shared"; +import { useState, useEffect } from "react"; +import { LanguageSwitcher, ImageUploadCard } from "@widgets"; export const CityCreatePage = observer(() => { const navigate = useNavigate(); @@ -72,9 +76,13 @@ export const CityCreatePage = observer(() => { ); }; - const selectedMedia = createCityData.arms - ? mediaStore.media.find((m) => m.id === createCityData.arms) - : null; + const selectedMedia = + createCityData.arms && !isMediaIdEmpty(createCityData.arms) + ? mediaStore.media.find((m) => m.id === createCityData.arms) + : null; + const effectiveArmsUrl = isMediaIdEmpty(createCityData.arms) + ? null + : (selectedMedia?.id ?? createCityData.arms); return ( @@ -135,10 +143,10 @@ export const CityCreatePage = observer(() => { { setIsPreviewMediaOpen(true); - setMediaId(selectedMedia?.id ?? ""); + setMediaId(effectiveArmsUrl ?? ""); }} onDeleteImageClick={() => { setCreateCityData( diff --git a/src/pages/City/CityEditPage/index.tsx b/src/pages/City/CityEditPage/index.tsx index b5a274e..ecb9f14 100644 --- a/src/pages/City/CityEditPage/index.tsx +++ b/src/pages/City/CityEditPage/index.tsx @@ -9,8 +9,7 @@ import { Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; -import { ArrowLeft, Save } from "lucide-react"; -import { Loader2 } from "lucide-react"; +import { ArrowLeft, Loader2, Save } from "lucide-react"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import { @@ -18,16 +17,15 @@ import { countryStore, languageStore, mediaStore, + isMediaIdEmpty, CashedCities, LoadingSpinner, -} from "@shared"; -import { useEffect, useState } from "react"; -import { LanguageSwitcher, ImageUploadCard } from "@widgets"; -import { SelectMediaDialog, UploadMediaDialog, PreviewMediaDialog, } from "@shared"; +import { useEffect, useState } from "react"; +import { LanguageSwitcher, ImageUploadCard } from "@widgets"; export const CityEditPage = observer(() => { const navigate = useNavigate(); @@ -99,13 +97,17 @@ export const CityEditPage = observer(() => { editCityData[language].name, editCityData.country_code, media.id, - language + language, ); }; - const selectedMedia = editCityData.arms - ? mediaStore.media.find((m) => m.id === editCityData.arms) - : null; + const selectedMedia = + editCityData.arms && !isMediaIdEmpty(editCityData.arms) + ? mediaStore.media.find((m) => m.id === editCityData.arms) + : null; + const effectiveArmsUrl = isMediaIdEmpty(editCityData.arms) + ? null + : selectedMedia?.id ?? editCityData.arms; if (isLoadingData) { return ( @@ -149,7 +151,7 @@ export const CityEditPage = observer(() => { e.target.value, editCityData.country_code, editCityData.arms, - language + language, ) } /> @@ -165,7 +167,7 @@ export const CityEditPage = observer(() => { editCityData[language].name, e.target.value, editCityData.arms, - language + language, ); }} > @@ -181,17 +183,17 @@ export const CityEditPage = observer(() => { { setIsPreviewMediaOpen(true); - setMediaId(selectedMedia?.id ?? ""); + setMediaId(effectiveArmsUrl ?? ""); }} onDeleteImageClick={() => { setEditCityData( editCityData[language].name, editCityData.country_code, "", - language + language, ); setActiveMenuType(null); }} diff --git a/src/pages/MapPage/index.tsx b/src/pages/MapPage/index.tsx index cbe655a..b3857f5 100644 --- a/src/pages/MapPage/index.tsx +++ b/src/pages/MapPage/index.tsx @@ -186,7 +186,7 @@ const saveHiddenRoutes = (hiddenRoutes: Set): void => { try { localStorage.setItem( HIDDEN_ROUTES_KEY, - JSON.stringify(Array.from(hiddenRoutes)) + JSON.stringify(Array.from(hiddenRoutes)), ); } catch (error) { console.warn("Failed to save hidden routes:", error); @@ -221,7 +221,7 @@ class MapStore { try { localStorage.setItem( HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY, - JSON.stringify(!!val) + JSON.stringify(!!val), ); } catch (e) {} } @@ -239,7 +239,7 @@ class MapStore { private sortFeatures( features: T[], - sortType: SortType + sortType: SortType, ): T[] { const sorted = [...features]; switch (sortType) { @@ -324,7 +324,7 @@ class MapStore { return this.sortedStations; } return this.sortedStations.filter( - (station) => station.city_id === selectedCityId + (station) => station.city_id === selectedCityId, ); } @@ -365,7 +365,7 @@ class MapStore { const response = await languageInstance("ru").get("/route"); const routesIds = response.data.map((route: any) => route.id); const routePromises = routesIds.map((id: number) => - languageInstance("ru").get(`/route/${id}`) + languageInstance("ru").get(`/route/${id}`), ); const routeResponses = await Promise.all(routePromises); this.routes = routeResponses.map((res) => ({ @@ -379,7 +379,7 @@ class MapStore { })); this.routes = this.routes.sort((a, b) => - a.route_number.localeCompare(b.route_number) + a.route_number.localeCompare(b.route_number), ); await this.preloadRouteStations(routesIds); @@ -391,14 +391,14 @@ class MapStore { const stationPromises = routesIds.map(async (routeId) => { try { const stationsResponse = await languageInstance("ru").get( - `/route/${routeId}/station` + `/route/${routeId}/station`, ); const stationIds = stationsResponse.data.map((s: any) => s.id); this.routeStationsCache.set(routeId, stationIds); } catch (error) { console.error( `Failed to preload stations for route ${routeId}:`, - error + error, ); } }); @@ -409,7 +409,7 @@ class MapStore { const sightPromises = routesIds.map(async (routeId) => { try { const sightsResponse = await languageInstance("ru").get( - `/route/${routeId}/sight` + `/route/${routeId}/sight`, ); const sightIds = sightsResponse.data.map((s: any) => s.id); this.routeSightsCache.set(routeId, sightIds); @@ -493,7 +493,7 @@ class MapStore { if (selectedCityStore.selectedCityId) { const carriersInCity = carrierStore.carriers.ru.data.filter( - (c: any) => c.city_id === selectedCityStore.selectedCityId + (c: any) => c.city_id === selectedCityStore.selectedCityId, ); if (carriersInCity.length > 0) { @@ -521,7 +521,7 @@ class MapStore { if (!carrier_id && selectedCityStore.selectedCityId) { toast.error( - "В выбранном городе нет доступных перевозчиков, маршрут отображается в общем списке" + "В выбранном городе нет доступных перевозчиков, маршрут отображается в общем списке", ); } createdItem = routeStore.routes.data[routeStore.routes.data.length - 1]; @@ -573,7 +573,7 @@ class MapStore { const centerCoords = getCenter(lineGeom.getExtent()); const [center_longitude, center_latitude] = toLonLat( centerCoords, - "EPSG:3857" + "EPSG:3857", ); data = { route_number: properties.name, @@ -606,7 +606,7 @@ class MapStore { return; } throw new Error( - `Could not find old data for ${featureType} with id ${numericId}` + `Could not find old data for ${featureType} with id ${numericId}`, ); } @@ -626,7 +626,7 @@ class MapStore { const response = await languageInstance("ru").patch( `/${featureType}/${numericId}`, - requestBody + requestBody, ); const updateStore = (store: any[], updatedItem: any) => { @@ -745,7 +745,7 @@ class MapService { private selectInteraction: Select; private hoveredFeatureId: string | number | null; private boundHandlePointerMove: ( - event: MapBrowserEvent + event: MapBrowserEvent, ) => void; private boundHandlePointerLeave: () => void; private boundHandleContextMenu: (event: MouseEvent) => void; @@ -784,7 +784,7 @@ class MapService { onFeaturesChange: (features: Feature[]) => void, onFeatureSelect: (feature: Feature | null) => void, tooltipElement: HTMLElement, - onSelectionChange?: (ids: Set) => void + onSelectionChange?: (ids: Set) => void, ) { this.map = null; this.tooltipElement = tooltipElement; @@ -933,7 +933,7 @@ class MapService { style: (featureLike: FeatureLike) => { const clusterFeature = featureLike as Feature; const featuresInCluster = clusterFeature.get( - "features" + "features", ) as Feature[]; const size = featuresInCluster.length; @@ -991,18 +991,18 @@ class MapService { this.pointSource.on( "addfeature", - this.handleFeatureEvent.bind(this) as any + this.handleFeatureEvent.bind(this) as any, ); this.pointSource.on("removefeature", () => this.updateFeaturesInReact()); this.pointSource.on( "changefeature", - this.handleFeatureChange.bind(this) as any + this.handleFeatureChange.bind(this) as any, ); this.lineSource.on("addfeature", this.handleFeatureEvent.bind(this) as any); this.lineSource.on("removefeature", () => this.updateFeaturesInReact()); this.lineSource.on( "changefeature", - this.handleFeatureChange.bind(this) as any + this.handleFeatureChange.bind(this) as any, ); let renderCompleteHandled = false; @@ -1056,7 +1056,7 @@ class MapService { if (center && zoom !== undefined && this.map) { const [lon, lat] = toLonLat( center, - this.map.getView().getProjection() + this.map.getView().getProjection(), ); saveMapPosition({ center: [lon, lat], zoom }); } @@ -1068,7 +1068,7 @@ class MapService { if (center && zoom !== undefined && this.map) { const [lon, lat] = toLonLat( center, - this.map.getView().getProjection() + this.map.getView().getProjection(), ); saveMapPosition({ center: [lon, lat], zoom }); } @@ -1189,7 +1189,7 @@ class MapService { const feature = this.map?.forEachFeatureAtPixel( event.pixel, (f: FeatureLike) => f as Feature, - { layerFilter, hitTolerance: 5 } + { layerFilter, hitTolerance: 5 }, ); if (!feature) return; @@ -1227,7 +1227,7 @@ class MapService { } const newCoordinates = coordinates.filter( - (_, index) => index !== closestIndex + (_, index) => index !== closestIndex, ); lineString.setCoordinates(newCoordinates); this.saveModifiedFeature(feature); @@ -1270,7 +1270,7 @@ class MapService { selected.add(f.getId()!); } } - } + }, ); this.setSelectedIds(selected); @@ -1417,7 +1417,7 @@ class MapService { public loadFeaturesFromApi( _apiStations: typeof mapStore.stations, _apiRoutes: typeof mapStore.routes, - _apiSights: typeof mapStore.sights + _apiSights: typeof mapStore.sights, ): void { if (!this.map) return; @@ -1450,8 +1450,8 @@ class MapService { transform( [station.longitude, station.latitude], "EPSG:4326", - projection - ) + projection, + ), ); const feature = new Feature({ geometry: point, name: station.name }); feature.setId(`station-${station.id}`); @@ -1462,7 +1462,7 @@ class MapService { filteredSights.forEach((sight) => { if (sight.longitude == null || sight.latitude == null) return; const point = new Point( - transform([sight.longitude, sight.latitude], "EPSG:4326", projection) + transform([sight.longitude, sight.latitude], "EPSG:4326", projection), ); const feature = new Feature({ geometry: point, @@ -1482,7 +1482,7 @@ class MapService { const coordinates = route.path .filter((c) => c && c[0] != null && c[1] != null) .map((c: [number, number]) => - transform([c[1], c[0]], "EPSG:4326", projection) + transform([c[1], c[0]], "EPSG:4326", projection), ); if (coordinates.length === 0) return; @@ -1568,7 +1568,7 @@ class MapService { public startDrawing( type: "Point" | "LineString", - featureType: FeatureType + featureType: FeatureType, ): void { if (!this.map) return; @@ -1732,7 +1732,7 @@ class MapService { this.map.forEachFeatureAtPixel( event.pixel, (f: FeatureLike) => f as Feature, - { layerFilter, hitTolerance: 5 } + { layerFilter, hitTolerance: 5 }, ); let finalFeature: Feature | null = null; @@ -1807,7 +1807,7 @@ class MapService { public deleteFeature( featureId: string | number | undefined, - recourse: string + recourse: string, ): void { if (featureId === undefined) return; @@ -1863,7 +1863,7 @@ class MapService { const lineFeature = this.lineSource.getFeatureById(id); if (lineFeature) this.lineSource.removeFeature( - lineFeature as Feature + lineFeature as Feature, ); const pointFeature = this.pointSource.getFeatureById(id); if (pointFeature) @@ -1890,11 +1890,11 @@ class MapService { if (targetEl instanceof HTMLElement) { targetEl.removeEventListener( "contextmenu", - this.boundHandleContextMenu + this.boundHandleContextMenu, ); targetEl.removeEventListener( "pointerleave", - this.boundHandlePointerLeave + this.boundHandlePointerLeave, ); } this.map.un("pointermove", this.boundHandlePointerMove as any); @@ -1907,7 +1907,7 @@ class MapService { } private handleFeatureEvent( - event: VectorSourceEvent> + event: VectorSourceEvent>, ): void { if (!event.feature) return; const feature = event.feature; @@ -1918,7 +1918,7 @@ class MapService { } private handleFeatureChange( - event: VectorSourceEvent> + event: VectorSourceEvent>, ): void { if (!event.feature) return; this.updateFeaturesInReact(); @@ -1956,7 +1956,7 @@ class MapService { }); this.modifyInteraction.setActive( - this.selectInteraction.getFeatures().getLength() > 0 + this.selectInteraction.getFeatures().getLength() > 0, ); this.clusterLayer.changed(); this.routeLayer.changed(); @@ -2026,7 +2026,7 @@ class MapService { if (typeof featureId === "number" || !String(featureId).includes("-")) { console.warn( "Skipping save for feature with non-standard ID:", - featureId + featureId, ); return; } @@ -2074,7 +2074,7 @@ class MapService { try { const createdFeatureData = await mapStore.createFeature( featureType, - featureGeoJSON + featureGeoJSON, ); const newFeatureId = `${featureType}-${createdFeatureData.id}`; @@ -2093,8 +2093,8 @@ class MapService { const lineGeom = new LineString( routeData.path.map((c) => - transform([c[1], c[0]], "EPSG:4326", projection) - ) + transform([c[1], c[0]], "EPSG:4326", projection), + ), ); feature.setGeometry(lineGeom); } else { @@ -2196,8 +2196,8 @@ const MapControls: React.FC = ({ isDisabled ? "bg-gray-200 text-gray-400 cursor-not-allowed" : isActive - ? "bg-blue-600 text-white shadow-md hover:bg-blue-700" - : "bg-gray-100 text-gray-700 hover:bg-blue-100 hover:text-blue-700" + ? "bg-blue-600 text-white shadow-md hover:bg-blue-700" + : "bg-gray-100 text-gray-700 hover:bg-blue-100 hover:text-blue-700" }`; return ( - {showHelp && ( -
-

Горячие клавиши:

-
    -
  • - - Shift - {" "} - - Режим выделения (лассо) -
  • -
  • - - Ctrl + клик - {" "} - - Добавить/убрать из выделения -
  • -
  • - Esc{" "} - - Отменить выделение -
  • -
+
+

Управление картой

+
+
+

Перемещение и масштаб:

+
    +
  • Колесо мыши — приблизить / отдалить.
  • +
  • + Средняя кнопка мыши (колесо зажато) — перетаскивание карты. +
  • +
+
+ +
+

Выделение объектов:

+
    +
  • Одинарный клик по объекту — выделить и центрировать.
  • +
  • + + Ctrl + {" "} + + клик — добавить / убрать объект из выделения. +
  • +
  • + + Shift + {" "} + — временно включить режим лассо (выделение области). +
  • +
  • Клик по пустому месту карты — снять выделение.
  • +
  • + + Esc + {" "} + — снять выделение всех объектов. +
  • +
+
+ +
+

+ Рисование и редактирование: +

+
    +
  • + Кнопки в верхней панели — выбор режима: редактирование, + добавление остановки, достопримечательности или маршрута. +
  • +
  • + При рисовании маршрута: правый клик — завершить линию. +
  • +
  • + В режиме редактирования: перетаскивайте точки маршрута для + изменения траектории. +
  • +
  • + Двойной клик по внутренней точке маршрута — удалить эту + точку (при наличии не менее 2 точек). +
  • +
+
+ +
+

Боковая панель:

+
    +
  • Клик по строке в списке — перейти к объекту на карте.
  • +
  • + Иконка карандаша — открыть объект в режиме редактирования. +
  • +
  • + Иконка карты у маршрутов — открыть предпросмотр маршрута. +
  • +
  • + Иконка глаза у маршрутов — скрыть / показать маршрут и + связанные остановки. +
  • +
  • Иконка корзины — удалить объект.
  • +
+
+
)} + +
{showContent && ( { const navigate = useNavigate(); @@ -45,18 +53,27 @@ export const RouteCreatePage = observer(() => { const [centerLat, setCenterLat] = useState(""); const [centerLng, setCenterLng] = useState(""); const [videoPreview, setVideoPreview] = useState(""); + const [icon, setIcon] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] = useState(false); const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false); const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false); const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false); + const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false); + const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false); + const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false); + const [previewIconId, setPreviewIconId] = useState(""); + const [activeIconMenuType, setActiveIconMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null + >(null); const [fileToUpload, setFileToUpload] = useState(null); const { language } = languageStore; useEffect(() => { carrierStore.getCarriers(language); articlesStore.getArticleList(); + mediaStore.getMedia(); }, [language]); const filteredCarriers = useMemo(() => { @@ -150,6 +167,23 @@ export const RouteCreatePage = observer(() => { setIsVideoPreviewOpen(true); }; + const handleIconSelect = (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + setIcon(media.id); + setIsSelectIconDialogOpen(false); + }; + + const selectedIconMedia = + icon && !isMediaIdEmpty(icon) + ? mediaStore.media.find((m) => m.id === icon) + : null; + const effectiveIconUrl = isMediaIdEmpty(icon) ? null : selectedIconMedia?.id ?? icon; + const effectiveVideoId = isMediaIdEmpty(videoPreview) ? null : videoPreview; + const handleCreateRoute = async () => { try { setIsLoading(true); @@ -243,8 +277,8 @@ export const RouteCreatePage = observer(() => { center_latitude, center_longitude, path, - video_preview: - videoPreview && videoPreview !== "" ? videoPreview : undefined, + video_preview: !isMediaIdEmpty(videoPreview) ? videoPreview : undefined, + icon: !isMediaIdEmpty(icon) ? icon : undefined, }; if (governor_appeal !== undefined) { @@ -403,16 +437,41 @@ export const RouteCreatePage = observer(() => { - { - setVideoPreview(""); - }} - onSelectVideoClick={handleVideoFileSelect} - className="w-full" - /> + +
+ { + setIsPreviewIconOpen(true); + setPreviewIconId(selectedIconMedia?.id ?? icon ?? ""); + }} + onDeleteImageClick={() => { + setIcon(""); + setActiveIconMenuType(null); + }} + onSelectFileClick={() => { + setActiveIconMenuType("image"); + setIsSelectIconDialogOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadIconDialogOpen(true); + setActiveIconMenuType("image"); + }} + /> +
+
+ setVideoPreview("")} + onSelectVideoClick={handleVideoFileSelect} + className="w-full" + /> +
+
Прямой/обратный маршрут @@ -522,7 +581,7 @@ export const RouteCreatePage = observer(() => { onSelectMedia={handleVideoSelect} mediaType={2} /> - {videoPreview && videoPreview !== "" && ( + {effectiveVideoId && ( setIsVideoPreviewOpen(false)} @@ -534,7 +593,7 @@ export const RouteCreatePage = observer(() => { { initialFile={fileToUpload || undefined} afterUpload={handleVideoUpload} /> + + setIsSelectIconDialogOpen(false)} + onSelectMedia={handleIconSelect} + mediaType={1} + /> + + setIsUploadIconDialogOpen(false)} + contextObjectName={routeName || "Маршрут"} + contextType="route" + afterUpload={handleIconSelect} + hardcodeType={activeIconMenuType} + /> + + setIsPreviewIconOpen(false)} + mediaId={previewIconId} + />
); }); diff --git a/src/pages/Route/RouteEditPage/index.tsx b/src/pages/Route/RouteEditPage/index.tsx index db66de9..d0cd621 100644 --- a/src/pages/Route/RouteEditPage/index.tsx +++ b/src/pages/Route/RouteEditPage/index.tsx @@ -13,24 +13,31 @@ import { DialogContent, DialogActions, } from "@mui/material"; -import { MediaViewer, VideoPreviewCard } from "@widgets"; +import { + MediaViewer, + VideoPreviewCard, + ImageUploadCard, + DeleteModal, +} from "@widgets"; import { observer } from "mobx-react-lite"; import { ArrowLeft, Copy, Save, Plus, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; - -import { carrierStore } from "../../../shared/store/CarrierStore"; -import { articlesStore } from "../../../shared/store/ArticlesStore"; +import { toast } from "react-toastify"; import { + carrierStore, + articlesStore, routeStore, languageStore, + mediaStore, + isMediaIdEmpty, + stationsStore, ArticleSelectOrCreateDialog, SelectMediaDialog, UploadMediaDialog, + PreviewMediaDialog, LoadingSpinner, } from "@shared"; -import { toast } from "react-toastify"; -import { stationsStore } from "@shared"; import { LinkedItems } from "../LinekedStations"; export const RouteEditPage = observer(() => { @@ -44,6 +51,14 @@ export const RouteEditPage = observer(() => { const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false); const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false); const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false); + const [isSelectIconDialogOpen, setIsSelectIconDialogOpen] = useState(false); + const [isUploadIconDialogOpen, setIsUploadIconDialogOpen] = useState(false); + const [isPreviewIconOpen, setIsPreviewIconOpen] = useState(false); + const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false); + const [previewIconId, setPreviewIconId] = useState(""); + const [activeIconMenuType, setActiveIconMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null + >(null); const [fileToUpload, setFileToUpload] = useState(null); const { language } = languageStore; const [coordinates, setCoordinates] = useState(""); @@ -71,10 +86,32 @@ export const RouteEditPage = observer(() => { await carrierStore.getCarriers(language); await stationsStore.getStations(); await articlesStore.getArticleList(); + await mediaStore.getMedia(); }; fetchData(); }, [id, language]); + const handleIconSelect = (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + routeStore.setEditRouteData({ icon: media.id }); + setIsSelectIconDialogOpen(false); + }; + + const selectedIconMedia = + editRouteData.icon && !isMediaIdEmpty(editRouteData.icon) + ? mediaStore.media.find((m) => m.id === editRouteData.icon) + : null; + const effectiveIconUrl = isMediaIdEmpty(editRouteData.icon) + ? null + : (selectedIconMedia?.id ?? editRouteData.icon); + const effectiveVideoId = isMediaIdEmpty(editRouteData.video_preview) + ? null + : editRouteData.video_preview; + useEffect(() => { if (editRouteData.path && editRouteData.path.length > 0) { const formattedPath = editRouteData.path @@ -552,16 +589,42 @@ export const RouteEditPage = observer(() => { - { - routeStore.setEditRouteData({ video_preview: "" }); - }} - onSelectVideoClick={handleVideoFileSelect} - className="w-full" - /> + +
+ { + setIsPreviewIconOpen(true); + setPreviewIconId( + selectedIconMedia?.id ?? editRouteData.icon ?? "" + ); + }} + onDeleteImageClick={() => setIsDeleteIconModalOpen(true)} + onSelectFileClick={() => { + setActiveIconMenuType("image"); + setIsSelectIconDialogOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadIconDialogOpen(true); + setActiveIconMenuType("image"); + }} + /> +
+
+ { + routeStore.setEditRouteData({ video_preview: "" }); + }} + onSelectVideoClick={handleVideoFileSelect} + className="w-full" + /> +
+
{ Предпросмотр видео - {editRouteData.video_preview && ( + {effectiveVideoId && ( { initialFile={fileToUpload || undefined} afterUpload={handleVideoUpload} /> + + setIsSelectIconDialogOpen(false)} + onSelectMedia={handleIconSelect} + mediaType={1} + /> + + setIsUploadIconDialogOpen(false)} + contextObjectName={editRouteData.route_name || "Маршрут"} + contextType="route" + afterUpload={handleIconSelect} + hardcodeType={activeIconMenuType} + /> + + setIsPreviewIconOpen(false)} + mediaId={previewIconId} + /> + + { + routeStore.setEditRouteData({ icon: "" }); + setIsDeleteIconModalOpen(false); + }} + onCancel={() => setIsDeleteIconModalOpen(false)} + edit + />
); }); diff --git a/src/pages/Route/route-preview/LeftSidebar.tsx b/src/pages/Route/route-preview/LeftSidebar.tsx index de2794d..39677d9 100644 --- a/src/pages/Route/route-preview/LeftSidebar.tsx +++ b/src/pages/Route/route-preview/LeftSidebar.tsx @@ -4,7 +4,7 @@ import { MediaViewer } from "@widgets"; import { useMapData } from "./MapDataContext"; import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; -import { authInstance } from "@shared"; +import { authInstance, isMediaIdEmpty } from "@shared"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import LanguageSelector from "./web-gl/LanguageSelector"; @@ -106,7 +106,7 @@ export const LeftSidebar = observer(({ open, onToggle }: LeftSidebarProps) => { gap: 10, }} > - {carrierThumbnail && ( + {carrierThumbnail && !isMediaIdEmpty(carrierThumbnail) && ( { justifyContent="center" flexGrow={1} > - {carrierLogo && ( + {carrierLogo && !isMediaIdEmpty(carrierLogo) && ( { ? { right: 0, transform: "none" } : { left: "50%", transform: "translateX(-50%)" }; + const iconUrl = station.icon + ? `${import.meta.env.VITE_KRBL_MEDIA}${station.icon}/download?token=${localStorage.getItem("token") ?? ""}` + : null; + const iconSizePx = Math.round(primaryFontSize * 1.2); + const secondaryLineHeight = 1.2; const secondaryHeight = showSecondary ? secondaryFontSize * secondaryLineHeight @@ -2019,10 +2024,24 @@ export const WebGLRouteMapPrototype = observer(() => {
+ {iconUrl ? ( + + ) : null}
{ ); }); -export default WebGLRouteMapPrototype; +export default WebGLRouteMapPrototype; \ No newline at end of file diff --git a/src/pages/Snapshot/SnapshotCreatePage/index.tsx b/src/pages/Snapshot/SnapshotCreatePage/index.tsx index c7f9be3..a02c9b6 100644 --- a/src/pages/Snapshot/SnapshotCreatePage/index.tsx +++ b/src/pages/Snapshot/SnapshotCreatePage/index.tsx @@ -25,7 +25,7 @@ export const SnapshotCreatePage = observer(() => { Назад
-

Создание снапшота

+

Создание экспорта медиа

{ } if (snapshotStore.snapshotStatus?.Status === "done") { - toast.success("Снапшот успешно создан"); + toast.success("Экспорт медиа успешно создан"); runInAction(() => { snapshotStore.snapshotStatus = null; @@ -63,7 +63,7 @@ export const SnapshotCreatePage = observer(() => { } } catch (error) { console.error(error); - toast.error("Ошибка при создании снапшота"); + toast.error("Ошибка при создании экспорта медиа"); } finally { setIsLoading(false); } diff --git a/src/pages/Snapshot/SnapshotListPage/index.tsx b/src/pages/Snapshot/SnapshotListPage/index.tsx index 1c6e11b..14a51cb 100644 --- a/src/pages/Snapshot/SnapshotListPage/index.tsx +++ b/src/pages/Snapshot/SnapshotListPage/index.tsx @@ -30,6 +30,14 @@ export const SnapshotListPage = observer(() => { fetchSnapshots(); }, [language]); + const formatCreationTime = (isoString: string | undefined) => { + if (!isoString) return ""; + const [datePart, timePartWithMs] = isoString.split("T"); + if (!datePart || !timePartWithMs) return isoString; + const timePart = timePartWithMs.split(".")[0]; + return `${datePart} - ${timePart}`; + }; + const columns: GridColDef[] = [ { field: "name", @@ -41,7 +49,14 @@ export const SnapshotListPage = observer(() => { headerName: "Родитель", flex: 1, }, - + { + field: "created_at", + headerName: "Дата создания", + flex: 1, + renderCell: (params: GridRenderCellParams) => { + return
{params.value ? params.value : "-"}
; + }, + }, { field: "actions", headerName: "Действия", @@ -79,14 +94,15 @@ export const SnapshotListPage = observer(() => { id: snapshot.ID, name: snapshot.Name, parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name, + created_at: formatCreationTime(snapshot.CreationTime), })); return ( <>
-

Снапшоты

- +

Экспорт Медиа

+
{ slots={{ noRowsOverlay: () => ( - {isLoading ? : "Нет снапшотов"} + {isLoading ? ( + + ) : ( + "Нет экспортов медиа" + )} ), }} diff --git a/src/pages/Station/StationCreatePage/index.tsx b/src/pages/Station/StationCreatePage/index.tsx index a6e2f31..d235f52 100644 --- a/src/pages/Station/StationCreatePage/index.tsx +++ b/src/pages/Station/StationCreatePage/index.tsx @@ -1,26 +1,33 @@ import { Button, - Paper, TextField, Select, MenuItem, FormControl, InputLabel, + Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; -import { ArrowLeft, Save } from "lucide-react"; -import { Loader2 } from "lucide-react"; +import { ArrowLeft, Loader2, Save } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; import { stationsStore, languageStore, cityStore, + mediaStore, + isMediaIdEmpty, useSelectedCity, + SelectMediaDialog, + UploadMediaDialog, + PreviewMediaDialog, } from "@shared"; import { useEffect, useState } from "react"; -import { LanguageSwitcher } from "@widgets"; -import { SaveWithoutCityAgree } from "@widgets"; +import { + ImageUploadCard, + LanguageSwitcher, + SaveWithoutCityAgree, +} from "@widgets"; export const StationCreatePage = observer(() => { const navigate = useNavigate(); @@ -35,6 +42,13 @@ export const StationCreatePage = observer(() => { const { cities, getCities } = cityStore; const { selectedCityId, selectedCity } = useSelectedCity(); const [coordinates, setCoordinates] = useState(""); + const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); + const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); + const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); + const [mediaId, setMediaId] = useState(""); + const [activeMenuType, setActiveMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null + >(null); const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false); @@ -96,8 +110,27 @@ export const StationCreatePage = observer(() => { }; fetchCities(); + mediaStore.getMedia(); }, []); + const handleMediaSelect = (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + setCreateCommonData({ icon: media.id }); + }; + + const selectedMedia = + createStationData.common.icon && + !isMediaIdEmpty(createStationData.common.icon) + ? mediaStore.media.find((m) => m.id === createStationData.common.icon) + : null; + const effectiveIconUrl = isMediaIdEmpty(createStationData.common.icon) + ? null + : selectedMedia?.id ?? createStationData.common.icon; + useEffect(() => { if (selectedCityId && selectedCity && !createStationData.common.city_id) { setCreateCommonData({ @@ -108,7 +141,7 @@ export const StationCreatePage = observer(() => { }, [selectedCityId, selectedCity, createStationData.common.city_id]); return ( - +
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */} + setIsSelectMediaOpen(false)} + onSelectMedia={handleMediaSelect} + mediaType={1} + /> + + setIsUploadMediaOpen(false)} + contextObjectName={createStationData[language].name || "Остановка"} + contextType="station" + afterUpload={handleMediaSelect} + hardcodeType={activeMenuType} + /> + + setIsPreviewMediaOpen(false)} + mediaId={mediaId} + /> + {isSaveWarningOpen && ( { }} /> )} -
+ ); }); diff --git a/src/pages/Station/StationEditPage/index.tsx b/src/pages/Station/StationEditPage/index.tsx index 7883b06..7fefe2f 100644 --- a/src/pages/Station/StationEditPage/index.tsx +++ b/src/pages/Station/StationEditPage/index.tsx @@ -1,6 +1,5 @@ import { Button, - Paper, TextField, Select, MenuItem, @@ -9,20 +8,28 @@ import { Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; -import { ArrowLeft, Save } from "lucide-react"; -import { Loader2 } from "lucide-react"; +import { ArrowLeft, Loader2, Save } from "lucide-react"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; import { stationsStore, languageStore, cityStore, + mediaStore, + isMediaIdEmpty, LoadingSpinner, + SelectMediaDialog, + PreviewMediaDialog, + UploadMediaDialog, } from "@shared"; import { useEffect, useState } from "react"; -import { LanguageSwitcher } from "@widgets"; +import { + ImageUploadCard, + LanguageSwitcher, + SaveWithoutCityAgree, + DeleteModal, +} from "@widgets"; import { LinkedSights } from "../LinkedSights"; -import { SaveWithoutCityAgree } from "@widgets"; export const StationEditPage = observer(() => { const navigate = useNavigate(); @@ -39,6 +46,14 @@ export const StationEditPage = observer(() => { } = stationsStore; const { cities, getCities } = cityStore; const [coordinates, setCoordinates] = useState(""); + const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); + const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); + const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); + const [mediaId, setMediaId] = useState(""); + const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false); + const [activeMenuType, setActiveMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null + >(null); const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false); @@ -95,6 +110,23 @@ export const StationEditPage = observer(() => { setIsSaveWarningOpen(false); }; + const handleMediaSelect = (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + setEditCommonData({ icon: media.id }); + }; + + const selectedMedia = + editStationData.common.icon && !isMediaIdEmpty(editStationData.common.icon) + ? mediaStore.media.find((m) => m.id === editStationData.common.icon) + : null; + const effectiveIconUrl = isMediaIdEmpty(editStationData.common.icon) + ? null + : selectedMedia?.id ?? editStationData.common.icon; + useEffect(() => { const fetchAndSetStationData = async () => { if (!id) { @@ -109,6 +141,7 @@ export const StationEditPage = observer(() => { await getCities("ru"); await getCities("en"); await getCities("zh"); + await mediaStore.getMedia(); } finally { setIsLoadingData(false); } @@ -133,7 +166,7 @@ export const StationEditPage = observer(() => { } return ( - +
@@ -239,6 +272,29 @@ export const StationEditPage = observer(() => { +
+ { + setIsPreviewMediaOpen(true); + setMediaId(effectiveIconUrl ?? ""); + }} + onDeleteImageClick={() => { + setIsDeleteIconModalOpen(true); + }} + onSelectFileClick={() => { + setActiveMenuType("image"); + setIsSelectMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("image"); + }} + /> +
+ {id && ( {
+ setIsSelectMediaOpen(false)} + onSelectMedia={handleMediaSelect} + mediaType={1} + /> + + setIsUploadMediaOpen(false)} + contextObjectName={editStationData[language].name || "Остановка"} + contextType="station" + afterUpload={handleMediaSelect} + hardcodeType={activeMenuType} + /> + + setIsPreviewMediaOpen(false)} + mediaId={mediaId} + /> + + { + setEditCommonData({ icon: "" }); + setIsDeleteIconModalOpen(false); + }} + onCancel={() => setIsDeleteIconModalOpen(false)} + edit + /> + {/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */} {isSaveWarningOpen && ( { }} /> )} -
+ ); }); diff --git a/src/pages/User/UserCreatePage/index.tsx b/src/pages/User/UserCreatePage/index.tsx index 219ab77..2816b5b 100644 --- a/src/pages/User/UserCreatePage/index.tsx +++ b/src/pages/User/UserCreatePage/index.tsx @@ -6,17 +6,35 @@ import { FormControlLabel, } from "@mui/material"; import { observer } from "mobx-react-lite"; -import { ArrowLeft, Save } from "lucide-react"; -import { Loader2 } from "lucide-react"; +import { ArrowLeft, Loader2, Save } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { toast } from "react-toastify"; -import { userStore } from "@shared"; -import { useState } from "react"; +import { + userStore, + mediaStore, + isMediaIdEmpty, + SelectMediaDialog, + UploadMediaDialog, + PreviewMediaDialog, +} from "@shared"; +import { useState, useEffect } from "react"; +import { ImageUploadCard } from "@widgets"; export const UserCreatePage = observer(() => { const navigate = useNavigate(); const { createUserData, setCreateUserData, createUser } = userStore; const [isLoading, setIsLoading] = useState(false); + const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); + const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); + const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); + const [mediaId, setMediaId] = useState(""); + const [activeMenuType, setActiveMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null + >(null); + + useEffect(() => { + mediaStore.getMedia(); + }, []); const handleCreate = async () => { try { @@ -31,6 +49,29 @@ export const UserCreatePage = observer(() => { } }; + const handleMediaSelect = (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + setCreateUserData( + createUserData.name || "", + createUserData.email || "", + createUserData.password || "", + createUserData.is_admin || false, + media.id + ); + }; + + const selectedMedia = + createUserData.icon && !isMediaIdEmpty(createUserData.icon) + ? mediaStore.media.find((m) => m.id === createUserData.icon) + : null; + const effectiveIconUrl = isMediaIdEmpty(createUserData.icon) + ? null + : selectedMedia?.id ?? createUserData.icon ?? null; + return (
@@ -54,7 +95,8 @@ export const UserCreatePage = observer(() => { e.target.value, createUserData.email || "", createUserData.password || "", - createUserData.is_admin || false + createUserData.is_admin || false, + createUserData.icon ) } /> @@ -69,7 +111,8 @@ export const UserCreatePage = observer(() => { createUserData.name || "", e.target.value, createUserData.password || "", - createUserData.is_admin || false + createUserData.is_admin || false, + createUserData.icon ) } /> @@ -84,7 +127,8 @@ export const UserCreatePage = observer(() => { createUserData.name || "", createUserData.email || "", e.target.value, - createUserData.is_admin || false + createUserData.is_admin || false, + createUserData.icon ) } /> @@ -99,7 +143,8 @@ export const UserCreatePage = observer(() => { createUserData.name || "", createUserData.email || "", createUserData.password || "", - e.target.checked + e.target.checked, + createUserData.icon ); }} /> @@ -108,6 +153,36 @@ export const UserCreatePage = observer(() => { />
+
+ { + setIsPreviewMediaOpen(true); + setMediaId(effectiveIconUrl ?? ""); + }} + onDeleteImageClick={() => { + setCreateUserData( + createUserData.name || "", + createUserData.email || "", + createUserData.password || "", + createUserData.is_admin || false, + "" + ); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("image"); + setIsSelectMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("image"); + }} + /> +
+
+ + setIsSelectMediaOpen(false)} + onSelectMedia={handleMediaSelect} + mediaType={1} + /> + + setIsUploadMediaOpen(false)} + contextObjectName={createUserData.name || "Пользователь"} + contextType="user" + afterUpload={handleMediaSelect} + hardcodeType={activeMenuType} + /> + + setIsPreviewMediaOpen(false)} + mediaId={mediaId} + /> ); }); diff --git a/src/pages/User/UserEditPage/index.tsx b/src/pages/User/UserEditPage/index.tsx index b5cd7a8..c9899f4 100644 --- a/src/pages/User/UserEditPage/index.tsx +++ b/src/pages/User/UserEditPage/index.tsx @@ -7,12 +7,21 @@ import { Box, } from "@mui/material"; import { observer } from "mobx-react-lite"; -import { ArrowLeft, Save } from "lucide-react"; -import { Loader2 } from "lucide-react"; +import { ArrowLeft, Loader2, Save } from "lucide-react"; import { useNavigate, useParams } from "react-router-dom"; import { toast } from "react-toastify"; -import { userStore, languageStore, LoadingSpinner } from "@shared"; +import { + userStore, + languageStore, + LoadingSpinner, + mediaStore, + isMediaIdEmpty, + SelectMediaDialog, + UploadMediaDialog, + PreviewMediaDialog, +} from "@shared"; import { useEffect, useState } from "react"; +import { ImageUploadCard, DeleteModal } from "@widgets"; export const UserEditPage = observer(() => { const navigate = useNavigate(); @@ -22,8 +31,16 @@ export const UserEditPage = observer(() => { const { id } = useParams(); const { editUserData, editUser, getUser, setEditUserData } = userStore; + const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false); + const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false); + const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false); + const [mediaId, setMediaId] = useState(""); + const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false); + const [activeMenuType, setActiveMenuType] = useState< + "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null + >(null); + useEffect(() => { - // Устанавливаем русский язык при загрузке страницы languageStore.setLanguage("ru"); }, []); @@ -40,19 +57,38 @@ export const UserEditPage = observer(() => { } }; + const handleMediaSelect = (media: { + id: string; + filename: string; + media_name?: string; + media_type: number; + }) => { + setEditUserData( + editUserData.name || "", + editUserData.email || "", + editUserData.password || "", + editUserData.is_admin || false, + media.id + ); + }; + useEffect(() => { (async () => { if (id) { setIsLoadingData(true); try { + await mediaStore.getMedia(); const data = await getUser(Number(id)); - setEditUserData( - data?.name || "", - data?.email || "", - data?.password || "", - data?.is_admin || false - ); + if (data) { + setEditUserData( + data.name || "", + data.email || "", + data.password || "", + data.is_admin || false, + data.icon || "" + ); + } } finally { setIsLoadingData(false); } @@ -62,6 +98,14 @@ export const UserEditPage = observer(() => { })(); }, [id]); + const selectedMedia = + editUserData.icon && !isMediaIdEmpty(editUserData.icon) + ? mediaStore.media.find((m) => m.id === editUserData.icon) + : null; + const effectiveIconUrl = isMediaIdEmpty(editUserData.icon) + ? null + : selectedMedia?.id ?? editUserData.icon ?? null; + if (isLoadingData) { return ( { e.target.value, editUserData.email || "", editUserData.password || "", - editUserData.is_admin || false + editUserData.is_admin || false, + editUserData.icon ) } /> @@ -114,7 +159,8 @@ export const UserEditPage = observer(() => { editUserData.name || "", e.target.value, editUserData.password || "", - editUserData.is_admin || false + editUserData.is_admin || false, + editUserData.icon ) } /> @@ -129,7 +175,8 @@ export const UserEditPage = observer(() => { editUserData.name || "", editUserData.email || "", e.target.value, - editUserData.is_admin || false + editUserData.is_admin || false, + editUserData.icon ) } /> @@ -142,7 +189,8 @@ export const UserEditPage = observer(() => { editUserData.name || "", editUserData.email || "", editUserData.password || "", - e.target.checked + e.target.checked, + editUserData.icon ) } /> @@ -150,6 +198,27 @@ export const UserEditPage = observer(() => { label="Администратор" /> +
+ { + setIsPreviewMediaOpen(true); + setMediaId(effectiveIconUrl ?? ""); + }} + onDeleteImageClick={() => setIsDeleteIconModalOpen(true)} + onSelectFileClick={() => { + setActiveMenuType("image"); + setIsSelectMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("image"); + }} + /> +
+
+ + setIsSelectMediaOpen(false)} + onSelectMedia={handleMediaSelect} + mediaType={1} + /> + + setIsUploadMediaOpen(false)} + contextObjectName={editUserData.name || "Пользователь"} + contextType="user" + afterUpload={handleMediaSelect} + hardcodeType={activeMenuType} + /> + + setIsPreviewMediaOpen(false)} + mediaId={mediaId} + /> + + { + setEditUserData( + editUserData.name || "", + editUserData.email || "", + editUserData.password || "", + editUserData.is_admin || false, + "" + ); + setIsDeleteIconModalOpen(false); + }} + onCancel={() => setIsDeleteIconModalOpen(false)} + edit + /> ); }); diff --git a/src/pages/Vehicle/VehicleCreatePage/index.tsx b/src/pages/Vehicle/VehicleCreatePage/index.tsx index b8c98de..0dff141 100644 --- a/src/pages/Vehicle/VehicleCreatePage/index.tsx +++ b/src/pages/Vehicle/VehicleCreatePage/index.tsx @@ -25,6 +25,7 @@ export const VehicleCreatePage = observer(() => { const [tailNumber, setTailNumber] = useState(""); const [type, setType] = useState(""); const [carrierId, setCarrierId] = useState(null); + const [model, setModel] = useState(""); const [isLoading, setIsLoading] = useState(false); const { language } = languageStore; @@ -40,7 +41,8 @@ export const VehicleCreatePage = observer(() => { Number(type), carrierStore.carriers[language].data?.find((c) => c.id === carrierId) ?.full_name as string, - carrierId! + carrierId!, + model || undefined, ); toast.success("Транспорт успешно создан"); } catch (error) { @@ -103,6 +105,14 @@ export const VehicleCreatePage = observer(() => { + setModel(e.target.value)} + placeholder="Произвольное название модели" + /> + + + + + + ); + }, + }, + ], + [ + devices, + getDevices, + getVehicles, + navigate, + setSelectedDevice, + snapshots, + setLogsModalDeviceUuid, + setLogsModalOpen, + ] + ); useEffect(() => { const fetchData = async () => { + setIsLoading(true); await getVehicles(); await getDevices(); await getSnapshots(); + setIsLoading(false); }; fetchData(); - }, [getDevices, getSnapshots]); + }, [getDevices, getSnapshots, getVehicles]); - const isAllSelected = - currentTableRows.length > 0 && - selectedDeviceUuids.length === currentTableRows.length; - - const handleSelectAllDevices = () => { - if (isAllSelected) { - setSelectedDeviceUuids([]); - } else { - setSelectedDeviceUuids( - currentTableRows.map((row) => row.device_uuid ?? "") - ); - } - }; - - const handleSelectDevice = ( - event: React.ChangeEvent, - deviceUuid: string - ) => { - if (event.target.checked) { - setSelectedDeviceUuids((prevSelected) => [...prevSelected, deviceUuid]); - } else { - setSelectedDeviceUuids((prevSelected) => - prevSelected.filter((uuid) => uuid !== deviceUuid) - ); - } - }; + useEffect(() => { + carrierStore.getCarriers("ru"); + }, []); const handleOpenSendSnapshotModal = () => { - if (selectedDeviceUuids.length > 0) { + if (selectedDeviceUuidsAllowed.length > 0) { toggleSendSnapshotModal(); } }; - const handleReloadStatus = async (uuid: string) => { - setSelectedDevice(uuid); - try { - await authInstance.post(`/devices/${uuid}/request-status`); - await getVehicles(); - await getDevices(); - } catch (error) { - console.error(`Error requesting status for device ${uuid}:`, error); - } - }; - const handleSendSnapshotAction = async (snapshotId: string) => { - if (selectedDeviceUuids.length === 0) return; + if (selectedDeviceUuidsAllowed.length === 0) return; + + const blockedCount = + selectedDeviceUuids.length - selectedDeviceUuidsAllowed.length; + if (blockedCount > 0) { + toast.info( + `Обновление ПО не отправлено на ${blockedCount} устройств (блокировка)` + ); + } const send = async (deviceUuid: string) => { try { await authInstance.post( `/devices/${deviceUuid}/force-snapshot-update`, - { - snapshot_id: snapshotId, - } + { snapshot_id: snapshotId } ); - toast.success(`Снапшот отправлен на устройство `); + toast.success("Обновление ПО отправлено на устройство"); } catch (error) { console.error(`Error sending snapshot to device ${deviceUuid}:`, error); - toast.error(`Не удалось отправить снапшот на устройство`); + toast.error("Не удалось отправить обновление ПО на устройство"); } }; - try { - const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => { - return send(deviceUuid); - }); - await Promise.allSettled(snapshotPromises); - - await getDevices(); - setSelectedDeviceUuids([]); - toggleSendSnapshotModal(); - } catch (error) { - console.error("Error in snapshot sending process:", error); - } - }; - - const getVehicleIdsByUuids = (uuids: string[]): number[] => { - return vehicles.data - .filter((vehicle) => uuids.includes(vehicle.vehicle.uuid ?? "")) - .map((vehicle) => vehicle.vehicle.id); + await Promise.allSettled(selectedDeviceUuidsAllowed.map(send)); + await getDevices(); + setSelectedIds([]); + toggleSendSnapshotModal(); }; const handleDeleteVehicles = async () => { - if (selectedDeviceUuids.length === 0) return; - - const vehicleIds = getVehicleIdsByUuids(selectedDeviceUuids); + if (selectedIds.length === 0) return; try { - await Promise.all(vehicleIds.map((id) => deleteVehicle(id))); + await Promise.all(selectedIds.map((id) => deleteVehicle(id))); await getVehicles(); await getDevices(); - setSelectedDeviceUuids([]); + setSelectedIds([]); setIsDeleteModalOpen(false); - toast.success(`Удалено устройств: ${vehicleIds.length}`); + toast.success(`Удалено устройств: ${selectedIds.length}`); } catch (error) { console.error("Error deleting vehicles:", error); toast.error("Ошибка при удалении устройств"); } }; + const createSelectionHandler = (groupRowIds: number[]) => { + const groupIdSet = new Set(groupRowIds); + return (newSelection: { ids: Set } | number[]) => { + let newIds: number[] = []; + if (Array.isArray(newSelection)) { + newIds = newSelection.map((id) => Number(id)); + } else if ( + newSelection && + typeof newSelection === "object" && + "ids" in newSelection + ) { + const idsSet = newSelection.ids as Set; + newIds = Array.from(idsSet) + .map((id) => (typeof id === "string" ? Number.parseInt(id, 10) : id)) + .filter((id) => !Number.isNaN(id)); + } + setSelectedIds((prev) => { + const fromOtherGroups = prev.filter((id) => !groupIdSet.has(id)); + return [...fromOtherGroups, ...newIds]; + }); + }; + }; + return ( <> - -
+
+
-
-
- - {selectedDeviceUuids.length > 0 && ( + {selectedIds.length > 0 && ( )}
- - - - - 0 && - selectedDeviceUuids.length < currentTableRows.length - } - checked={isAllSelected} - onChange={handleSelectAllDevices} - inputProps={{ "aria-label": "select all devices" }} - size="small" - /> - - Борт. номер - Онлайн - Обновлено - GPS - Медиа - Связь - Действия - - - - {currentTableRows.map((row) => ( - { - if ( - (event.target as HTMLElement).closest("button") === null && - (event.target as HTMLElement).closest( - 'input[type="checkbox"]' - ) === null - ) { - if (event.shiftKey) { - if (row.device_uuid) { - navigator.clipboard - .writeText(row.device_uuid) - .then(() => { - toast.success(`UUID скопирован`); - }) - .catch(() => { - toast.error("Не удалось скопировать UUID"); - }); - } else { - toast.warning("Устройство не имеет UUID"); - } - } - if (!event.shiftKey) { - handleSelectDevice( - { - target: { - checked: !selectedDeviceUuids.includes( - row.device_uuid ?? "" - ), - }, - } as React.ChangeEvent, - row.device_uuid ?? "" - ); - } - } - }} - sx={{ - cursor: "pointer", - "&:last-child td, &:last-child th": { border: 0 }, - }} - > - - - handleSelectDevice(event, row.device_uuid ?? "") - } - size="small" - /> - - - {row.tail_number} - - -
- {row.online ? ( - - ) : ( - - )} -
-
- -
- {formatDate(row.lastUpdate)} -
-
- -
- {row.gps ? ( - - ) : ( - - )} -
-
- -
- {row.media ? ( - - ) : ( - - )} -
-
- -
- {row.connection ? ( - - ) : ( - - )} -
-
- -
- - - -
-
-
- ))} - {currentTableRows.length === 0 && ( - - - Нет устройств для отображения. - - + {groupsByModel.length === 0 ? ( + + {isLoading ? ( + + ) : ( + "Нет устройств для отображения" )} -
-
- + + ) : ( +
+ {groupsByModel.map(({ model: groupModel, rows: groupRows }) => { + const isCollapsed = collapsedModels.has(groupModel); + const groupRowIds = groupRows.map((r) => r.id); + const selectedInGroup = selectedIds.filter((id) => + groupRowIds.includes(id) + ); + + return ( +
+ + + {!isCollapsed && ( + + void + } + rowSelectionModel={{ + type: "include", + ids: new Set(selectedInGroup), + }} + localeText={ + ruRU.components.MuiDataGrid.defaultProps.localeText + } + autoHeight + slots={{ + noRowsOverlay: () => null, + }} + sx={{ + border: "none", + "& .MuiDataGrid-columnHeaders": { + borderBottom: 1, + borderColor: "divider", + }, + "& .MuiDataGrid-cell": { + borderBottom: 1, + borderColor: "divider", + }, + }} + /> + + )} +
+ ); + })} +
+ )} +
- - Отправить снапшот - - - Выбрано устройств:{" "} + + Обновление ПО + + + Выбрано устройств для обновления:{" "} - {selectedDeviceUuids.length} + {selectedDeviceUuidsAllowed.length} - + {selectedDeviceUuids.length !== selectedDeviceUuidsAllowed.length && ( + + (пропущено{" "} + {selectedDeviceUuids.length - selectedDeviceUuidsAllowed.length} с + блокировкой) + + )} +
{snapshots && (snapshots as Snapshot[]).length > 0 ? ( (snapshots as Snapshot[]).map((snapshot) => ( @@ -495,9 +678,9 @@ export const DevicesTable = observer(() => { )) ) : ( - - Нет доступных снапшотов. - + + Нет доступных экспортов медиа. + )}
-
- {(() => { - return ( - <> -

- { - users?.data?.find( - // @ts-ignore - (user) => user.id === authStore.payload?.user_id - )?.name - } -

+ {(() => { + const currentUser = users?.data?.find( + (user) => user.id === (authStore.payload as { user_id?: number })?.user_id, + ); + const hasAvatar = + currentUser?.icon && !isMediaIdEmpty(currentUser.icon); + const token = localStorage.getItem("token"); + + return ( + <> +
+

{currentUser?.name}

= observer(({ children }) => { padding: "2px 10px", }} > - {/* @ts-ignore */} - {authStore.payload?.is_admin + {(authStore.payload as { is_admin?: boolean })?.is_admin ? "Администратор" : "Режим пользователя"}
- - ); - })()} -
-
- -
+
+
+ {hasAvatar ? ( + Аватар + ) : ( + + )} +
+ + ); + })()}
@@ -138,6 +147,9 @@ export const Layout: React.FC = observer(({ children }) => { )} +
+ v.{__APP_VERSION__} +
{ setIsPreviewMediaOpen(true); setMediaId(sight.watermark_lu ?? ""); @@ -363,7 +364,7 @@ export const CreateInformationTab = observer( { handleChange({ diff --git a/src/widgets/SightTabs/InformationTab/index.tsx b/src/widgets/SightTabs/InformationTab/index.tsx index 40c4994..19f8951 100644 --- a/src/widgets/SightTabs/InformationTab/index.tsx +++ b/src/widgets/SightTabs/InformationTab/index.tsx @@ -17,6 +17,7 @@ import { Language, cityStore, editSightStore, + isMediaIdEmpty, SelectMediaDialog, PreviewMediaDialog, SightLanguageInfo, @@ -334,7 +335,7 @@ export const InformationTab = observer( { setIsPreviewMediaOpen(true); setMediaId(sight.common.watermark_lu ?? ""); @@ -396,7 +397,7 @@ export const InformationTab = observer( { handleChange( diff --git a/src/widgets/SnapshotRestore/index.tsx b/src/widgets/SnapshotRestore/index.tsx index f49a4c9..98bf044 100644 --- a/src/widgets/SnapshotRestore/index.tsx +++ b/src/widgets/SnapshotRestore/index.tsx @@ -19,7 +19,7 @@ export const SnapshotRestore = ({ >

- Вы уверены, что хотите восстановить этот снапшот? + Вы уверены, что хотите восстановить этот экспорт медиа?

Это действие нельзя будет отменить. diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 6acf558..be3d550 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/route/route-preview/web-gl/languageselector.tsx","./src/pages/route/route-preview/webgl-prototype/webglroutemapprototype.tsx","./src/pages/sight/linkedstations.tsx","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/gltfcachemanager.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/articleselectorcreatedialog/index.tsx","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/modelloadingstore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/animatedcirclebutton.tsx","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/loadingspinner/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/modelloadingindicator/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/editstationtransfersmodal/index.tsx","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/route/route-preview/web-gl/languageselector.tsx","./src/pages/route/route-preview/webgl-prototype/webglroutemapprototype.tsx","./src/pages/sight/linkedstations.tsx","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/gltfcachemanager.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/articleselectorcreatedialog/index.tsx","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/modelloadingstore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/animatedcirclebutton.tsx","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/loadingspinner/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/modelloadingindicator/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/devicelogsmodal.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/editstationtransfersmodal/index.tsx","./src/widgets/modals/selectarticledialog/index.tsx"],"version":"5.8.3"} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 2cdf8f2..c385836 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,12 +2,10 @@ import { defineConfig, type UserConfigExport } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; +import pkg from "./package.json"; export default defineConfig({ - plugins: [ - react(), - tailwindcss(), - ], + plugins: [react(), tailwindcss()], resolve: { alias: { "@shared": path.resolve(__dirname, "src/shared"), @@ -18,9 +16,11 @@ export default defineConfig({ "@app": path.resolve(__dirname, "src/app"), }, }, + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + }, build: { chunkSizeWarningLimit: 5000, }, }); -