diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index 6082418..e82522d 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -16,12 +16,7 @@ import { SnapshotListPage, CarrierListPage, StationListPage, - // VehicleListPage, ArticleListPage, - - // CountryPreviewPage, - // VehiclePreviewPage, - // CarrierPreviewPage, SnapshotCreatePage, CountryCreatePage, CityCreatePage, @@ -31,7 +26,6 @@ import { CityEditPage, UserCreatePage, UserEditPage, - // VehicleEditPage, CarrierEditPage, StationCreatePage, StationPreviewPage, @@ -75,7 +69,6 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { return <>{children}; }; -// Чтобы очистка сторов происходила при смене локации const ClearStoresWrapper: React.FC<{ children: React.ReactNode }> = ({ children, }) => { @@ -116,65 +109,45 @@ const router = createBrowserRouter([ children: [ { index: true, element: }, - // Sight { path: "sight", element: }, { path: "sight/create", element: }, { path: "sight/:id/edit", element: }, - // Device { path: "devices", element: }, - // Map { path: "map", element: }, - // Media { path: "media", element: }, { path: "media/:id", element: }, { path: "media/:id/edit", element: }, - // Country { path: "country", element: }, { path: "country/create", element: }, { path: "country/add", element: }, - // { path: "country/:id", element: }, { path: "country/:id/edit", element: }, - // City { path: "city", element: }, { path: "city/create", element: }, - // { path: "city/:id", element: }, { path: "city/:id/edit", element: }, - // Route { path: "route", element: }, { path: "route/create", element: }, { path: "route/:id/edit", element: }, - // User { path: "user", element: }, { path: "user/create", element: }, { path: "user/:id/edit", element: }, - // Snapshot { path: "snapshot", element: }, { path: "snapshot/create", element: }, - // Carrier { path: "carrier", element: }, { path: "carrier/create", element: }, - // { path: "carrier/:id", element: }, { path: "carrier/:id/edit", element: }, - // Station { path: "station", element: }, { path: "station/create", element: }, { path: "station/:id", element: }, { path: "station/:id/edit", element: }, - // Vehicle - // { path: "vehicle", element: }, { path: "vehicle/create", element: }, - // { path: "vehicle/:id", element: }, - // { path: "vehicle/:id/edit", element: }, - // Article { path: "article", element: }, { path: "article/:id", element: }, - // { path: "media/create", element: }, ], }, ]); diff --git a/src/pages/Carrier/CarrierCreatePage/index.tsx b/src/pages/Carrier/CarrierCreatePage/index.tsx index 98e795a..a61daae 100644 --- a/src/pages/Carrier/CarrierCreatePage/index.tsx +++ b/src/pages/Carrier/CarrierCreatePage/index.tsx @@ -48,7 +48,6 @@ export const CarrierCreatePage = observer(() => { languageStore.setLanguage("ru"); }, []); - // Автоматически устанавливаем выбранный город при загрузке страницы useEffect(() => { if (selectedCityId && !createCarrierData.city_id) { setCreateCarrierData( diff --git a/src/pages/Carrier/CarrierEditPage/index.tsx b/src/pages/Carrier/CarrierEditPage/index.tsx index e914ba2..ff6ed55 100644 --- a/src/pages/Carrier/CarrierEditPage/index.tsx +++ b/src/pages/Carrier/CarrierEditPage/index.tsx @@ -74,7 +74,6 @@ export const CarrierEditPage = observer(() => { mediaStore.getMedia(); })(); - // Устанавливаем русский язык при загрузке страницы languageStore.setLanguage("ru"); }, [id]); diff --git a/src/pages/City/CityEditPage/index.tsx b/src/pages/City/CityEditPage/index.tsx index 4d7b0b4..dcccc3f 100644 --- a/src/pages/City/CityEditPage/index.tsx +++ b/src/pages/City/CityEditPage/index.tsx @@ -44,7 +44,6 @@ export const CityEditPage = observer(() => { const { getMedia, getOneMedia } = mediaStore; useEffect(() => { - // Устанавливаем русский язык при загрузке страницы languageStore.setLanguage("ru"); }, []); @@ -64,12 +63,11 @@ export const CityEditPage = observer(() => { (async () => { if (id) { await getCountries("ru"); - // Fetch data for all languages + const ruData = await getCity(id as string, "ru"); const enData = await getCity(id as string, "en"); const zhData = await getCity(id as string, "zh"); - // Set data for each language setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru"); setEditCityData(enData.name, enData.country_code, enData.arms, "en"); setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh"); @@ -207,7 +205,7 @@ export const CityEditPage = observer(() => { open={isSelectMediaOpen} onClose={() => setIsSelectMediaOpen(false)} onSelectMedia={handleMediaSelect} - mediaType={1} // Тип медиа для иконок + mediaType={1} /> { countryStore; useEffect(() => { - // Устанавливаем русский язык при загрузке страницы languageStore.setLanguage("ru"); }, []); @@ -36,12 +35,10 @@ export const CountryEditPage = observer(() => { useEffect(() => { (async () => { if (id) { - // Fetch data for all languages const ruData = await getCountry(id as string, "ru"); const enData = await getCountry(id as string, "en"); const zhData = await getCountry(id as string, "zh"); - // Set data for each language setEditCountryData(ruData.name, "ru"); setEditCountryData(enData.name, "en"); setEditCountryData(zhData.name, "zh"); diff --git a/src/pages/LoginPage/index.tsx b/src/pages/LoginPage/index.tsx index ef14c28..d69157a 100644 --- a/src/pages/LoginPage/index.tsx +++ b/src/pages/LoginPage/index.tsx @@ -24,7 +24,6 @@ export const LoginPage = () => { const { login } = authStore; const { getUsers } = userStore; useEffect(() => { - // Load saved credentials if they exist const savedEmail = localStorage.getItem("rememberedEmail"); const savedPassword = localStorage.getItem("rememberedPassword"); if (savedEmail && savedPassword) { @@ -42,7 +41,6 @@ export const LoginPage = () => { try { await login(email, password); - // Save or clear credentials based on remember me checkbox if (rememberMe) { localStorage.setItem("rememberedEmail", email); localStorage.setItem("rememberedPassword", password); diff --git a/src/pages/MapPage/index.tsx b/src/pages/MapPage/index.tsx index db3732b..33fdf5d 100644 --- a/src/pages/MapPage/index.tsx +++ b/src/pages/MapPage/index.tsx @@ -60,7 +60,6 @@ import Source from "ol/source/Source"; import { FeatureLike } from "ol/Feature"; import { createEmpty, extend, getCenter } from "ol/extent"; -// --- CUSTOM SCROLLBAR STYLES --- const scrollbarStyles = ` .scrollbar-hide { -ms-overflow-style: none; @@ -100,8 +99,6 @@ if (typeof document !== "undefined") { document.head.appendChild(styleElement); } -// --- MAP STORE --- -// @ts-ignore import { languageInstance } from "@shared"; import { makeAutoObservable } from "mobx"; @@ -114,14 +111,11 @@ import { carrierStore, } from "@shared"; -// Функция для сброса кешей карты export const clearMapCaches = () => { - // Сброс кешей маршрутов mapStore.routes = []; mapStore.stations = []; mapStore.sights = []; - // Сброс кешей MapService если он доступен if (typeof window !== "undefined" && (window as any).mapServiceInstance) { (window as any).mapServiceInstance.clearCaches(); } @@ -166,7 +160,6 @@ export type SortType = | "updated_asc" | "updated_desc"; -// --- HIDDEN ROUTES STORAGE --- const HIDDEN_ROUTES_KEY = "mapHiddenRoutes"; const HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY = "mapHideSightsByHiddenRoutes"; @@ -202,9 +195,9 @@ const saveHiddenRoutes = (hiddenRoutes: Set): void => { class MapStore { constructor() { makeAutoObservable(this); - // Загружаем скрытые маршруты из localStorage при инициализации + this.hiddenRoutes = getStoredHiddenRoutes(); - // Загружаем настройку скрытия достопримечательностей + try { const stored = localStorage.getItem(HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY); this.hideSightsByHiddenRoutes = stored @@ -220,8 +213,8 @@ class MapStore { sights: ApiSight[] = []; hiddenRoutes: Set; hideSightsByHiddenRoutes: boolean = false; - routeStationsCache: Map = new Map(); // Кэш станций для маршрутов - routeSightsCache: Map = new Map(); // Кэш достопримечательностей для маршрутов + routeStationsCache: Map = new Map(); + routeSightsCache: Map = new Map(); setHideSightsByHiddenRoutes(val: boolean) { this.hideSightsByHiddenRoutes = val; try { @@ -232,11 +225,9 @@ class MapStore { } catch (e) {} } - // НОВЫЕ ПОЛЯ ДЛЯ СОРТИРОВКИ stationSort: SortType = "name_asc"; sightSort: SortType = "name_asc"; - // НОВЫЕ МЕТОДЫ-СЕТТЕРЫ setStationSort = (sortType: SortType) => { this.stationSort = sortType; }; @@ -245,7 +236,6 @@ class MapStore { this.sightSort = sortType; }; - // ПРИВАТНЫЙ МЕТОД ДЛЯ ОБЩЕЙ ЛОГИКИ СОРТИРОВКИ private sortFeatures( features: T[], sortType: SortType @@ -269,7 +259,7 @@ class MapStore { new Date(b.created_at).getTime() ); } - // Фоллбэк: сортировка по ID, если дата недоступна + return a.id - b.id; }); case "created_desc": @@ -285,7 +275,7 @@ class MapStore { new Date(a.created_at).getTime() ); } - // Фоллбэк: сортировка по ID, если дата недоступна + return b.id - a.id; }); case "updated_asc": @@ -319,7 +309,6 @@ class MapStore { } } - // НОВЫЕ ГЕТТЕРЫ, ВОЗВРАЩАЮЩИЕ ОТСОРТИРОВАННЫЕ СПИСКИ get sortedStations(): ApiStation[] { return this.sortFeatures(this.stations, this.stationSort); } @@ -328,7 +317,6 @@ class MapStore { return this.sortFeatures(this.sights, this.sightSort); } - // ГЕТТЕРЫ ДЛЯ ФИЛЬТРАЦИИ ПО ГОРОДУ get filteredStations(): ApiStation[] { const selectedCityId = selectedCityStore.selectedCityId; if (!selectedCityId) { @@ -345,12 +333,9 @@ class MapStore { return this.routes; } - // Получаем carriers для текущего языка const carriers = carrierStore.carriers.ru.data; - // Фильтруем маршруты по городу через carriers return this.routes.filter((route: ApiRoute) => { - // Находим carrier для маршрута const carrier = carriers.find((c: any) => c.id === route.carrier_id); return carrier && carrier.city_id === selectedCityId; }); @@ -366,14 +351,12 @@ class MapStore { return cityFiltered; } - // Собираем все достопримечательности, связанные со скрытыми маршрутами const hiddenSightIds = new Set(); this.hiddenRoutes.forEach((routeId) => { const sightIds = this.routeSightsCache.get(routeId) || []; sightIds.forEach((id) => hiddenSightIds.add(id)); }); - // Фильтруем достопримечательности, исключая привязанные к скрытым маршрутам return cityFiltered.filter((s) => !hiddenSightIds.has(s.id)); } @@ -397,16 +380,12 @@ class MapStore { a.route_number.localeCompare(b.route_number) ); - // Предзагружаем станции для всех маршрутов и кэшируем их await this.preloadRouteStations(routesIds); - // Предзагружаем достопримечательности для всех маршрутов + await this.preloadRouteSights(routesIds); }; preloadRouteStations = async (routesIds: number[]) => { - console.log( - `[MapStore] Preloading stations for ${routesIds.length} routes` - ); const stationPromises = routesIds.map(async (routeId) => { try { const stationsResponse = await languageInstance("ru").get( @@ -422,13 +401,9 @@ class MapStore { } }); await Promise.all(stationPromises); - console.log( - `[MapStore] Preloaded stations for ${this.routeStationsCache.size} routes` - ); }; preloadRouteSights = async (routesIds: number[]) => { - console.log(`[MapStore] Preloading sights for ${routesIds.length} routes`); const sightPromises = routesIds.map(async (routeId) => { try { const sightsResponse = await languageInstance("ru").get( @@ -441,9 +416,6 @@ class MapStore { } }); await Promise.all(sightPromises); - console.log( - `[MapStore] Preloaded sights for ${this.routeSightsCache.size} routes` - ); }; getStations = async () => { @@ -473,7 +445,7 @@ class MapStore { createFeature = async (featureType: string, geoJsonFeature: any) => { const { geometry, properties } = geoJsonFeature; - let createdItem; + let createdItem: any; if (featureType === "station") { const name = properties.name || "Остановка 1"; @@ -524,7 +496,6 @@ class MapStore { "EPSG:3857" ); - // Автоматически назначаем перевозчика из выбранного города let carrier_id = 0; let carrier = ""; @@ -581,11 +552,8 @@ class MapStore { throw new Error(`Unknown feature type for creation: ${featureType}`); } - // @ts-ignore if (featureType === "route") this.routes.push(createdItem); - // @ts-ignore else if (featureType === "station") this.stations.push(createdItem); - // @ts-ignore else if (featureType === "sight") this.sights.push(createdItem); return createdItem; @@ -686,18 +654,15 @@ class MapStore { const mapStore = new MapStore(); -// Делаем mapStore доступным глобально для сброса кешей if (typeof window !== "undefined") { (window as any).mapStore = mapStore; } -// --- CONFIGURATION --- export const mapConfig = { center: [30.311, 59.94] as [number, number], zoom: 13, }; -// --- MAP POSITION STORAGE --- const MAP_POSITION_KEY = "mapPosition"; const ACTIVE_SECTION_KEY = "mapActiveSection"; @@ -736,7 +701,6 @@ const saveMapPosition = (position: MapPosition): void => { } }; -// --- ACTIVE SECTION STORAGE --- const getStoredActiveSection = (): string | null => { try { const stored = localStorage.getItem(ACTIVE_SECTION_KEY); @@ -761,7 +725,6 @@ const saveActiveSection = (section: string | null): void => { } }; -// --- TYPE DEFINITIONS --- interface MapServiceConfig { target: HTMLElement; center: [number, number]; @@ -774,15 +737,15 @@ class MapService { private map: OLMap | null; public pointSource: VectorSource>; public lineSource: VectorSource>; - public clusterLayer: VectorLayer; // Public for the deselect handler - public routeLayer: VectorLayer>>; // Public for deselect + public clusterLayer: VectorLayer; + public routeLayer: VectorLayer>>; private clusterSource: Cluster; private clusterStyleCache: { [key: number]: Style }; private tooltipElement: HTMLElement; private tooltipOverlay: Overlay | null; private mode: string | null; - // @ts-ignore + private currentDrawingType: "Point" | "LineString" | null; private currentDrawingFeatureType: FeatureType | null; private currentInteraction: Draw | null; @@ -801,7 +764,6 @@ class MapService { null; private isCreating: boolean = false; - // Styles private defaultStyle: Style; private selectedStyle: Style; private drawStyle: Style; @@ -816,7 +778,6 @@ class MapService { private hoverSightIconStyle: Style; private universalHoverStyleLine: Style; - // Callbacks private setLoading: (loading: boolean) => void; private setError: (error: string | null) => void; private onModeChangeCallback: (mode: string) => void; @@ -958,13 +919,12 @@ class MapService { this.routeLayer = new VectorLayer({ source: this.lineSource, - // @ts-ignore + style: (featureLike: FeatureLike) => { const feature = featureLike as Feature; if (!feature) return this.defaultStyle; const fId = feature.getId(); - // Все маршруты всегда отображаются, так как они не кластеризуются const isSelected = this.selectInteraction?.getFeatures().getArray().includes(feature) || (fId !== undefined && this.selectedIds.has(fId)); @@ -1029,9 +989,6 @@ class MapService { }); this.clusterSource.on("change", () => { - // Поскольку маршруты больше не добавляются как точки, - // нам не нужно отслеживать unclusteredRouteIds - // Все маршруты всегда отображаются как линии this.routeLayer.changed(); }); @@ -1080,21 +1037,18 @@ class MapService { new KeyboardZoom(), new PinchZoom(), new PinchRotate(), - // Отключаем DoubleClickZoom как было изначально - // new DoubleClickZoom(), + new DragPan({ condition: (event) => { - // Разрешаем перетаскивание только при нажатии средней кнопки мыши (колёсико) const originalEvent = event.originalEvent; if (!originalEvent) return false; - // Проверяем, что это событие мыши и нажата средняя кнопка if ( originalEvent.type === "pointerdown" || originalEvent.type === "pointermove" ) { const pointerEvent = originalEvent as PointerEvent; - return pointerEvent.buttons === 4; // 4 = средняя кнопка мыши + return pointerEvent.buttons === 4; } return false; @@ -1167,7 +1121,7 @@ class MapService { originalFeatures.length === 1 && originalFeatures[0].get("isProxy") ) - return false; // Ignore proxy points + return false; return true; }, multi: true, @@ -1302,7 +1256,7 @@ class MapService { const selected = new Set(); this.pointSource.forEachFeatureInExtent(extent, (f) => { - if (f.get("isProxy")) return; // Ignore proxy in lasso + if (f.get("isProxy")) return; const geom = f.getGeometry(); if (geom && geom.getType() === "Point") { const pointCoords = (geom as Point).getCoordinates(); @@ -1339,7 +1293,6 @@ class MapService { this.selectInteraction.setActive(false); this.lassoInteraction.setActive(false); - // --- ИСПРАВЛЕНИЕ: Главный обработчик выбора объектов и кластеров this.selectInteraction.on("select", (e: SelectEvent) => { if (this.mode !== "edit" || !this.map) return; @@ -1347,13 +1300,11 @@ class MapService { e.mapBrowserEvent.originalEvent.ctrlKey || e.mapBrowserEvent.originalEvent.metaKey; - // Проверяем, был ли клик по кластеру (группе) if (e.selected.length === 1 && !ctrlKey) { const clickedFeature = e.selected[0]; const originalFeatures = clickedFeature.get("features"); if (originalFeatures && originalFeatures.length > 1) { - // Если да, то приближаем карту const extent = createEmpty(); originalFeatures.forEach((feat: Feature) => { const geom = feat.getGeometry(); @@ -1364,20 +1315,17 @@ class MapService { padding: [60, 60, 60, 60], maxZoom: 18, }); - // Сбрасываем выделение, так как мы не хотим "выделять" сам кластер + this.selectInteraction.getFeatures().clear(); this.setSelectedIds(new Set()); - return; // Завершаем обработку + return; } } - // При Ctrl+клик сохраняем предыдущие выделения и добавляем/удаляем только изменённые - // При обычном клике создаём новый набор const newSelectedIds = ctrlKey ? new Set(this.selectedIds) : new Set(); - // Добавляем новые выбранные элементы e.selected.forEach((feature) => { const originalFeatures = feature.get("features"); let targetId: string | number | undefined; @@ -1389,8 +1337,6 @@ class MapService { } if (targetId !== undefined) { - // При Ctrl+клик: если элемент уже был выбран, снимаем его (toggle) - // Если не был выбран, добавляем if (ctrlKey && newSelectedIds.has(targetId)) { newSelectedIds.delete(targetId); } else { @@ -1399,9 +1345,6 @@ class MapService { } }); - // При Ctrl+клик игнорируем deselected, так как Select interaction может снимать - // предыдущие выделения, но мы хотим их сохранить - // При обычном клике удаляем deselected элементы if (!ctrlKey) { e.deselected.forEach((feature) => { const originalFeatures = feature.get("features"); @@ -1425,50 +1368,41 @@ class MapService { this.map.on("pointermove", this.boundHandlePointerMove as any); const targetEl = this.map.getTargetElement(); if (targetEl instanceof HTMLElement) { - // Устанавливаем курсор pointer по умолчанию для всей карты targetEl.style.cursor = "pointer"; targetEl.addEventListener("contextmenu", this.boundHandleContextMenu); targetEl.addEventListener("pointerleave", this.boundHandlePointerLeave); - // Добавляем обработчики для изменения курсора при нажатии средней кнопки мыши targetEl.addEventListener("pointerdown", (e) => { if (e.buttons === 4) { - // Средняя кнопка мыши - e.preventDefault(); // Предотвращаем скролл страницы + e.preventDefault(); targetEl.style.cursor = "grabbing"; } }); targetEl.addEventListener("pointerup", (e) => { if (e.button === 1) { - // Средняя кнопка мыши отпущена - e.preventDefault(); // Предотвращаем скролл страницы + e.preventDefault(); targetEl.style.cursor = "pointer"; } }); - // Также добавляем обработчик для mousedown/mouseup для совместимости targetEl.addEventListener("mousedown", (e) => { if (e.button === 1) { - // Средняя кнопка мыши - e.preventDefault(); // Предотвращаем скролл страницы + e.preventDefault(); targetEl.style.cursor = "grabbing"; } }); targetEl.addEventListener("mouseup", (e) => { if (e.button === 1) { - // Средняя кнопка мыши отпущена - e.preventDefault(); // Предотвращаем скролл страницы + e.preventDefault(); targetEl.style.cursor = "pointer"; } }); - // Дополнительная защита от нежелательного поведения средней кнопки мыши targetEl.addEventListener("auxclick", (e) => { if (e.button === 1) { - // Средняя кнопка мыши - e.preventDefault(); // Предотвращаем открытие ссылки в новой вкладке + e.preventDefault(); } }); } @@ -1499,16 +1433,10 @@ class MapService { const pointFeatures: Feature[] = []; const lineFeatures: Feature[] = []; - // Используем фильтрованные данные из mapStore const filteredStations = mapStore.filteredStations; const filteredSights = mapStore.filteredSights; const filteredRoutes = mapStore.filteredRoutes; - console.log( - `[loadFeaturesFromApi] Loading with ${mapStore.hiddenRoutes.size} hidden routes` - ); - - // Собираем все станции видимых маршрутов из кэша const stationsInVisibleRoutes = new Set(); filteredRoutes .filter((route) => !mapStore.hiddenRoutes.has(route.id)) @@ -1517,15 +1445,10 @@ class MapService { stationIds.forEach((id) => stationsInVisibleRoutes.add(id)); }); - console.log( - `[loadFeaturesFromApi] Found ${stationsInVisibleRoutes.size} stations in visible routes, total stations: ${filteredStations.length}` - ); - let skippedStations = 0; filteredStations.forEach((station) => { if (station.longitude == null || station.latitude == null) return; - // Пропускаем станции, которые принадлежат только скрытым маршрутам if (!stationsInVisibleRoutes.has(station.id)) { skippedStations++; return; @@ -1562,7 +1485,6 @@ class MapService { filteredRoutes.forEach((route) => { if (!route.path || route.path.length === 0) return; - // Пропускаем скрытые маршруты if (mapStore.hiddenRoutes.has(route.id)) return; const coordinates = route.path @@ -1583,10 +1505,6 @@ class MapService { lineFeatures.push(lineFeature); }); - console.log( - `[loadFeaturesFromApi] Skipped ${skippedStations} stations (belonging only to hidden routes)` - ); - this.pointSource.addFeatures(pointFeatures); this.lineSource.addFeatures(lineFeatures); @@ -1611,7 +1529,6 @@ class MapService { } if (this.tooltipOverlay) this.tooltipOverlay.setPosition(undefined); - // Сбрасываем курсор при покидании области карты if (this.map) { const targetEl = this.map.getTargetElement(); if (targetEl instanceof HTMLElement) { @@ -1692,10 +1609,9 @@ class MapService { const fType = this.currentDrawingFeatureType; if (!fType) return; - // Проверяем, не идет ли уже процесс создания if (this.isCreating) { toast.warning("Дождитесь завершения создания предыдущего объекта."); - // Удаляем созданный объект из источника + const sourceForDrawing = type === "Point" ? this.pointSource : this.lineSource; setTimeout(() => { @@ -1712,7 +1628,6 @@ class MapService { switch (fType) { case "station": - // Используем полный список из mapStore, а не отфильтрованный const stationNumbers = mapStore.stations .map((station) => { const match = station.name?.match(/^Остановка (\d+)$/); @@ -1724,7 +1639,6 @@ class MapService { resourceName = `Остановка ${nextStationNumber}`; break; case "sight": - // Используем полный список из mapStore, а не отфильтрованный const sightNumbers = mapStore.sights .map((sight) => { const match = sight.name?.match(/^Достопримечательность (\d+)$/); @@ -1736,7 +1650,6 @@ class MapService { resourceName = `Достопримечательность ${nextSightNumber}`; break; case "route": - // Используем полный список из mapStore, а не отфильтрованный const routeNumbers = mapStore.routes .map((route) => { const match = route.route_number?.match(/^Маршрут (\d+)$/); @@ -1778,11 +1691,8 @@ class MapService { private stopDrawing() { if (this.map && this.currentInteraction) { try { - // @ts-ignore this.currentInteraction.abortDrawing(); - } catch (e) { - /* ignore */ - } + } catch (e) {} this.map.removeInteraction(this.currentInteraction); } this.currentInteraction = null; @@ -1793,7 +1703,6 @@ class MapService { public finishDrawing(): void { if (!this.currentInteraction) return; - // Блокируем завершение рисования, если идет процесс создания if (this.isCreating) { toast.warning("Дождитесь завершения создания предыдущего объекта."); return; @@ -1824,7 +1733,7 @@ class MapService { layerFilter, hitTolerance: 5, }); - // Устанавливаем курсор pointer для всей карты, чтобы показать возможность перетаскивания колёсиком + this.map.getTargetElement().style.cursor = hit ? "pointer" : "pointer"; const featureAtPixel: Feature | undefined = @@ -1838,7 +1747,7 @@ class MapService { if (featureAtPixel) { const originalFeatures = featureAtPixel.get("features"); if (originalFeatures && originalFeatures.length > 0) { - if (originalFeatures[0].get("isProxy")) return; // Ignore hover on proxy + if (originalFeatures[0].get("isProxy")) return; finalFeature = originalFeatures[0]; } else { finalFeature = featureAtPixel; @@ -2087,13 +1996,11 @@ class MapService { return this.map; } - // Метод для сброса кешей карты public clearCaches() { this.clusterStyleCache = {}; this.hoveredFeatureId = null; this.selectedIds.clear(); - // Очищаем источники данных if (this.pointSource) { this.pointSource.clear(); } @@ -2101,7 +2008,6 @@ class MapService { this.lineSource.clear(); } - // Обновляем слои if (this.clusterLayer) { this.clusterLayer.changed(); } @@ -2151,10 +2057,9 @@ class MapService { const featureType = feature.get("featureType") as FeatureType; if (!featureType || !this.map) return; - // Проверяем, не идет ли уже процесс создания if (this.isCreating) { toast.warning("Дождитесь завершения создания предыдущего объекта."); - // Удаляем незавершенный объект с карты + if (feature.getGeometry()?.getType() === "LineString") { if (this.lineSource.hasFeature(feature as Feature)) this.lineSource.removeFeature(feature as Feature); @@ -2181,37 +2086,28 @@ class MapService { ); const newFeatureId = `${featureType}-${createdFeatureData.id}`; - // @ts-ignore + const displayName = featureType === "route" - ? // @ts-ignore - createdFeatureData.route_number - : // @ts-ignore - createdFeatureData.name; + ? createdFeatureData.route_number + : createdFeatureData.name; if (featureType === "route") { - // @ts-ignore const routeData = createdFeatureData as ApiRoute; const projection = this.map.getView().getProjection(); - // Update existing line feature feature.setId(newFeatureId); feature.set("name", displayName); - // Optionally update geometry if server modified it const lineGeom = new LineString( routeData.path.map((c) => transform([c[1], c[0]], "EPSG:4326", projection) ) ); feature.setGeometry(lineGeom); - - // Не создаем прокси-точку для маршрута - только линия } else { - // For points: update existing feature.setId(newFeatureId); feature.set("name", displayName); - // No need to remove and re-add since it's already in the source } this.updateFeaturesInReact(); @@ -2233,7 +2129,6 @@ class MapService { } } -// --- MAP CONTROLS COMPONENT --- interface MapControlsProps { mapService: MapService | null; activeMode: string; @@ -2331,7 +2226,6 @@ const MapControls: React.FC = ({ import { observer } from "mobx-react-lite"; -// --- MAP SIGHTBAR COMPONENT --- interface MapSightbarProps { mapService: MapService | null; mapFeatures: Feature[]; @@ -2364,7 +2258,6 @@ const MapSightbar: React.FC = observer( [mapFeatures] ); - // Создаем объединенный список всех объектов для поиска const allFeatures = useMemo(() => { const stations = mapStore.filteredStations.map((station) => { const feature = new Feature({ @@ -2437,7 +2330,6 @@ const MapSightbar: React.FC = observer( const ctrlKey = event?.ctrlKey || event?.metaKey; if (ctrlKey) { - // Множественный выбор: добавляем к существующему const newSet = new Set(selectedIds); if (newSet.has(id)) { newSet.delete(id); @@ -2447,7 +2339,6 @@ const MapSightbar: React.FC = observer( setSelectedIds(newSet); mapService.setSelectedIds(newSet); } else { - // Одиночный выбор: используем стандартный метод mapService.selectFeature(id); } }, @@ -2505,32 +2396,19 @@ const MapSightbar: React.FC = observer( if (isNaN(numericRouteId)) return; const isHidden = mapStore.hiddenRoutes.has(numericRouteId); - console.log( - `[handleHideRoute] Route ${numericRouteId}, isHidden: ${isHidden}` - ); try { if (isHidden) { - console.log(`[handleHideRoute] Showing route ${numericRouteId}`); - // Показываем маршрут обратно const route = mapStore.routes.find((r) => r.id === numericRouteId); if (!route) { - console.warn( - `[handleHideRoute] Route ${numericRouteId} not found in mapStore` - ); return; } const projection = mapService.getMap()?.getView().getProjection(); if (!projection) { - console.error(`[handleHideRoute] Failed to get map projection`); return; } - console.log( - `[handleHideRoute] Route ${numericRouteId} (${route.route_number}) found, showing` - ); - // Показываем сам маршрут const coordinates = route.path .filter((c) => c && c[0] != null && c[1] != null) .map((c: [number, number]) => @@ -2546,33 +2424,19 @@ const MapSightbar: React.FC = observer( lineFeature.setId(routeId); lineFeature.set("featureType", "route"); mapService.lineSource.addFeature(lineFeature); - console.log(`[handleHideRoute] Added route line to map`); } else { - console.warn( - `[handleHideRoute] No valid coordinates for route ${numericRouteId}` - ); } - // Получаем станции текущего маршрута из кэша const routeStationIds = mapStore.routeStationsCache.get(numericRouteId) || []; - console.log( - `[handleHideRoute] Route ${numericRouteId} has ${routeStationIds.length} stations` - ); - // Получаем все маршруты для проверки const allRouteIds = mapStore.routes.map((r) => r.id); - // Исключаем скрытые маршруты из проверки const visibleRouteIds = allRouteIds.filter( (id: number) => id !== numericRouteId && !mapStore.hiddenRoutes.has(id) ); - console.log( - `[handleHideRoute] Checking against ${visibleRouteIds.length} visible routes (excluding hidden)` - ); - // Собираем все станции видимых маршрутов из кэша const stationsInVisibleRoutes = new Set(); visibleRouteIds.forEach((otherRouteId) => { const stationIds = @@ -2582,16 +2446,10 @@ const MapSightbar: React.FC = observer( ); }); - console.log( - `[handleHideRoute] Found ${stationsInVisibleRoutes.size} unique stations in visible routes` - ); - - // Показываем станции, которые не используются в других ВИДИМЫХ маршрутах const stationsToShow = routeStationIds.filter( (id: number) => !stationsInVisibleRoutes.has(id) ); - // Показываем станции на карте for (const stationId of stationsToShow) { const station = mapStore.stations.find((s) => s.id === stationId); if (!station) continue; @@ -2610,7 +2468,6 @@ const MapSightbar: React.FC = observer( feature.setId(`station-${station.id}`); feature.set("featureType", "station"); - // Добавляем станцию только если её еще нет на карте const existingFeature = mapService.pointSource.getFeatureById( `station-${station.id}` ); @@ -2619,36 +2476,19 @@ const MapSightbar: React.FC = observer( } } - // Удаляем из скрытых mapStore.hiddenRoutes.delete(numericRouteId); - saveHiddenRoutes(mapStore.hiddenRoutes); // Сохраняем в localStorage - console.log( - `[handleHideRoute] Removed ${numericRouteId} from hiddenRoutes, stations to show: ${stationsToShow.length}` - ); + saveHiddenRoutes(mapStore.hiddenRoutes); } else { - // Скрываем маршрут - console.log(`[handleHideRoute] Hiding route ${numericRouteId}`); - - // Получаем станции текущего маршрута из кэша const routeStationIds = mapStore.routeStationsCache.get(numericRouteId) || []; - console.log( - `[handleHideRoute] Route ${numericRouteId} has ${routeStationIds.length} stations` - ); - // Получаем все маршруты для проверки const allRouteIds = mapStore.routes.map((r) => r.id); - // Исключаем скрытые маршруты из проверки const visibleRouteIds = allRouteIds.filter( (id: number) => id !== numericRouteId && !mapStore.hiddenRoutes.has(id) ); - console.log( - `[handleHideRoute] Checking against ${visibleRouteIds.length} visible routes (excluding hidden)` - ); - // Собираем все станции видимых маршрутов из кэша const stationsInVisibleRoutes = new Set(); visibleRouteIds.forEach((otherRouteId) => { const stationIds = @@ -2658,16 +2498,10 @@ const MapSightbar: React.FC = observer( ); }); - console.log( - `[handleHideRoute] Found ${stationsInVisibleRoutes.size} unique stations in visible routes` - ); - - // Скрываем станции, которые не используются в других ВИДИМЫХ маршрутах const stationsToHide = routeStationIds.filter( (id: number) => !stationsInVisibleRoutes.has(id) ); - // Скрываем станции с карты stationsToHide.forEach((stationId: number) => { const pointFeature = mapService.pointSource.getFeatureById( `station-${stationId}` @@ -2679,7 +2513,6 @@ const MapSightbar: React.FC = observer( } }); - // Скрываем сам маршрут с карты const lineFeature = mapService.lineSource.getFeatureById(routeId); if (lineFeature) { mapService.lineSource.removeFeature( @@ -2687,15 +2520,10 @@ const MapSightbar: React.FC = observer( ); } - // Добавляем в скрытые mapStore.hiddenRoutes.add(numericRouteId); - saveHiddenRoutes(mapStore.hiddenRoutes); // Сохраняем в localStorage - console.log( - `[handleHideRoute] Added ${numericRouteId} to hiddenRoutes, stations to hide: ${stationsToHide.length}` - ); + saveHiddenRoutes(mapStore.hiddenRoutes); } - // Снимаем выделение mapService.unselect(); } catch (error) { console.error( @@ -2801,12 +2629,11 @@ const MapSightbar: React.FC = observer( {features.length > 0 ? ( features.map((feature) => { const fId = feature.getId(); - if (fId === undefined) return null; // TypeScript-safe + if (fId === undefined) return null; const fName = (feature.get("name") as string) || "Без названия"; const isSelected = selectedFeature?.getId() === fId; const isChecked = selectedIds.has(fId); - // Проверяем, скрыт ли маршрут const numericRouteId = featureType === "route" ? parseInt(String(fId).split("-")[1], 10) @@ -3146,7 +2973,7 @@ const MapSightbar: React.FC = observer( ); } ); -// --- MAP PAGE COMPONENT --- + export const MapPage: React.FC = observer(() => { const mapRef = useRef(null); const tooltipRef = useRef(null); @@ -3177,7 +3004,6 @@ export const MapPage: React.FC = observer(() => { const handleFeatureSelectForSidebar = useCallback( (feat: Feature | null) => { - // Logic to sync sidebar selection with map setSelectedFeatureForSidebar(feat); if (feat) { const featureType = feat.get("featureType"); @@ -3239,7 +3065,6 @@ export const MapPage: React.FC = observer(() => { ); setMapServiceInstance(service); - // Делаем mapServiceInstance доступным глобально для сброса кешей if (typeof window !== "undefined") { (window as any).mapServiceInstance = service; } @@ -3257,15 +3082,12 @@ export const MapPage: React.FC = observer(() => { service?.destroy(); setMapServiceInstance(null); - // Удаляем глобальную ссылку if (typeof window !== "undefined") { delete (window as any).mapServiceInstance; } }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // --- ИСПРАВЛЕНИЕ: Этот хук отвечает ТОЛЬКО за клики по ПУСТОМУ месту useEffect(() => { const olMap = mapServiceInstance?.getMap(); if (!olMap || !mapServiceInstance) return; @@ -3280,11 +3102,9 @@ export const MapPage: React.FC = observer(() => { hitTolerance: 5, }); - // Если клик был НЕ по объекту, снимаем выделение if (!hit) { mapServiceInstance.unselect(); } - // Если клик был ПО объекту, НИЧЕГО не делаем. За это отвечает selectInteraction. }; olMap.on("click", handleMapClickForDeselect); @@ -3337,14 +3157,11 @@ export const MapPage: React.FC = observer(() => { saveActiveSection(activeSectionFromParent); }, [activeSectionFromParent]); - // Перезагружаем данные при изменении города useEffect(() => { if (mapServiceInstance && !isDataLoading) { - // Очищаем текущие объекты на карте mapServiceInstance.pointSource.clear(); mapServiceInstance.lineSource.clear(); - // Загружаем новые данные с фильтрацией по городу mapServiceInstance.loadFeaturesFromApi( mapStore.stations, mapStore.routes, @@ -3353,14 +3170,11 @@ export const MapPage: React.FC = observer(() => { } }, [selectedCityId, mapServiceInstance, isDataLoading]); - // Перезагружаем данные при изменении настройки скрытия достопримечательностей useEffect(() => { if (mapServiceInstance && !isDataLoading) { - // Очищаем текущие объекты на карте mapServiceInstance.pointSource.clear(); mapServiceInstance.lineSource.clear(); - // Загружаем новые данные с учетом фильтрации достопримечательностей mapServiceInstance.loadFeaturesFromApi( mapStore.stations, mapStore.routes, diff --git a/src/pages/MapPage/mapStore.ts b/src/pages/MapPage/mapStore.ts index 91ad7eb..273925f 100644 --- a/src/pages/MapPage/mapStore.ts +++ b/src/pages/MapPage/mapStore.ts @@ -22,10 +22,8 @@ interface ApiSight { longitude: number; } -// Допуск для сравнения координат, чтобы избежать ошибок с точностью чисел. const COORDINATE_PRECISION_TOLERANCE = 1e-9; -// Вспомогательная функция, обновленная для сравнения с допуском. const arePathsEqual = ( path1: [number, number][], path2: [number, number][] @@ -136,7 +134,6 @@ class MapStore { longitude: geometry.coordinates[0], }; - // ИЗМЕНЕНИЕ: Сравнение координат с допуском if ( originalStation.name !== currentStation.name || Math.abs(originalStation.latitude - currentStation.latitude) > @@ -155,7 +152,6 @@ class MapStore { path: geometry.coordinates, }; - // ИЗМЕНЕНИЕ: Используется новая функция arePathsEqual с допуском if ( originalRoute.route_number !== currentRoute.route_number || !arePathsEqual(originalRoute.path, currentRoute.path) @@ -173,7 +169,6 @@ class MapStore { longitude: geometry.coordinates[0], }; - // ИЗМЕНЕНИЕ: Сравнение координат с допуском if ( originalSight.name !== currentSight.name || originalSight.description !== currentSight.description || diff --git a/src/pages/Media/MediaEditPage/index.tsx b/src/pages/Media/MediaEditPage/index.tsx index 1f4b6e7..26237f3 100644 --- a/src/pages/Media/MediaEditPage/index.tsx +++ b/src/pages/Media/MediaEditPage/index.tsx @@ -33,7 +33,7 @@ export const MediaEditPage = observer(() => { const fileInputRef = useRef(null); const [newFile, setNewFile] = useState(null); - const [uploadDialogOpen, setUploadDialogOpen] = useState(false); // State for the upload dialog + const [uploadDialogOpen, setUploadDialogOpen] = useState(false); const media = id ? mediaStore.media.find((m) => m.id === id) : null; const [mediaName, setMediaName] = useState(media?.media_name ?? ""); @@ -55,51 +55,27 @@ export const MediaEditPage = observer(() => { setMediaFilename(media.filename); setMediaType(media.media_type); - // Set available media types based on current file extension const extension = media.filename.split(".").pop()?.toLowerCase(); if (extension) { if (["glb", "gltf"].includes(extension)) { - setAvailableMediaTypes([6]); // 3D model + setAvailableMediaTypes([6]); } else if ( ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes( extension ) ) { - setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama + setAvailableMediaTypes([1, 3, 4, 5]); } else if (["mp4", "webm", "mov"].includes(extension)) { - setAvailableMediaTypes([2]); // Video + setAvailableMediaTypes([2]); } } } }, [media]); useEffect(() => { - // Устанавливаем русский язык при загрузке страницы languageStore.setLanguage("ru"); }, []); - // const handleDrop = (e: DragEvent) => { - // e.preventDefault(); - // e.stopPropagation(); - // setIsDragging(false); - - // const files = Array.from(e.dataTransfer.files); - // if (files.length > 0) { - // setNewFile(files[0]); - // setMediaFilename(files[0].name); - // setUploadDialogOpen(true); // Open dialog on file drop - // } - // }; - - // const handleDragOver = (e: DragEvent) => { - // e.preventDefault(); - // setIsDragging(true); - // }; - - // const handleDragLeave = () => { - // setIsDragging(false); - // }; - const handleFileSelect = (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { @@ -107,26 +83,25 @@ export const MediaEditPage = observer(() => { setNewFile(file); setMediaFilename(file.name); - // Determine media type based on file extension const extension = file.name.split(".").pop()?.toLowerCase(); if (extension) { if (["glb", "gltf"].includes(extension)) { - setAvailableMediaTypes([6]); // 3D model + setAvailableMediaTypes([6]); setMediaType(6); } else if ( ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes( extension ) ) { - setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama - setMediaType(1); // Default to Photo + setAvailableMediaTypes([1, 3, 4, 5]); + setMediaType(1); } else if (["mp4", "webm", "mov"].includes(extension)) { - setAvailableMediaTypes([2]); // Video + setAvailableMediaTypes([2]); setMediaType(2); } } - setUploadDialogOpen(true); // Open dialog on file selection + setUploadDialogOpen(true); } }; @@ -143,11 +118,6 @@ export const MediaEditPage = observer(() => { type: mediaType, }); - // If a new file was selected, the actual file upload will happen - // via the UploadMediaDialog. We just need to make sure the metadata - // is updated correctly before or after. - // Since the dialog handles the actual upload, we don't call updateMediaFile here. - setSuccess(true); handleUploadSuccess(); } catch (err) { @@ -158,17 +128,15 @@ export const MediaEditPage = observer(() => { }; const handleUploadSuccess = () => { - // After successful upload in the dialog, refresh media data if needed if (id) { mediaStore.getOneMedia(id); } - setNewFile(null); // Clear the new file state after successful upload + setNewFile(null); setUploadDialogOpen(false); setSuccess(true); }; if (!media && id) { - // Only show loading if an ID is present and media is not yet loaded return ( diff --git a/src/pages/Route/LinekedStations.tsx b/src/pages/Route/LinekedStations.tsx index 40fa24b..1ff13b1 100644 --- a/src/pages/Route/LinekedStations.tsx +++ b/src/pages/Route/LinekedStations.tsx @@ -42,7 +42,6 @@ import { } from "@shared"; import { EditStationModal } from "../../widgets/modals/EditStationModal"; -// Helper function to insert an item at a specific position (1-based index) function insertAtPosition(arr: T[], pos: number, value: T): T[] { const index = pos - 1; const result = [...arr]; @@ -54,7 +53,6 @@ function insertAtPosition(arr: T[], pos: number, value: T): T[] { return result; } -// Helper function to reorder items after drag and drop const reorder = (list: T[], startIndex: number, endIndex: number): T[] => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); @@ -152,13 +150,11 @@ const LinkedItemsContentsInner = < const availableItems = allItems .filter((item) => !linkedItems.some((linked) => linked.id === item.id)) .filter((item) => { - // Если направление маршрута не указано, показываем все станции if (routeDirection === undefined) return true; - // Фильтруем станции по направлению маршрута + return item.direction === routeDirection; }) .filter((item) => { - // Фильтруем по городу из навбара const selectedCityId = selectedCityStore.selectedCityId; if (selectedCityId && "city_id" in item) { return item.city_id === selectedCityId; @@ -167,7 +163,6 @@ const LinkedItemsContentsInner = < }) .sort((a, b) => a.name.localeCompare(b.name)); - // Фильтрация по поиску для массового режима const filteredAvailableItems = availableItems.filter((item) => { if (!searchQuery.trim()) return true; return String(item.name).toLowerCase().includes(searchQuery.toLowerCase()); diff --git a/src/pages/Route/RouteCreatePage/index.tsx b/src/pages/Route/RouteCreatePage/index.tsx index 1f21c9f..f8317cc 100644 --- a/src/pages/Route/RouteCreatePage/index.tsx +++ b/src/pages/Route/RouteCreatePage/index.tsx @@ -113,7 +113,6 @@ export const RouteCreatePage = observer(() => { const handleArticleSelect = (articleId: number) => { setGovernorAppeal(articleId.toString()); setIsSelectArticleDialogOpen(false); - // Обновляем список статей после создания новой articlesStore.getArticleList(); }; @@ -155,7 +154,6 @@ export const RouteCreatePage = observer(() => { try { setIsLoading(true); - // Валидация обязательных полей if (!routeName.trim()) { toast.error("Заполните название маршрута"); setIsLoading(false); @@ -189,10 +187,9 @@ export const RouteCreatePage = observer(() => { return; } - // Валидация масштабов const scale_min = scaleMin ? Number(scaleMin) : null; const scale_max = scaleMax ? Number(scaleMax) : null; - console.log(scale_min, scale_max); + if ( scale_min === 0 || scale_max === 0 || @@ -215,7 +212,6 @@ export const RouteCreatePage = observer(() => { return; } - // Преобразуем значения в нужные типы const carrier_id = Number(carrier); const governor_appeal = Number(governorAppeal); const rotate = turn ? Number(turn) : undefined; @@ -223,7 +219,6 @@ export const RouteCreatePage = observer(() => { const center_longitude = centerLng ? Number(centerLng) : undefined; const route_direction = direction === "forward"; - // Координаты маршрута как массив массивов чисел const path = routeCoords .trim() .split("\n") @@ -235,7 +230,6 @@ export const RouteCreatePage = observer(() => { return [lat, lon]; }); - // Собираем объект маршрута const newRoute: Partial = { carrier: carrierStore.carriers[ @@ -268,7 +262,6 @@ export const RouteCreatePage = observer(() => { } }; - // Получаем название выбранной статьи для отображения const selectedArticle = articlesStore.articleList.ru.data.find( (article) => article.id === Number(governorAppeal) ); @@ -429,7 +422,6 @@ export const RouteCreatePage = observer(() => { onChange={(e) => { const value = e.target.value; setScaleMin(value); - // Если максимальный масштаб стал меньше минимального, обновляем его if (value && scaleMax && Number(value) > Number(scaleMax)) { setScaleMax(value); } diff --git a/src/pages/Route/route-preview/InfiniteCanvas.tsx b/src/pages/Route/route-preview/InfiniteCanvas.tsx index 7925d12..a31317c 100644 --- a/src/pages/Route/route-preview/InfiniteCanvas.tsx +++ b/src/pages/Route/route-preview/InfiniteCanvas.tsx @@ -47,10 +47,8 @@ export function InfiniteCanvas({ const [startPosition, setStartPosition] = useState({ x: 0, y: 0 }); const [isPointerDown, setIsPointerDown] = useState(false); - // Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута const [isUserInteracting, setIsUserInteracting] = useState(false); - // Реф для отслеживания последнего значения originalRouteData?.rotate const lastOriginalRotation = useRef(undefined); useEffect(() => { @@ -68,7 +66,7 @@ export function InfiniteCanvas({ const handlePointerDown = (e: FederatedMouseEvent) => { setIsPointerDown(true); setIsDragging(false); - setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя + setIsUserInteracting(true); setStartPosition({ x: position.x, y: position.y, @@ -81,13 +79,9 @@ export function InfiniteCanvas({ e.stopPropagation(); }; - // Устанавливаем rotation только при изменении originalRouteData и отсутствии взаимодействия пользователя useEffect(() => { const newRotation = originalRouteData?.rotate ?? 0; - // Обновляем rotation только если: - // 1. Пользователь не взаимодействует с канвасом - // 2. Значение действительно изменилось if (!isUserInteracting && lastOriginalRotation.current !== newRotation) { setRotation((newRotation * Math.PI) / 180); lastOriginalRotation.current = newRotation; @@ -97,7 +91,6 @@ export function InfiniteCanvas({ const handlePointerMove = (e: FederatedMouseEvent) => { if (!isPointerDown) return; - // Проверяем, началось ли перетаскивание if (!isDragging) { const dx = e.globalX - startMousePosition.x; const dy = e.globalY - startMousePosition.y; @@ -119,10 +112,8 @@ export function InfiniteCanvas({ e.globalX - center.x ); - // Calculate rotation difference in radians const rotationDiff = currentAngle - startAngle; - // Update rotation setRotation(startRotation + rotationDiff); const cosDelta = Math.cos(rotationDiff); @@ -149,15 +140,13 @@ export function InfiniteCanvas({ }; const handlePointerUp = (e: FederatedMouseEvent) => { - // Если не было перетаскивания, то это простой клик - закрываем виджет if (!isDragging) { setSelectedSight(undefined); } setIsPointerDown(false); setIsDragging(false); - // Сбрасываем флаг взаимодействия через небольшую задержку - // чтобы избежать немедленного срабатывания useEffect + setTimeout(() => { setIsUserInteracting(false); }, 100); @@ -166,29 +155,25 @@ export function InfiniteCanvas({ const handleWheel = (e: FederatedWheelEvent) => { e.stopPropagation(); - setIsUserInteracting(true); // Устанавливаем флаг при зуме + setIsUserInteracting(true); - // Get mouse position relative to canvas const mouseX = e.globalX - position.x; const mouseY = e.globalY - position.y; - // Calculate new scale const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR; const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR; - const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in + const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor)); const actualZoomFactor = newScale / scale; if (scale === newScale) { - // Сбрасываем флаг, если зум не изменился setTimeout(() => { setIsUserInteracting(false); }, 100); return; } - // Update position to zoom towards mouse cursor setPosition({ x: position.x + mouseX * (1 - actualZoomFactor), y: position.y + mouseY * (1 - actualZoomFactor), @@ -196,7 +181,6 @@ export function InfiniteCanvas({ setScale(newScale); - // Сбрасываем флаг взаимодействия через задержку setTimeout(() => { setIsUserInteracting(false); }, 100); diff --git a/src/pages/Route/route-preview/MapDataContext.tsx b/src/pages/Route/route-preview/MapDataContext.tsx index 1e1f7b1..dfe7de7 100644 --- a/src/pages/Route/route-preview/MapDataContext.tsx +++ b/src/pages/Route/route-preview/MapDataContext.tsx @@ -141,7 +141,6 @@ export const MapDataProvider = observer( }, [routeId]); useEffect(() => { - // combine changes with original data if (originalRouteData) setRouteData({ ...originalRouteData, ...routeChanges }); if (originalSightData) setSightData(originalSightData); diff --git a/src/pages/Route/route-preview/RightSidebar.tsx b/src/pages/Route/route-preview/RightSidebar.tsx index 3694561..dd09d29 100644 --- a/src/pages/Route/route-preview/RightSidebar.tsx +++ b/src/pages/Route/route-preview/RightSidebar.tsx @@ -37,11 +37,9 @@ export function RightSidebar() { useEffect(() => { if (originalRouteData) { - // Проверяем и сбрасываем минимальный масштаб если нужно const originalMinScale = originalRouteData.scale_min ?? 1; const resetMinScale = originalMinScale < 1 ? 1 : originalMinScale; - // Проверяем и сбрасываем максимальный масштаб если нужно const originalMaxScale = originalRouteData.scale_max ?? 5; const resetMaxScale = originalMaxScale < 3 ? 3 : originalMaxScale; @@ -130,7 +128,6 @@ export function RightSidebar() { onChange={(e) => { let newMinScale = Number(e.target.value); - // Сбрасываем к 1 если меньше if (newMinScale < 1) { newMinScale = 1; } @@ -139,10 +136,10 @@ export function RightSidebar() { if (maxScale - newMinScale < 2) { let newMaxScale = newMinScale + 2; - // Сбрасываем максимальный к 3 если меньше минимального + if (newMaxScale < 3) { newMaxScale = 3; - setMinScale(1); // Сбрасываем минимальный к 1 + setMinScale(1); } setMaxScale(newMaxScale); } @@ -175,7 +172,6 @@ export function RightSidebar() { onChange={(e) => { let newMaxScale = Number(e.target.value); - // Сбрасываем к 3 если меньше минимального if (newMaxScale < 3) { newMaxScale = 3; } @@ -184,10 +180,10 @@ export function RightSidebar() { if (newMaxScale - minScale < 2) { let newMinScale = newMaxScale - 2; - // Сбрасываем минимальный к 1 если меньше + if (newMinScale < 1) { newMinScale = 1; - setMaxScale(3); // Сбрасываем максимальный к минимальному значению + setMaxScale(3); } setMinScale(newMinScale); } diff --git a/src/pages/Route/route-preview/Station.tsx b/src/pages/Route/route-preview/Station.tsx index 1e51b67..41fd367 100644 --- a/src/pages/Route/route-preview/Station.tsx +++ b/src/pages/Route/route-preview/Station.tsx @@ -2,7 +2,6 @@ import { FederatedMouseEvent, Graphics } from "pixi.js"; import { useCallback, useState, useEffect, useRef, FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; -// --- Заглушки для зависимостей (замените на ваши реальные импорты) --- import { BACKGROUND_COLOR, PATH_COLOR, @@ -15,22 +14,16 @@ import { StationData } from "./types"; import { useMapData } from "./MapDataContext"; import { coordinatesToLocal } from "./utils"; import { languageStore } from "@shared"; -// --- Конец заглушек --- -// --- Декларации для react-pixi --- -// (В реальном проекте типы должны быть предоставлены библиотекой @pixi/react-pixi) declare const pixiContainer: any; declare const pixiGraphics: any; declare const pixiText: any; -// --- Типы --- type HorizontalAlign = "left" | "center" | "right"; type VerticalAlign = "top" | "center" | "bottom"; type TextAlign = HorizontalAlign | `${HorizontalAlign} ${VerticalAlign}`; type LabelAlign = "left" | "center" | "right"; -// --- Утилиты --- - /** * Преобразует текстовое позиционирование в anchor координаты. */ @@ -39,8 +32,6 @@ type LabelAlign = "left" | "center" | "right"; * Получает координату anchor.x из типа выравнивания. */ -// --- Интерфейсы пропсов --- - interface StationProps { station: StationData; ruLabel: string | null; @@ -83,10 +74,6 @@ const getAnchorFromOffset = ( return { x: (1 - nx) / 2, y: (1 - ny) / 2 }; }; -// ========================================================================= -// Компонент: Панель управления выравниванием в стиле УрФУ -// ========================================================================= - const LabelAlignmentControl: FC = ({ scale, currentAlign, @@ -107,7 +94,6 @@ const LabelAlignmentControl: FC = ({ (g: Graphics) => { g.clear(); - // Основной фон с градиентом g.roundRect( -controlWidth / 2, 0, @@ -115,9 +101,8 @@ const LabelAlignmentControl: FC = ({ controlHeight, borderRadius ); - g.fill({ color: "#1a1a1a" }); // Темный фон как у УрФУ + g.fill({ color: "#1a1a1a" }); - // Тонкая рамка g.roundRect( -controlWidth / 2, 0, @@ -127,7 +112,6 @@ const LabelAlignmentControl: FC = ({ ); g.stroke({ color: "#333333", width: strokeWidth }); - // Разделители между кнопками for (let i = 1; i < 3; i++) { const x = -controlWidth / 2 + buttonWidth * i; g.moveTo(x, strokeWidth); @@ -151,7 +135,7 @@ const LabelAlignmentControl: FC = ({ controlHeight - strokeWidth * 2, borderRadius / 2 ); - g.fill({ color: "#0066cc", alpha: 0.8 }); // Синий акцент УрФУ + g.fill({ color: "#0066cc", alpha: 0.8 }); } }, [controlWidth, controlHeight, buttonWidth, strokeWidth, borderRadius] @@ -230,10 +214,6 @@ const LabelAlignmentControl: FC = ({ ); }; -// ========================================================================= -// Компонент: Метка Станции (с логикой) -// ========================================================================= - const StationLabel = observer( ({ station, @@ -274,48 +254,45 @@ const StationLabel = observer( hideTimer.current = null; } setIsHovered(true); - onTextHover?.(true); // Call the callback to indicate text is hovered + onTextHover?.(true); }; const handleControlPointerEnter = () => { - // Дополнительная обработка для панели управления if (hideTimer.current) { clearTimeout(hideTimer.current); hideTimer.current = null; } setIsControlHovered(true); setIsHovered(true); - onTextHover?.(true); // Call the callback to indicate text/control is hovered + onTextHover?.(true); }; const handleControlPointerLeave = () => { setIsControlHovered(false); - // Если курсор не над основным контейнером, скрываем панель через некоторое время + if (!isHovered) { hideTimer.current = setTimeout(() => { setIsHovered(false); - onTextHover?.(false); // Call the callback to indicate neither text nor control is hovered + onTextHover?.(false); }, 0); } }; const handlePointerLeave = () => { - // Увеличиваем время до скрытия панели и добавляем проверку hideTimer.current = setTimeout(() => { setIsHovered(false); - // Если курсор не над панелью управления, скрываем и её + if (!isControlHovered) { setIsControlHovered(false); } - onTextHover?.(false); // Call the callback to indicate text is no longer hovered - }, 100); // Увеличиваем время до скрытия панели + onTextHover?.(false); + }, 100); }; useEffect(() => { setPosition({ x: station.offset_x ?? 0, y: station.offset_y ?? 0 }); }, [station.offset_x, station.offset_y, station.id]); - // Функция для конвертации числового align в строковый const convertNumericAlign = (align: number): LabelAlign => { switch (align) { case 0: @@ -329,7 +306,6 @@ const StationLabel = observer( } }; - // Функция для конвертации строкового align в числовой const convertStringAlign = (align: LabelAlign): number => { switch (align) { case "left": @@ -353,7 +329,6 @@ const StationLabel = observer( const compensatedRuFontSize = (26 * 0.75) / scale; const compensatedNameFontSize = (16 * 0.75) / scale; - // Измеряем ширину верхнего лейбла useEffect(() => { if (ruLabelRef.current && ruLabel) { setRuLabelWidth(ruLabelRef.current.width); @@ -386,7 +361,6 @@ const StationLabel = observer( y: dragStartPos.current.y + dy_screen, }; - // Проверяем, изменилась ли позиция if ( Math.abs(newPosition.x - position.x) > 0.01 || Math.abs(newPosition.y - position.y) > 0.01 @@ -406,7 +380,7 @@ const StationLabel = observer( const handleAlignChange = async (align: LabelAlign) => { setCurrentLabelAlign(align); onLabelAlignChange?.(align); - // Сохраняем в стор + const numericAlign = convertStringAlign(align); setStationAlign(station.id, numericAlign); }; @@ -416,34 +390,29 @@ const StationLabel = observer( [position.x, position.y] ); - // Функция для расчета позиции нижнего лейбла относительно ширины верхнего const getSecondLabelPosition = (): number => { if (!ruLabelWidth) return 0; switch (currentLabelAlign) { case "left": - // Позиционируем относительно левого края верхнего текста return -ruLabelWidth / 2; case "center": - // Центрируем относительно центра верхнего текста return 0; case "right": - // Позиционируем относительно правого края верхнего текста return ruLabelWidth / 2; default: return 0; } }; - // Функция для расчета anchor нижнего лейбла const getSecondLabelAnchor = (): number => { switch (currentLabelAlign) { case "left": - return 0; // anchor.x = 0 (левый край) + return 0; case "center": - return 0.5; // anchor.x = 0.5 (центр) + return 0.5; case "right": - return 1; // anchor.x = 1 (правый край) + return 1; default: return 0.5; } @@ -522,10 +491,6 @@ const StationLabel = observer( } ); -// ========================================================================= -// Главный экспортируемый компонент: Станция -// ========================================================================= - export const Station = ({ station, ruLabel, @@ -548,10 +513,9 @@ export const Station = ({ g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, radius); - // Change fill color when text is hovered if (isTextHovered) { - g.fill({ color: 0x00aaff }); // Highlight color when hovered - g.stroke({ color: 0xffffff, width: strokeWidth + 1 }); // Brighter outline when hovered + g.fill({ color: 0x00aaff }); + g.stroke({ color: 0xffffff, width: strokeWidth + 1 }); } else { g.fill({ color: PATH_COLOR }); g.stroke({ color: BACKGROUND_COLOR, width: strokeWidth }); diff --git a/src/pages/Route/route-preview/TransformContext.tsx b/src/pages/Route/route-preview/TransformContext.tsx index 8655b18..7ee2ac5 100644 --- a/src/pages/Route/route-preview/TransformContext.tsx +++ b/src/pages/Route/route-preview/TransformContext.tsx @@ -50,7 +50,6 @@ const TransformContext = createContext<{ setScaleAtCenter: () => {}, }); -// Provider component export const TransformProvider = ({ children }: { children: ReactNode }) => { const [position, setPosition] = useState({ x: 0, y: 0 }); const [scale, setScale] = useState(1); @@ -59,12 +58,10 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => { const screenToLocal = useCallback( (screenX: number, screenY: number) => { - // Translate point relative to current pan position const translatedX = (screenX - position.x) / scale; const translatedY = (screenY - position.y) / scale; - // Rotate point around center - const cosRotation = Math.cos(-rotation); // Negative rotation to reverse transform + const cosRotation = Math.cos(-rotation); const sinRotation = Math.sin(-rotation); const rotatedX = translatedX * cosRotation - translatedY * sinRotation; const rotatedY = translatedX * sinRotation + translatedY * cosRotation; @@ -77,7 +74,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => { [position.x, position.y, scale, rotation] ); - // Inverse of screenToLocal const localToScreen = useCallback( (localX: number, localY: number) => { const upscaledX = localX * UP_SCALE; @@ -120,7 +116,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => { (currentFromPosition.x - center.x) * sinDelta, }; - // Update both rotation and position in a single batch to avoid stale closure setRotation(to); setPosition(newPosition); }, @@ -150,13 +145,11 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => { const cosRot = Math.cos(selectedRotation); const sinRot = Math.sin(selectedRotation); - // Translate point relative to center, rotate, then translate back const dx = newPosition.x; const dy = newPosition.y; newPosition.x = dx * cosRot - dy * sinRot + center.x; newPosition.y = dx * sinRot + dy * cosRot + center.y; - // Batch state updates to avoid intermediate renders setPosition(newPosition); setRotation(selectedRotation); setScale(selectedScale); @@ -184,7 +177,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => { ); const setScaleOnly = useCallback((newScale: number) => { - // Изменяем только масштаб, не трогая позицию и поворот setScale(newScale); }, []); @@ -237,7 +229,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => { ); }; -// Custom hook for easy access to transform values export const useTransform = () => { const context = useContext(TransformContext); if (!context) { diff --git a/src/pages/Route/route-preview/web-gl/web-gl-version.tsx b/src/pages/Route/route-preview/web-gl/web-gl-version.tsx index 2597ced..8c4190b 100644 --- a/src/pages/Route/route-preview/web-gl/web-gl-version.tsx +++ b/src/pages/Route/route-preview/web-gl/web-gl-version.tsx @@ -53,13 +53,11 @@ export const WebGLMap = observer(() => { const cameraAnimationStore = useCameraAnimationStore(); - // Ref для хранения ограничений масштаба const scaleLimitsRef = useRef({ min: null as number | null, max: null as number | null, }); - // Обновляем ограничения масштаба при изменении routeData useEffect(() => { if ( routeData?.scale_min !== undefined && @@ -72,7 +70,6 @@ export const WebGLMap = observer(() => { } }, [routeData?.scale_min, routeData?.scale_max]); - // Функция для ограничения масштаба значениями с бекенда const clampScale = useCallback((value: number) => { const { min, max } = scaleLimitsRef.current; @@ -90,7 +87,6 @@ export const WebGLMap = observer(() => { const setPositionRef = useRef(setPosition); const setScaleRef = useRef(setScale); - // Обновляем refs при изменении функций useEffect(() => { setPositionRef.current = setPosition; }, [setPosition]); @@ -99,7 +95,6 @@ export const WebGLMap = observer(() => { setScaleRef.current = setScale; }, [setScale]); - // Логирование данных маршрута для отладки useEffect(() => { if (routeData) { } @@ -124,7 +119,6 @@ export const WebGLMap = observer(() => { setPositionImmediate: setYellowDotPositionImmediate, } = useAnimatedPolarPosition(0, 0, 800); - // Build transformed route path (map coords) const routePath = useMemo(() => { if (!routeData?.path || routeData?.path.length === 0) return new Float32Array(); @@ -180,7 +174,6 @@ export const WebGLMap = observer(() => { rotationAngle, ]); - // Настройка CameraAnimationStore callback - только один раз при монтировании useEffect(() => { const callback = (newPos: { x: number; y: number }, newZoom: number) => { setPosition(newPos); @@ -189,15 +182,13 @@ export const WebGLMap = observer(() => { cameraAnimationStore.setUpdateCallback(callback); - // Синхронизируем начальное состояние только один раз cameraAnimationStore.syncState(position, scale); return () => { cameraAnimationStore.setUpdateCallback(null); }; - }, []); // Пустой массив - выполняется только при монтировании + }, []); - // Установка границ зума useEffect(() => { if ( routeData?.scale_min !== undefined && @@ -208,28 +199,23 @@ export const WebGLMap = observer(() => { } }, [routeData?.scale_min, routeData?.scale_max, cameraAnimationStore]); - // Автоматический режим - таймер для включения через 5 секунд бездействия useEffect(() => { const interval = setInterval(() => { const timeSinceActivity = Date.now() - userActivityTimestamp; if (timeSinceActivity >= 5000 && !isAutoMode) { - // 5 секунд бездействия - включаем авто режим setIsAutoMode(true); } - }, 1000); // Проверяем каждую секунду + }, 1000); return () => clearInterval(interval); }, [userActivityTimestamp, isAutoMode, setIsAutoMode]); - // Следование за желтой точкой с зумом при включенном авто режиме useEffect(() => { - // Пропускаем обновление если анимация уже идет if (cameraAnimationStore.isActivelyAnimating) { return; } if (isAutoMode && transformedTramCoords && screenCenter) { - // Преобразуем станции в формат для CameraAnimationStore const transformedStations = stationData ? stationData .map((station: any) => { @@ -270,10 +256,8 @@ export const WebGLMap = observer(() => { cameraAnimationStore.setMaxZoom(scaleLimitsRef.current!.max); cameraAnimationStore.setMinZoom(scaleLimitsRef.current!.min); - // Синхронизируем текущее состояние камеры перед запуском анимации cameraAnimationStore.syncState(positionRef.current, scaleRef.current); - // Запускаем анимацию к желтой точке cameraAnimationStore.followTram( transformedTramCoords, screenCenter, @@ -293,7 +277,6 @@ export const WebGLMap = observer(() => { rotationAngle, ]); - // Station label overlay positions (DOM overlay) const stationLabels = useMemo(() => { if (!stationData || !routeData) return [] as Array<{ x: number; y: number; name: string; sub?: string }>; @@ -356,7 +339,6 @@ export const WebGLMap = observer(() => { selectedLanguage as any, ]); - // Build transformed stations (map coords) const stationPoints = useMemo(() => { if (!stationData || !routeData) return new Float32Array(); const centerLat = routeData.center_latitude; @@ -386,7 +368,6 @@ export const WebGLMap = observer(() => { rotationAngle, ]); - // Build transformed sights (map coords) const sightPoints = useMemo(() => { if (!sightData || !routeData) return new Float32Array(); const centerLat = routeData.center_latitude; @@ -530,8 +511,6 @@ export const WebGLMap = observer(() => { const handleResize = () => { const changed = resizeCanvasToDisplaySize(canvas); if (!gl) return; - // Update screen center when canvas size changes - // Use physical pixels (canvas.width) instead of CSS pixels setScreenCenter({ x: canvas.width / 2, y: canvas.height / 2, @@ -567,7 +546,6 @@ export const WebGLMap = observer(() => { const rx = x * cos - y * sin; const ry = x * sin + y * cos; - // В авторежиме используем анимацию, иначе мгновенное обновление if (isAutoMode) { animateYellowDotTo(rx, ry); } else { @@ -666,21 +644,18 @@ export const WebGLMap = observer(() => { const vertexCount = routePath.length / 2; if (vertexCount > 1) { - // Generate thick line geometry using triangles with proper joins const generateThickLine = (points: Float32Array, width: number) => { const vertices: number[] = []; const halfWidth = width / 2; if (points.length < 4) return new Float32Array(); - // Process each segment for (let i = 0; i < points.length - 2; i += 2) { const x1 = points[i]; const y1 = points[i + 1]; const x2 = points[i + 2]; const y2 = points[i + 3]; - // Calculate perpendicular vector const dx = x2 - x1; const dy = y2 - y1; const length = Math.sqrt(dx * dx + dy * dy); @@ -689,18 +664,14 @@ export const WebGLMap = observer(() => { const perpX = (-dy / length) * halfWidth; const perpY = (dx / length) * halfWidth; - // Create quad (two triangles) for this line segment - // Triangle 1 vertices.push(x1 + perpX, y1 + perpY); vertices.push(x1 - perpX, y1 - perpY); vertices.push(x2 + perpX, y2 + perpY); - // Triangle 2 vertices.push(x1 - perpX, y1 - perpY); vertices.push(x2 - perpX, y2 - perpY); vertices.push(x2 + perpX, y2 + perpY); - // Add simple join triangles to fill gaps if (i < points.length - 4) { const x3 = points[i + 4]; const y3 = points[i + 5]; @@ -712,7 +683,6 @@ export const WebGLMap = observer(() => { const perpX2 = (-dy2 / length2) * halfWidth; const perpY2 = (dx2 / length2) * halfWidth; - // Simple join - just connect the endpoints vertices.push(x2 + perpX, y2 + perpY); vertices.push(x2 - perpX, y2 - perpY); vertices.push(x2 + perpX2, y2 + perpY2); @@ -734,22 +704,18 @@ export const WebGLMap = observer(() => { gl.uniform4f(uniforms.u_color, r1, g1, b1, 1); if (tramSegIndex >= 0) { - // Используем точную позицию желтой точки для определения конца красной линии const animatedPos = animatedYellowDotPosition; if ( animatedPos && animatedPos.x !== undefined && animatedPos.y !== undefined ) { - // Создаем массив точек от начала маршрута до позиции желтой точки const passedPoints: number[] = []; - // Добавляем все точки до текущего сегмента for (let i = 0; i <= tramSegIndex; i++) { passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]); } - // Добавляем точную позицию желтой точки как конечную точку passedPoints.push(animatedPos.x, animatedPos.y); if (passedPoints.length >= 4) { @@ -768,7 +734,6 @@ export const WebGLMap = observer(() => { const b2 = (UNPASSED_STATION_COLOR & 0xff) / 255; gl.uniform4f(uniforms.u_color, r2, g2, b2, 1); - // Серая линия начинается точно от позиции желтой точки const animatedPos = animatedYellowDotPosition; if ( animatedPos && @@ -777,10 +742,8 @@ export const WebGLMap = observer(() => { ) { const unpassedPoints: number[] = []; - // Добавляем позицию желтой точки как начальную точку серой линии unpassedPoints.push(animatedPos.x, animatedPos.y); - // Добавляем все точки после текущего сегмента for (let i = tramSegIndex + 1; i < vertexCount; i++) { unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]); } @@ -796,7 +759,6 @@ export const WebGLMap = observer(() => { } } - // Draw stations if (stationPoints.length > 0) { gl.useProgram(pprog); const a_pos_pts = gl.getAttribLocation(pprog, "a_pos"); @@ -814,7 +776,6 @@ export const WebGLMap = observer(() => { gl.enableVertexAttribArray(a_pos_pts); gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0); - // Draw station outlines (black background) gl.uniform1f(u_pointSize, 10 * scale * 1.5); const r_outline = ((BACKGROUND_COLOR >> 16) & 0xff) / 255; const g_outline = ((BACKGROUND_COLOR >> 8) & 0xff) / 255; @@ -822,15 +783,12 @@ export const WebGLMap = observer(() => { gl.uniform4f(u_color_pts, r_outline, g_outline, b_outline, 1); gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2); - // Draw station cores (colored based on passed/unpassed) gl.uniform1f(u_pointSize, 8.0 * scale * 1.5); - // Draw passed stations (red) if (tramSegIndex >= 0) { const passedStations = []; for (let i = 0; i < stationData.length; i++) { if (i <= tramSegIndex) { - // @ts-ignore passedStations.push(stationPoints[i * 2], stationPoints[i * 2 + 1]); } } @@ -848,13 +806,11 @@ export const WebGLMap = observer(() => { } } - // Draw unpassed stations (gray) if (tramSegIndex >= 0) { const unpassedStations = []; for (let i = 0; i < stationData.length; i++) { if (i > tramSegIndex) { unpassedStations.push( - // @ts-ignore stationPoints[i * 2], stationPoints[i * 2 + 1] ); @@ -873,7 +829,6 @@ export const WebGLMap = observer(() => { gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2); } } else { - // If no tram position, draw all stations as unpassed const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255; const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255; const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255; @@ -1015,7 +970,6 @@ export const WebGLMap = observer(() => { if (passedStations.length) gl.drawArrays(gl.POINTS, 0, passedStations.length / 2); - // Draw black dots with white outline for terminal stations (startStopId and endStopId) - 5x larger if ( stationData && stationData.length > 0 && @@ -1028,7 +982,6 @@ export const WebGLMap = observer(() => { const cos = Math.cos(rotationAngle); const sin = Math.sin(rotationAngle); - // Find terminal stations using startStopId and endStopId from context const startStationData = stationData.find( (station) => station.id.toString() === apiStore.context?.startStopId ); @@ -1038,7 +991,6 @@ export const WebGLMap = observer(() => { const terminalStations: number[] = []; - // Transform start station coordinates if found if (startStationData) { const startLocal = coordinatesToLocal( startStationData.latitude - centerLat, @@ -1051,7 +1003,6 @@ export const WebGLMap = observer(() => { terminalStations.push(startRx, startRy); } - // Transform end station coordinates if found if (endStationData) { const endLocal = coordinatesToLocal( endStationData.latitude - centerLat, @@ -1065,12 +1016,10 @@ export const WebGLMap = observer(() => { } if (terminalStations.length > 0) { - // Determine if each terminal station is passed const terminalStationData: any[] = []; if (startStationData) terminalStationData.push(startStationData); if (endStationData) terminalStationData.push(endStationData); - // Get tram segment index for comparison let tramSegIndex = -1; const coords: any = apiStore?.context?.currentCoordinates; if (coords && centerLat !== undefined && centerLon !== undefined) { @@ -1085,7 +1034,6 @@ export const WebGLMap = observer(() => { const tx = wx * cosR - wy * sinR; const ty = wx * sinR + wy * cosR; - // Find closest segment to tram position let best = -1; let bestD = Infinity; for (let i = 0; i < routePath.length - 2; i += 2) { @@ -1110,7 +1058,6 @@ export const WebGLMap = observer(() => { tramSegIndex = best; } - // Check if each terminal station is passed const isStartPassed = startStationData ? (() => { const sx = terminalStations[0]; @@ -1186,46 +1133,41 @@ export const WebGLMap = observer(() => { gl.enableVertexAttribArray(a_pos_pts); gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0); - // Draw colored outline based on passed status - 24 pixels (x2) gl.uniform1f(u_pointSize, 18.0 * scale); if (startStationData && endStationData) { - // Both stations - draw each with its own color if (isStartPassed) { - gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных + gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); } else { - gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных + gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); } - gl.drawArrays(gl.POINTS, 0, 1); // Draw start station + gl.drawArrays(gl.POINTS, 0, 1); if (isEndPassed) { - gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных + gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); } else { - gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных + gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); } - gl.drawArrays(gl.POINTS, 1, 1); // Draw end station + gl.drawArrays(gl.POINTS, 1, 1); } else { - // Single station - use appropriate color const isPassed = startStationData ? isStartPassed : isEndPassed; if (isPassed) { - gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных + gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); } else { - gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных + gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); } gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2); } - // Draw dark center - 12 pixels (x2) gl.uniform1f(u_pointSize, 11.0 * scale); const r_center = ((BACKGROUND_COLOR >> 16) & 0xff) / 255; const g_center = ((BACKGROUND_COLOR >> 8) & 0xff) / 255; const b_center = (BACKGROUND_COLOR & 0xff) / 255; - gl.uniform4f(u_color_pts, r_center, g_center, b_center, 1.0); // Dark color + gl.uniform4f(u_color_pts, r_center, g_center, b_center, 1.0); gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2); } } } - // Draw yellow dot for tram position if (animatedYellowDotPosition) { const rx = animatedYellowDotPosition.x; const ry = animatedYellowDotPosition.y; @@ -1327,7 +1269,6 @@ export const WebGLMap = observer(() => { }); const onPointerDown = (e: PointerEvent) => { - // Отслеживаем активность пользователя updateUserActivity(); if (isAutoMode) { setIsAutoMode(false); @@ -1360,7 +1301,6 @@ export const WebGLMap = observer(() => { const onPointerMove = (e: PointerEvent) => { if (!activePointers.has(e.pointerId)) return; - // Отслеживаем активность пользователя updateUserActivity(); const rect = canvas.getBoundingClientRect(); @@ -1386,7 +1326,6 @@ export const WebGLMap = observer(() => { }; } - // Process the pinch gesture if (pinchStart) { const currentDistance = getDistance(p1, p2); const zoomFactor = currentDistance / pinchStart.distance; @@ -1405,7 +1344,6 @@ export const WebGLMap = observer(() => { } else if (isDragging && activePointers.size === 1) { const p = Array.from(activePointers.values())[0]; - // Проверяем валидность значений if ( !startMouse || !startPos || @@ -1433,7 +1371,6 @@ export const WebGLMap = observer(() => { }; const onPointerUp = (e: PointerEvent) => { - // Отслеживаем активность пользователя updateUserActivity(); canvas.releasePointerCapture(e.pointerId); @@ -1453,7 +1390,6 @@ export const WebGLMap = observer(() => { }; const onPointerCancel = (e: PointerEvent) => { - // Handle pointer cancellation (e.g., when touch is interrupted) updateUserActivity(); canvas.releasePointerCapture(e.pointerId); activePointers.delete(e.pointerId); @@ -1467,7 +1403,6 @@ export const WebGLMap = observer(() => { const onWheel = (e: WheelEvent) => { e.preventDefault(); - // Отслеживаем активность пользователя updateUserActivity(); if (isAutoMode) { setIsAutoMode(false); @@ -1475,7 +1410,6 @@ export const WebGLMap = observer(() => { cameraAnimationStore.stopAnimation(); const rect = canvas.getBoundingClientRect(); - // Convert mouse coordinates from CSS pixels to physical canvas pixels const mouseX = (e.clientX - rect.left) * (canvas.width / canvas.clientWidth); const mouseY = @@ -1582,7 +1516,6 @@ export const WebGLMap = observer(() => { const sy = (ry * scale + position.y) / dpr; const size = 30; - // Обработчик клика для выбора достопримечательности const handleSightClick = () => { const { setSelectedSightId, diff --git a/src/pages/Station/StationCreatePage/index.tsx b/src/pages/Station/StationCreatePage/index.tsx index 94f1864..09d85d5 100644 --- a/src/pages/Station/StationCreatePage/index.tsx +++ b/src/pages/Station/StationCreatePage/index.tsx @@ -35,7 +35,7 @@ export const StationCreatePage = observer(() => { const { cities, getCities } = cityStore; const { selectedCityId, selectedCity } = useSelectedCity(); const [coordinates, setCoordinates] = useState(""); - // НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА + const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false); useEffect(() => { @@ -49,7 +49,6 @@ export const StationCreatePage = observer(() => { } }, [createStationData.common.latitude, createStationData.common.longitude]); - // НОВАЯ ФУНКЦИЯ: Фактическое создание (вызывается после подтверждения) const executeCreate = async () => { try { setIsLoading(true); @@ -64,11 +63,13 @@ export const StationCreatePage = observer(() => { } }; - // ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или создания const handleCreate = async () => { const isCityMissing = !createStationData.common.city_id; - // Проверяем названия на всех языках - const isNameMissing = !createStationData.ru.name || !createStationData.en.name || !createStationData.zh.name; + + const isNameMissing = + !createStationData.ru.name || + !createStationData.en.name || + !createStationData.zh.name; if (isCityMissing || isNameMissing) { setIsSaveWarningOpen(true); @@ -78,13 +79,11 @@ export const StationCreatePage = observer(() => { await executeCreate(); }; - // Обработчик "Да" в предупреждающем окне const handleConfirmCreate = async () => { setIsSaveWarningOpen(false); await executeCreate(); }; - // Обработчик "Нет" в предупреждающем окне const handleCancelCreate = () => { setIsSaveWarningOpen(false); }; @@ -99,7 +98,6 @@ export const StationCreatePage = observer(() => { fetchCities(); }, []); - // Автоматически устанавливаем выбранный город при загрузке страницы useEffect(() => { if (selectedCityId && selectedCity && !createStationData.common.city_id) { setCreateCommonData({ @@ -237,7 +235,7 @@ export const StationCreatePage = observer(() => { className="w-min flex gap-2 items-center" startIcon={} onClick={handleCreate} - disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleCreate + disabled={isLoading} > {isLoading ? ( diff --git a/src/pages/Station/StationEditPage/index.tsx b/src/pages/Station/StationEditPage/index.tsx index 03ba50e..69d3275 100644 --- a/src/pages/Station/StationEditPage/index.tsx +++ b/src/pages/Station/StationEditPage/index.tsx @@ -32,7 +32,7 @@ export const StationEditPage = observer(() => { } = stationsStore; const { cities, getCities } = cityStore; const [coordinates, setCoordinates] = useState(""); - // НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА + const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false); useEffect(() => { @@ -50,7 +50,6 @@ export const StationEditPage = observer(() => { } }, [editStationData.common.latitude, editStationData.common.longitude]); - // НОВАЯ ФУНКЦИЯ: Фактическое редактирование (вызывается после подтверждения) const executeEdit = async () => { try { setIsLoading(true); @@ -64,10 +63,9 @@ export const StationEditPage = observer(() => { } }; - // ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или редактирования const handleEdit = async () => { const isCityMissing = !editStationData.common.city_id; - // Проверяем названия на всех языках + const isNameMissing = !editStationData.ru.name || !editStationData.en.name || @@ -81,13 +79,11 @@ export const StationEditPage = observer(() => { await executeEdit(); }; - // Обработчик "Да" в предупреждающем окне const handleConfirmEdit = async () => { setIsSaveWarningOpen(false); await executeEdit(); }; - // Обработчик "Нет" в предупреждающем окне const handleCancelEdit = () => { setIsSaveWarningOpen(false); }; @@ -243,7 +239,7 @@ export const StationEditPage = observer(() => { className="w-min flex gap-2 items-center" startIcon={} onClick={handleEdit} - disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleEdit + disabled={isLoading} > {isLoading ? ( diff --git a/src/shared/config/constants.tsx b/src/shared/config/constants.tsx index 4c9a8fb..9edef16 100644 --- a/src/shared/config/constants.tsx +++ b/src/shared/config/constants.tsx @@ -8,13 +8,10 @@ import { Earth, Landmark, GitBranch, - // Car, Table, Split, - // Newspaper, PersonStanding, Cpu, - // BookImage, } from "lucide-react"; import carrierIcon from "./carrier.svg"; @@ -57,12 +54,6 @@ export const NAVIGATION_ITEMS: { path: "/devices", for_admin: true, }, - // { - // id: "vehicles", - // label: "Транспорт", - // icon: Car, - // path: "/vehicle", - // }, { id: "users", label: "Пользователи", @@ -75,18 +66,6 @@ export const NAVIGATION_ITEMS: { label: "Справочник", icon: Table, nestedItems: [ - // { - // id: "media", - // label: "Медиа", - // icon: BookImage, - // path: "/media", - // }, - // { - // id: "articles", - // label: "Статьи", - // icon: Newspaper, - // path: "/article", - // }, { id: "attractions", label: "Достопримечательности", @@ -124,7 +103,7 @@ export const NAVIGATION_ITEMS: { id: "carriers", label: "Перевозчики", // @ts-ignore - icon: () => Перевозчики, + icon: () => Перевозчики, path: "/carrier", for_admin: true, }, diff --git a/src/shared/lib/gltfCacheManager.ts b/src/shared/lib/gltfCacheManager.ts index 6550252..f9d0b66 100644 --- a/src/shared/lib/gltfCacheManager.ts +++ b/src/shared/lib/gltfCacheManager.ts @@ -1,8 +1,3 @@ -/** - * Утилита для управления кешем GLTF и blob URL - */ - -// Динамический импорт useGLTF для избежания проблем с SSR let useGLTF: any = null; const initializeUseGLTF = async () => { @@ -20,9 +15,6 @@ const initializeUseGLTF = async () => { return useGLTF; }; -/** - * Очищает кеш GLTF для конкретного URL - */ export const clearGLTFCacheForUrl = async (url: string) => { try { const gltf = await initializeUseGLTF(); @@ -32,9 +24,6 @@ export const clearGLTFCacheForUrl = async (url: string) => { } catch (error) {} }; -/** - * Очищает весь кеш GLTF - */ export const clearAllGLTFCache = async () => { try { const gltf = await initializeUseGLTF(); @@ -44,9 +33,6 @@ export const clearAllGLTFCache = async () => { } catch (error) {} }; -/** - * Очищает blob URL из памяти браузера - */ export const revokeBlobURL = (url: string) => { if (url && url.startsWith("blob:")) { try { @@ -55,25 +41,16 @@ export const revokeBlobURL = (url: string) => { } }; -/** - * Комплексная очистка: blob URL + кеш GLTF - */ export const clearBlobAndGLTFCache = async (url: string) => { - // Сначала отзываем blob URL revokeBlobURL(url); - // Затем очищаем кеш GLTF await clearGLTFCacheForUrl(url); }; -/** - * Очистка при смене медиа (для предотвращения конфликтов) - */ export const clearMediaTransitionCache = async ( previousMediaId: string | number | null, newMediaType?: number ) => { - // Если переключаемся с/на 3D модель, очищаем весь кеш if (newMediaType === 6 || previousMediaId) { await clearAllGLTFCache(); } diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index e21c768..a58d0fc 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -2,34 +2,17 @@ export * from "./mui/theme"; export * from "./DecodeJWT"; export * from "./gltfCacheManager"; -/** - * Генерирует название медиа по умолчанию в разных форматах - * - * Примеры использования: - * - Для достопримечательности с названием: "Михайловский замок_mikhail-zamok_Фото" - * - Для достопримечательности без названия: "Название_mikhail-zamok_Фото" - * - Для статьи: "Михайловский замок_mikhail-zamok_Факты" (где "Факты" - название статьи) - * - * @param objectName - Название объекта (достопримечательности, города и т.д.) - * @param fileName - Название файла - * @param mediaType - Тип медиа (число) или название статьи - * @param isArticle - Флаг, указывающий что медиа добавляется к статье - * @returns Строка в нужном формате - */ export const generateDefaultMediaName = ( objectName: string, fileName: string, mediaType: number | string, isArticle: boolean = false ): string => { - // Убираем расширение из названия файла const fileNameWithoutExtension = fileName.split(".").slice(0, -1).join("."); if (isArticle && typeof mediaType === "string") { - // Для статей: "Название достопримечательности_название файла_название статьи" return `${objectName}_${fileNameWithoutExtension}_${mediaType}`; } else if (typeof mediaType === "number") { - // Получаем название типа медиа const mediaTypeLabels: Record = { 1: "Фото", 2: "Видео", @@ -42,14 +25,11 @@ export const generateDefaultMediaName = ( const mediaTypeLabel = mediaTypeLabels[mediaType] || "Медиа"; if (objectName && objectName.trim() !== "") { - // Если есть название объекта: "Название объекта_название файла_тип медиа" return `${objectName}_${fileNameWithoutExtension}_${mediaTypeLabel}`; } else { - // Если нет названия объекта: "Название_название файла_тип медиа" return `Название_${fileNameWithoutExtension}_${mediaTypeLabel}`; } } - // Fallback return `${objectName || "Название"}_${fileNameWithoutExtension}_Медиа`; }; diff --git a/src/shared/modals/ArticleSelectOrCreateDialog/index.tsx b/src/shared/modals/ArticleSelectOrCreateDialog/index.tsx index b100909..e1eee1f 100644 --- a/src/shared/modals/ArticleSelectOrCreateDialog/index.tsx +++ b/src/shared/modals/ArticleSelectOrCreateDialog/index.tsx @@ -523,7 +523,6 @@ export const ArticleSelectOrCreateDialog = observer( article?.service_name?.toLowerCase().includes(searchQuery.toLowerCase()) ); - // Preview-by-click logic with request serialization to avoid concurrent requests const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [queuedPreviewId, setQueuedPreviewId] = useState(null); const clickTimerRef = (typeof window !== "undefined" @@ -551,7 +550,7 @@ export const ArticleSelectOrCreateDialog = observer( if (queuedPreviewId && queuedPreviewId !== articleId) { const nextId = queuedPreviewId; setQueuedPreviewId(null); - // Run the next queued preview + runPreviewFetch(nextId); } else { setQueuedPreviewId(null); @@ -560,7 +559,6 @@ export const ArticleSelectOrCreateDialog = observer( }; const handleListItemClick = (articleId: number) => { - // Delay to allow double-click to cancel preview if (clickTimerRef.current) clearTimeout(clickTimerRef.current); clickTimerRef.current = setTimeout(() => { if (tabValue === 0 && !selectedArticleId && !tempArticleId) { @@ -570,7 +568,6 @@ export const ArticleSelectOrCreateDialog = observer( }; const handleListItemDoubleClick = (articleId: number) => { - // Cancel pending single-click preview and proceed to select if (clickTimerRef.current) { clearTimeout(clickTimerRef.current); (clickTimerRef as any).current = null; diff --git a/src/shared/modals/UploadMediaDialog/index.tsx b/src/shared/modals/UploadMediaDialog/index.tsx index fa9efd4..62e1cdc 100644 --- a/src/shared/modals/UploadMediaDialog/index.tsx +++ b/src/shared/modals/UploadMediaDialog/index.tsx @@ -54,7 +54,7 @@ interface UploadMediaDialogProps { | "station"; isArticle?: boolean; articleName?: string; - initialFile?: File; // <--- добавлено + initialFile?: File; } export const UploadMediaDialog = observer( @@ -68,7 +68,7 @@ export const UploadMediaDialog = observer( isArticle, articleName, - initialFile, // <--- добавлено + initialFile, }: UploadMediaDialogProps) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -87,7 +87,6 @@ export const UploadMediaDialog = observer( useEffect(() => { if (initialFile) { - // Очищаем предыдущий blob URL если он существует if ( previousMediaUrlRef.current && previousMediaUrlRef.current.startsWith("blob:") @@ -106,7 +105,6 @@ export const UploadMediaDialog = observer( } }, [initialFile]); - // Очистка blob URL при размонтировании компонента useEffect(() => { return () => { if ( @@ -116,13 +114,13 @@ export const UploadMediaDialog = observer( clearBlobAndGLTFCache(previousMediaUrlRef.current); } }; - }, []); // Пустой массив зависимостей - выполняется только при размонтировании + }, []); useEffect(() => { if (fileToUpload) { setMediaFile(fileToUpload); setMediaFilename(fileToUpload.name); - // Try to determine media type from file extension + const extension = fileToUpload.name.split(".").pop()?.toLowerCase(); if (extension) { if (["glb", "gltf"].includes(extension)) { @@ -134,22 +132,18 @@ export const UploadMediaDialog = observer( extension ) ) { - // Для изображений доступны все типы кроме видео - setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель - setMediaType(1); // По умолчанию Фото + setAvailableMediaTypes([1, 3, 4, 5]); + setMediaType(1); } else if (["mp4", "webm", "mov"].includes(extension)) { - // Для видео только тип Видео setAvailableMediaTypes([2]); setMediaType(2); } } - // Генерируем название по умолчанию если есть контекст if (fileToUpload.name) { let defaultName = ""; if (isArticle && articleName && contextObjectName) { - // Для статей: "Название достопримечательности_название файла_название статьи" defaultName = generateDefaultMediaName( contextObjectName, fileToUpload.name, @@ -157,10 +151,9 @@ export const UploadMediaDialog = observer( true ); } else if (contextObjectName && contextObjectName.trim() !== "") { - // Для обычных медиа с названием объекта const currentMediaType = hardcodeType ? MEDIA_TYPE_VALUES[hardcodeType] - : 1; // По умолчанию фото + : 1; defaultName = generateDefaultMediaName( contextObjectName, fileToUpload.name, @@ -168,10 +161,9 @@ export const UploadMediaDialog = observer( false ); } else { - // Для медиа без названия объекта const currentMediaType = hardcodeType ? MEDIA_TYPE_VALUES[hardcodeType] - : 1; // По умолчанию фото + : 1; defaultName = generateDefaultMediaName( "", fileToUpload.name, @@ -185,13 +177,11 @@ export const UploadMediaDialog = observer( } }, [fileToUpload, contextObjectName, hardcodeType, isArticle, articleName]); - // Обновляем название при изменении типа медиа useEffect(() => { if (mediaFilename && mediaType > 0) { let defaultName = ""; if (isArticle && articleName && contextObjectName) { - // Для статей: "Название достопримечательности_название файла_название статьи" defaultName = generateDefaultMediaName( contextObjectName, mediaFilename, @@ -199,7 +189,6 @@ export const UploadMediaDialog = observer( true ); } else if (contextObjectName && contextObjectName.trim() !== "") { - // Для обычных медиа с названием объекта const currentMediaType = hardcodeType ? MEDIA_TYPE_VALUES[hardcodeType] : mediaType; @@ -210,7 +199,6 @@ export const UploadMediaDialog = observer( false ); } else { - // Для медиа без названия объекта const currentMediaType = hardcodeType ? MEDIA_TYPE_VALUES[hardcodeType] : mediaType; @@ -235,7 +223,6 @@ export const UploadMediaDialog = observer( useEffect(() => { if (mediaFile) { - // Очищаем предыдущий blob URL и кеш GLTF если он существует if ( previousMediaUrlRef.current && previousMediaUrlRef.current.startsWith("blob:") @@ -245,22 +232,10 @@ export const UploadMediaDialog = observer( const newBlobUrl = URL.createObjectURL(mediaFile as Blob); setMediaUrl(newBlobUrl); - previousMediaUrlRef.current = newBlobUrl; // Сохраняем новый URL в ref - setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла + previousMediaUrlRef.current = newBlobUrl; + setIsPreviewLoaded(false); } - }, [mediaFile]); // Убираем mediaUrl из зависимостей чтобы избежать зацикливания - - // const fileFormat = useEffect(() => { - // const handleKeyPress = (event: KeyboardEvent) => { - // if (event.key.toLowerCase() === "enter" && !event.ctrlKey) { - // event.preventDefault(); - // onClose(); - // } - // }; - - // window.addEventListener("keydown", handleKeyPress); - // return () => window.removeEventListener("keydown", handleKeyPress); - // }, [onClose]); + }, [mediaFile]); const handleSave = async () => { if (!mediaFile) return; @@ -285,10 +260,10 @@ export const UploadMediaDialog = observer( } } setSuccess(true); - // Закрываем модальное окно после успешного сохранения + setTimeout(() => { handleClose(); - }, 1000); // Небольшая задержка, чтобы пользователь увидел сообщение об успехе + }, 1000); } catch (err) { setError(err instanceof Error ? err.message : "Failed to save media"); } finally { @@ -297,7 +272,6 @@ export const UploadMediaDialog = observer( }; const handleClose = () => { - // Очищаем blob URL и кеш GLTF при закрытии диалога if ( previousMediaUrlRef.current && previousMediaUrlRef.current.startsWith("blob:") @@ -310,7 +284,7 @@ export const UploadMediaDialog = observer( setMediaUrl(null); setMediaFile(null); setIsPreviewLoaded(false); - previousMediaUrlRef.current = null; // Очищаем ref + previousMediaUrlRef.current = null; onClose(); }; diff --git a/src/shared/store/CarrierStore/index.tsx b/src/shared/store/CarrierStore/index.tsx index 273a3c4..f0886ba 100644 --- a/src/shared/store/CarrierStore/index.tsx +++ b/src/shared/store/CarrierStore/index.tsx @@ -171,7 +171,6 @@ class CarrierStore { this.carriers[language].data.push(response.data); }); - // Create translations for other languages for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) { const patchPayload = { // @ts-ignore diff --git a/src/shared/store/CreateSightStore/index.tsx b/src/shared/store/CreateSightStore/index.tsx index 9b74aad..762cbe4 100644 --- a/src/shared/store/CreateSightStore/index.tsx +++ b/src/shared/store/CreateSightStore/index.tsx @@ -1,4 +1,3 @@ -// @shared/stores/createSightStore.ts import { articlesStore, Language, @@ -27,7 +26,6 @@ type SightLanguageInfo = { }; type SightCommonInfo = { - // id: number; // ID is 0 until created city_id: number; city: string; latitude: number; @@ -35,13 +33,11 @@ type SightCommonInfo = { thumbnail: string | null; watermark_lu: string | null; watermark_rd: string | null; - left_article: number; // Can be 0 or a real ID, or placeholder like 10000000 + left_article: number; preview_media: string | null; video_preview: string | null; }; -// SightBaseInfo combines common info with language-specific info -// The 'id' for the sight itself will be assigned upon creation by the backend. type SightBaseInfo = SightCommonInfo & { [key in Language]: SightLanguageInfo; }; @@ -78,7 +74,7 @@ const initialSightState: SightBaseInfo = { }; class CreateSightStore { - sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState)); // Deep copy for reset + sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState)); uploadMediaOpen = false; setUploadMediaOpen = (open: boolean) => { @@ -93,9 +89,7 @@ class CreateSightStore { makeAutoObservable(this); } - // --- Right Article Management --- createNewRightArticle = async () => { - // Create article in DB for all languages const articleRuData = { heading: "Новый заголовок (RU)", body: "Новый текст (RU)", @@ -125,7 +119,7 @@ class CreateSightStore { }, }, }); - const { id } = articleRes.data; // New article's ID + const { id } = articleRes.data; runInAction(() => { const newArticleEntry = { id, media: [] }; @@ -133,7 +127,7 @@ class CreateSightStore { this.sight.en.right.push({ ...newArticleEntry, ...articleEnData }); this.sight.zh.right.push({ ...newArticleEntry, ...articleZhData }); }); - return id; // Return ID for potential immediate use + return id; } catch (error) { console.error("Error creating new right article:", error); throw error; @@ -169,7 +163,7 @@ class CreateSightStore { }); }); - return articleId; // Return the linked article ID + return articleId; } catch (error) { console.error("Error linking existing right article:", error); throw error; @@ -188,9 +182,7 @@ class CreateSightStore { } }; - // "Unlink" in create mode means just removing from the list to be created with the sight unlinkRightAritcle = (articleId: number) => { - // Changed from 'unlinkRightAritcle' spelling runInAction(() => { this.sight.ru.right = this.sight.ru.right.filter( (article) => article.id !== articleId @@ -202,16 +194,12 @@ class CreateSightStore { (article) => article.id !== articleId ); }); - // Note: If this article was created via createNewRightArticle, it still exists in the DB. - // Consider if an orphaned article should be deleted here or managed separately. - // For now, it just removes it from the list associated with *this specific sight creation process*. }; deleteRightArticle = async (articleId: number) => { try { - await authInstance.delete(`/article/${articleId}`); // Delete from backend + await authInstance.delete(`/article/${articleId}`); runInAction(() => { - // Remove from local store for all languages this.sight.ru.right = this.sight.ru.right.filter( (article) => article.id !== articleId ); @@ -228,12 +216,11 @@ class CreateSightStore { } }; - // --- Right Article Media Management --- createLinkWithRightArticle = async (media: MediaItem, articleId: number) => { try { await authInstance.post(`/article/${articleId}/media`, { media_id: media.id, - media_order: 1, // Or calculate based on existing media.length + 1 + media_order: 1, }); runInAction(() => { (["ru", "en", "zh"] as Language[]).forEach((lang) => { @@ -242,7 +229,7 @@ class CreateSightStore { ); if (article) { if (!article.media) article.media = []; - article.media.unshift(media); // Add to the beginning + article.media.unshift(media); } }); }); @@ -273,7 +260,6 @@ class CreateSightStore { } }; - // --- Left Article Management (largely unchanged from your provided store) --- updateLeftInfo = (language: Language, heading: string, body: string) => { this.sight[language].left.heading = heading; this.sight[language].left.body = body; @@ -323,7 +309,7 @@ class CreateSightStore { deleteLeftArticle = async (articleId: number) => { /* ... your existing logic ... */ await authInstance.delete(`/article/${articleId}`); - // articlesStore.getArticles(languageStore.language); // If still neede + runInAction(() => { articlesStore.articles.ru = articlesStore.articles.ru.filter( (article) => article.id !== articleId @@ -344,7 +330,6 @@ class CreateSightStore { const enName = (this.sight.en.name || "").trim(); const zhName = (this.sight.zh.name || "").trim(); - // If all names are empty, skip defaulting and use empty headings const hasAnyName = !!(ruName || enName || zhName); const response = await languageInstance("ru").post("/article", { @@ -363,7 +348,7 @@ class CreateSightStore { }); runInAction(() => { - this.sight.left_article = newLeftArticleId; // Store the actual ID + this.sight.left_article = newLeftArticleId; this.sight.ru.left = { heading: hasAnyName ? ruName : "", body: "", @@ -402,9 +387,8 @@ class CreateSightStore { return newLeftArticleId; }; - // Placeholder for a "new" unsaved left article setNewLeftArticlePlaceholder = () => { - this.sight.left_article = 10000000; // Special placeholder ID + this.sight.left_article = 10000000; this.sight.ru.left = { heading: "Новая левая статья", body: "Заполните контентом", @@ -422,7 +406,6 @@ class CreateSightStore { }; }; - // --- Sight Preview Media --- linkPreviewMedia = (mediaId: string) => { this.sight.preview_media = mediaId; }; @@ -431,32 +414,27 @@ class CreateSightStore { this.sight.preview_media = null; }; - // --- General Store Methods --- clearCreateSight = () => { this.needLeaveAgree = false; - this.sight = JSON.parse(JSON.stringify(initialSightState)); // Reset to initial + this.sight = JSON.parse(JSON.stringify(initialSightState)); }; updateSightInfo = ( - content: Partial, // Corrected types + content: Partial, language?: Language ) => { this.needLeaveAgree = true; if (language) { this.sight[language] = { ...this.sight[language], ...content }; } else { - // Assuming content here is for SightCommonInfo this.sight = { ...this.sight, ...(content as Partial) }; } }; - // --- Main Sight Creation Logic --- createSight = async (primaryLanguage: Language) => { let finalLeftArticleId = this.sight.left_article; - // 1. Handle Left Article (Create if new, or use existing ID) if (this.sight.left_article === 10000000) { - // Placeholder for new const res = await languageInstance("ru").post("/article", { heading: this.sight.ru.left.heading, body: this.sight.ru.left.body, @@ -474,7 +452,6 @@ class CreateSightStore { this.sight.left_article !== 0 && this.sight.left_article !== null ) { - // Existing, ensure it's up-to-date await languageInstance("ru").patch( `/article/${this.sight.left_article}`, { heading: this.sight.ru.left.heading, body: this.sight.ru.left.body } @@ -488,10 +465,7 @@ class CreateSightStore { { heading: this.sight.zh.left.heading, body: this.sight.zh.left.body } ); } - // else: left_article is 0, so no left article - // 2. Right articles are already created in DB and their IDs are in this.sight[lang].right. - // We just need to update their content if changed before saving the sight. for (const lang of ["ru", "en", "zh"] as Language[]) { for (const article of this.sight[lang].right) { if (article.id == 0 || article.id == null) { @@ -501,14 +475,12 @@ class CreateSightStore { heading: article.heading, body: article.body, }); - // Media for these articles are already linked via createLinkWithRightArticle } } const rightArticleIdsForLink = this.sight[primaryLanguage].right.map( (a) => a.id ); - // 3. Create Sight object in DB const sightPayload = { city_id: this.sight.city_id, city: this.sight.city, @@ -528,9 +500,8 @@ class CreateSightStore { "/sight", sightPayload ); - const newSightId = response.data.id; // ID of the newly created sight + const newSightId = response.data.id; - // 4. Update other languages for the sight const otherLanguages = (["ru", "en", "zh"] as Language[]).filter( (l) => l !== primaryLanguage ); @@ -551,20 +522,17 @@ class CreateSightStore { }); } - // 5. Link Right Articles to the new Sight for (let i = 0; i < rightArticleIdsForLink.length; i++) { await authInstance.post(`/sight/${newSightId}/article`, { article_id: rightArticleIdsForLink[i], - page_num: i + 1, // Or other logic for page_num + page_num: i + 1, }); } - // Optionally: this.clearCreateSight(); // To reset form after successful creation this.needLeaveAgree = false; return newSightId; }; - // --- Media Upload (Generic, used by dialogs) --- uploadMedia = async ( filename: string, type: number, @@ -583,12 +551,12 @@ class CreateSightStore { this.fileToUpload = null; this.uploadMediaOpen = false; }); - mediaStore.getMedia(); // Refresh global media list + mediaStore.getMedia(); return { id: response.data.id, - filename: filename, // Or response.data.filename if backend returns it - media_name: media_name, // Or response.data.media_name - media_type: type, // Or response.data.type + filename: filename, + media_name: media_name, + media_type: type, }; } catch (error) { console.error("Error uploading media:", error); @@ -596,15 +564,12 @@ class CreateSightStore { } }; - // For Left Article Media createLinkWithLeftArticle = async (media: MediaItem) => { if (!this.sight.left_article || this.sight.left_article === 10000000) { console.warn( "Left article not selected or is a placeholder. Cannot link media yet." ); - // If it's a placeholder, we could store the media temporarily and link it after the article is created. - // For simplicity, we'll assume the article must exist. - // A more robust solution might involve creating the article first if it's a placeholder. + return; } try { @@ -663,7 +628,7 @@ class CreateSightStore { this.sight.ru.right = sortArticles(this.sight.ru.right); this.sight.en.right = sortArticles(this.sight.en.right); - this.sight.zh.right = sortArticles(this.sight.zh.right); // теперь zh тоже сортируется одинаково + this.sight.zh.right = sortArticles(this.sight.zh.right); this.needLeaveAgree = true; }; diff --git a/src/shared/store/EditSightStore/index.tsx b/src/shared/store/EditSightStore/index.tsx index 6a1a6eb..5c6ab82 100644 --- a/src/shared/store/EditSightStore/index.tsx +++ b/src/shared/store/EditSightStore/index.tsx @@ -1,4 +1,3 @@ -// @shared/stores/editSightStore.ts import { articlesStore, authInstance, @@ -96,13 +95,11 @@ class EditSightStore { } runInAction(() => { - // Обновляем языковую часть this.sight[language] = { ...this.sight[language], ...data, }; - // Только при первом запросе обновляем общую часть if (!this.hasLoadedCommon) { this.sight.common = { ...this.sight.common, @@ -123,7 +120,6 @@ class EditSightStore { let responseEn = await languageInstance("en").get(`/sight/${id}/article`); let responseZh = await languageInstance("zh").get(`/sight/${id}/article`); - // Create a map of article IDs to their media const mediaMap = new Map(); for (const article of responseRu.data) { const responseMedia = await authInstance.get( @@ -132,7 +128,6 @@ class EditSightStore { mediaMap.set(article.id, responseMedia.data); } - // Function to add media to articles const addMediaToArticles = (articles: any[]) => { return articles.map((article) => ({ ...article, @@ -327,28 +322,6 @@ class EditSightStore { articles: articleIdsInObject, }); - // await languageInstance("ru").patch( - // `/sight/${this.sight.common.left_article}/article`, - // { - // heading: this.sight.ru.left.heading, - // body: this.sight.ru.left.body, - // } - // ); - // await languageInstance("en").patch( - // `/sight/${this.sight.common.left_article}/article`, - // { - // heading: this.sight.en.left.heading, - // body: this.sight.en.left.body, - // } - // ); - // await languageInstance("zh").patch( - // `/sight/${this.sight.common.left_article}/article`, - // { - // heading: this.sight.zh.left.heading, - // body: this.sight.zh.left.body, - // } - // ); - this.needLeaveAgree = false; }; @@ -589,7 +562,7 @@ class EditSightStore { }); }); - return article_id; // Return the linked article ID + return article_id; }; deleteRightArticleMedia = async (article_id: number, media_id: string) => { @@ -695,7 +668,7 @@ class EditSightStore { }); }); - return id; // Return the ID of the newly created article + return id; }; createLinkWithRightArticle = async ( @@ -770,7 +743,7 @@ class EditSightStore { this.sight.ru.right = sortArticles(this.sight.ru.right); this.sight.en.right = sortArticles(this.sight.en.right); - this.sight.zh.right = sortArticles(this.sight.zh.right); // теперь zh тоже сортируется одинаково + this.sight.zh.right = sortArticles(this.sight.zh.right); this.needLeaveAgree = true; }; diff --git a/src/shared/store/MediaStore/index.tsx b/src/shared/store/MediaStore/index.tsx index 7daeb26..bbbb3bb 100644 --- a/src/shared/store/MediaStore/index.tsx +++ b/src/shared/store/MediaStore/index.tsx @@ -39,12 +39,11 @@ class MediaStore { updateMedia = async (id: string, data: Partial) => { const response = await authInstance.patch(`/media/${id}`, data); runInAction(() => { - // Update in media array const index = this.media.findIndex((m) => m.id === id); if (index !== -1) { this.media[index] = { ...this.media[index], ...response.data }; } - // Update oneMedia if it's the current media being viewed + if (this.oneMedia?.id === id) { this.oneMedia = { ...this.oneMedia, ...response.data }; } @@ -64,12 +63,11 @@ class MediaStore { }); runInAction(() => { - // Update in media array const index = this.media.findIndex((m) => m.id === id); if (index !== -1) { this.media[index] = { ...this.media[index], ...response.data }; } - // Update oneMedia if it's the current media being viewed + if (this.oneMedia?.id === id) { this.oneMedia = { ...this.oneMedia, ...response.data }; } diff --git a/src/shared/store/ModelLoadingStore/index.ts b/src/shared/store/ModelLoadingStore/index.ts index a55906b..81c0e27 100644 --- a/src/shared/store/ModelLoadingStore/index.ts +++ b/src/shared/store/ModelLoadingStore/index.ts @@ -15,7 +15,6 @@ class ModelLoadingStore { makeAutoObservable(this); } - // Начать отслеживание загрузки модели startLoading(modelId: string) { this.loadingStates.set(modelId, { isLoading: true, @@ -25,7 +24,6 @@ class ModelLoadingStore { }); } - // Обновить прогресс загрузки updateProgress(modelId: string, progress: number) { const state = this.loadingStates.get(modelId); if (state) { @@ -33,7 +31,6 @@ class ModelLoadingStore { } } - // Завершить загрузку модели finishLoading(modelId: string) { const state = this.loadingStates.get(modelId); if (state) { @@ -42,12 +39,10 @@ class ModelLoadingStore { } } - // Остановить загрузку (в случае ошибки) stopLoading(modelId: string) { this.loadingStates.delete(modelId); } - // Обработать ошибку загрузки handleError(modelId: string, error?: string) { const state = this.loadingStates.get(modelId); if (state) { @@ -56,26 +51,22 @@ class ModelLoadingStore { } } - // Получить состояние загрузки для конкретной модели getLoadingState(modelId: string): ModelLoadingState | undefined { return this.loadingStates.get(modelId); } - // Проверить, загружается ли какая-либо модель get isAnyModelLoading(): boolean { return Array.from(this.loadingStates.values()).some( (state) => state.isLoading ); } - // Получить все загружающиеся модели get loadingModels(): ModelLoadingState[] { return Array.from(this.loadingStates.values()).filter( (state) => state.isLoading ); } - // Получить общий прогресс всех загружающихся моделей get overallProgress(): number { const loadingModels = this.loadingModels; if (loadingModels.length === 0) return 100; @@ -87,12 +78,10 @@ class ModelLoadingStore { return Math.round(totalProgress / loadingModels.length); } - // Проверить, заблокировано ли сохранение (есть ли загружающиеся модели) get isSaveBlocked(): boolean { return this.isAnyModelLoading; } - // Очистить все состояния загрузки clearAll() { this.loadingStates.clear(); } diff --git a/src/shared/store/SightsStore/index.tsx b/src/shared/store/SightsStore/index.tsx index 8515ae5..d4dc4f0 100644 --- a/src/shared/store/SightsStore/index.tsx +++ b/src/shared/store/SightsStore/index.tsx @@ -58,41 +58,6 @@ class SightsStore { }); }; - // getSight = async (id: number) => { - // const response = await authInstance.get(`/sight/${id}`); - - // runInAction(() => { - // this.sight = response.data; - // editSightStore.sightInfo = { - // ...editSightStore.sightInfo, - // id: response.data.id, - // city_id: response.data.city_id, - // city: response.data.city, - // latitude: response.data.latitude, - // longitude: response.data.longitude, - // thumbnail: response.data.thumbnail, - // watermark_lu: response.data.watermark_lu, - // watermark_rd: response.data.watermark_rd, - // left_article: response.data.left_article, - // preview_media: response.data.preview_media, - // video_preview: response.data.video_preview, - - // [languageStore.language]: { - // info: { - // name: response.data.name, - // address: response.data.address, - // }, - // left: { - // heading: articlesStore.articles[languageStore.language].find( - // (article) => article.id === response.data.left_article - // )?.heading, - // body: articlesStore.articles[languageStore.language].find( - // }, - // }, - // }; - // }); - // }; - createSightAction = async ( city: number, coordinates: { latitude: number; longitude: number } @@ -167,16 +132,12 @@ class SightsStore { common: boolean ) => { if (common) { - // @ts-ignore this.sight!.common = { - // @ts-ignore ...this.sight!.common, ...content, }; } else { - // @ts-ignore this.sight![language] = { - // @ts-ignore ...this.sight![language], ...content, }; diff --git a/src/shared/store/SnapshotStore/index.ts b/src/shared/store/SnapshotStore/index.ts index 41cc382..ca70e34 100644 --- a/src/shared/store/SnapshotStore/index.ts +++ b/src/shared/store/SnapshotStore/index.ts @@ -1,8 +1,6 @@ import { authInstance } from "@shared"; import { makeAutoObservable, runInAction } from "mobx"; -// Импорт функции сброса кешей карты -// import { clearMapCaches } from "../../pages/MapPage"; import { articlesStore, cityStore, @@ -35,9 +33,7 @@ class SnapshotStore { makeAutoObservable(this); } - // Функция для сброса всех кешей в приложении private clearAllCaches = () => { - // Сброс кешей статей articlesStore.articleList = { ru: { data: [], loaded: false }, en: { data: [], loaded: false }, @@ -47,7 +43,6 @@ class SnapshotStore { articlesStore.articleData = null; articlesStore.articleMedia = null; - // Сброс кешей городов cityStore.cities = { ru: { data: [], loaded: false }, en: { data: [], loaded: false }, @@ -56,21 +51,18 @@ class SnapshotStore { cityStore.ruCities = { data: [], loaded: false }; cityStore.city = {}; - // Сброс кешей стран countryStore.countries = { ru: { data: [], loaded: false }, en: { data: [], loaded: false }, zh: { data: [], loaded: false }, }; - // Сброс кешей перевозчиков carrierStore.carriers = { ru: { data: [], loaded: false }, en: { data: [], loaded: false }, zh: { data: [], loaded: false }, }; - // Сброс кешей станций stationsStore.stationLists = { ru: { data: [], loaded: false }, en: { data: [], loaded: false }, @@ -78,24 +70,18 @@ class SnapshotStore { }; stationsStore.stationPreview = {}; - // Сброс кешей достопримечательностей sightsStore.sights = []; sightsStore.sight = null; - // Сброс кешей маршрутов routeStore.routes = { data: [], loaded: false }; - // Сброс кешей транспорта vehicleStore.vehicles = { data: [], loaded: false }; - // Сброс кешей пользователей userStore.users = { data: [], loaded: false }; - // Сброс кешей медиа mediaStore.media = []; mediaStore.oneMedia = null; - // Сброс кешей создания и редактирования достопримечательностей createSightStore.sight = JSON.parse( JSON.stringify({ city_id: 0, @@ -173,26 +159,21 @@ class SnapshotStore { editSightStore.fileToUpload = null; editSightStore.needLeaveAgree = false; - // Сброс кешей устройств devicesStore.devices = []; devicesStore.uuid = null; devicesStore.sendSnapshotModalOpen = false; - // Сброс кешей авторизации (кроме токена) authStore.payload = null; authStore.error = null; authStore.isLoading = false; - // Сброс кешей карты (если они загружены) try { - // Сбрасываем кеши mapStore если он доступен if (typeof window !== "undefined" && (window as any).mapStore) { (window as any).mapStore.routes = []; (window as any).mapStore.stations = []; (window as any).mapStore.sights = []; } - // Сбрасываем кеши MapService если он доступен if (typeof window !== "undefined" && (window as any).mapServiceInstance) { (window as any).mapServiceInstance.clearCaches(); } @@ -200,7 +181,6 @@ class SnapshotStore { console.warn("Не удалось сбросить кеши карты:", error); } - // Сброс localStorage кешей (кроме токена авторизации) const token = localStorage.getItem("token"); const rememberedEmail = localStorage.getItem("rememberedEmail"); const rememberedPassword = localStorage.getItem("rememberedPassword"); @@ -208,14 +188,12 @@ class SnapshotStore { localStorage.clear(); sessionStorage.clear(); - // Восстанавливаем важные данные if (token) localStorage.setItem("token", token); if (rememberedEmail) localStorage.setItem("rememberedEmail", rememberedEmail); if (rememberedPassword) localStorage.setItem("rememberedPassword", rememberedPassword); - // Сброс кешей карты (если они есть) const mapPositionKey = "mapPosition"; const activeSectionKey = "mapActiveSection"; if (localStorage.getItem(mapPositionKey)) { @@ -225,7 +203,6 @@ class SnapshotStore { localStorage.removeItem(activeSectionKey); } - // Попытка очистить кеш браузера (если поддерживается) if ("caches" in window) { try { caches.keys().then((cacheNames) => { @@ -240,7 +217,6 @@ class SnapshotStore { } } - // Попытка очистить IndexedDB (если поддерживается) if ("indexedDB" in window) { try { indexedDB.databases().then((databases) => { @@ -284,10 +260,8 @@ class SnapshotStore { }; restoreSnapshot = async (id: string) => { - // Сначала сбрасываем все кеши this.clearAllCaches(); - // Затем восстанавливаем снапшот await authInstance.post(`/snapshots/${id}/restore`); }; diff --git a/src/shared/store/StationsStore/index.ts b/src/shared/store/StationsStore/index.ts index 8ad2778..ea7444b 100644 --- a/src/shared/store/StationsStore/index.ts +++ b/src/shared/store/StationsStore/index.ts @@ -7,7 +7,7 @@ type StationLanguageData = { name: string; system_name: string; address: string; - loaded: boolean; // Indicates if this language's data has been loaded/modified + loaded: boolean; }; type StationCommonData = { @@ -92,7 +92,6 @@ class StationsStore { }, }; - // This will store the full station data, keyed by ID and then by language stationPreview: Record< string, Record @@ -264,7 +263,6 @@ class StationsStore { }; }; - // Sets language-specific station data setLanguageEditStationData = ( language: Language, data: Partial @@ -295,7 +293,7 @@ class StationsStore { `/station/${id}`, { name: name || "", - system_name: name || "", // system_name is often derived from name + system_name: name || "", description: description || "", address: address || "", ...commonDataPayload, @@ -303,7 +301,6 @@ class StationsStore { ); runInAction(() => { - // Update the cached preview data and station lists after successful patch if (this.stationPreview[id]) { this.stationPreview[id][language] = { loaded: true, @@ -343,11 +340,11 @@ class StationsStore { runInAction(() => { this.stations = this.stations.filter((station) => station.id !== id); - // Also clear from stationPreview cache + if (this.stationPreview[id]) { delete this.stationPreview[id]; } - // Clear from stationLists as well for all languages + for (const lang of ["ru", "en", "zh"] as const) { if (this.stationLists[lang].data) { this.stationLists[lang].data = this.stationLists[lang].data.filter( @@ -421,12 +418,11 @@ class StationsStore { delete commonDataPayload.icon; } - // First create station in Russian const { name, address } = this.createStationData[language]; const description = this.createStationData.common.description; const response = await languageInstance(language).post("/station", { name: name || "", - system_name: name || "", // system_name is often derived from name + system_name: name || "", description: description || "", address: address || "", ...commonDataPayload, @@ -438,7 +434,6 @@ class StationsStore { const stationId = response.data.id; - // Then update for other languages for (const lang of ["ru", "en", "zh"].filter( (lang) => lang !== language ) as Language[]) { @@ -448,7 +443,7 @@ class StationsStore { `/station/${stationId}`, { name: name || "", - system_name: name || "", // system_name is often derived from name + system_name: name || "", description: description || "", address: address || "", ...commonDataPayload, @@ -507,7 +502,6 @@ class StationsStore { return response.data; }; - // Reset editStationData when navigating away or after saving resetEditStationData = () => { this.editStationData = { ru: { diff --git a/src/widgets/DevicesTable/index.tsx b/src/widgets/DevicesTable/index.tsx index 5cfbcc9..0fb67b3 100644 --- a/src/widgets/DevicesTable/index.tsx +++ b/src/widgets/DevicesTable/index.tsx @@ -11,8 +11,8 @@ import { devicesStore, Modal, snapshotStore, - vehicleStore, // Not directly used in this component's rendering logic anymore -} from "@shared"; // Assuming @shared exports these + vehicleStore, +} from "@shared"; import { useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { Button, Checkbox, Typography } from "@mui/material"; @@ -23,12 +23,10 @@ import { useNavigate } from "react-router-dom"; export type ConnectedDevice = string; interface Snapshot { - ID: string; // Assuming ID is string based on usage + ID: string; Name: string; - // Add other snapshot properties if needed } -// --- HELPER FUNCTIONS --- const formatDate = (dateString: string | null) => { if (!dateString) return "Нет данных"; try { @@ -76,12 +74,7 @@ function createData( }; } -// This function transforms the raw device data (which includes vehicle and device_status) -// into the format expected by the table. It now filters for devices that have a UUID. -const transformDevicesToRows = ( - vehicles: Vehicle[] - // devices: ConnectedDevice[] -): TableRowData[] => { +const transformDevicesToRows = (vehicles: Vehicle[]): TableRowData[] => { return vehicles.map((vehicle) => { const uuid = vehicle.vehicle.uuid; if (!uuid) @@ -115,26 +108,21 @@ export const DevicesTable = observer(() => { } = devicesStore; const { snapshots, getSnapshots } = snapshotStore; - const { getVehicles, vehicles } = vehicleStore; // Removed as devicesStore.devices should be the source of truth + const { getVehicles, vehicles } = vehicleStore; const { devices } = devicesStore; const navigate = useNavigate(); const [selectedDeviceUuids, setSelectedDeviceUuids] = useState([]); - // Transform the raw devices data into rows suitable for the table - // This will also filter out devices without a UUID, as those cannot be acted upon. - const currentTableRows = transformDevicesToRows( - vehicles.data as Vehicle[] - // devices as ConnectedDevice[] - ); + const currentTableRows = transformDevicesToRows(vehicles.data as Vehicle[]); useEffect(() => { const fetchData = async () => { - await getVehicles(); // Not strictly needed if devicesStore.devices is populated correctly by getDevices - await getDevices(); // This should fetch the combined vehicle/device_status data + await getVehicles(); + await getDevices(); await getSnapshots(); }; fetchData(); - }, [getDevices, getSnapshots]); // Added dependencies + }, [getDevices, getSnapshots]); const isAllSelected = currentTableRows.length > 0 && @@ -144,7 +132,6 @@ export const DevicesTable = observer(() => { if (isAllSelected) { setSelectedDeviceUuids([]); } else { - // Select all device UUIDs from the *currently visible and selectable* rows setSelectedDeviceUuids( currentTableRows.map((row) => row.device_uuid ?? "") ); @@ -171,14 +158,13 @@ export const DevicesTable = observer(() => { }; const handleReloadStatus = async (uuid: string) => { - setSelectedDevice(uuid); // Sets the device in the store, useful for context elsewhere + setSelectedDevice(uuid); try { await authInstance.post(`/devices/${uuid}/request-status`); await getVehicles(); - await getDevices(); // Refresh devices to show updated status + await getDevices(); } catch (error) { console.error(`Error requesting status for device ${uuid}:`, error); - // Optionally: show a user-facing error message } }; @@ -200,22 +186,16 @@ export const DevicesTable = observer(() => { } }; try { - // Create an array of promises for all snapshot requests const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => { return send(deviceUuid); }); - // Wait for all promises to settle (either resolve or reject) await Promise.allSettled(snapshotPromises); - // After all requests are attempted - await getDevices(); // Refresh the device list - setSelectedDeviceUuids([]); // Clear the selection - toggleSendSnapshotModal(); // Close the modal + await getDevices(); + setSelectedDeviceUuids([]); + toggleSendSnapshotModal(); } catch (error) { - // This catch block might not be hit if Promise.allSettled is used, - // as it doesn't reject on individual promise failures. - // Individual errors should be handled if needed within the .map or by checking results. console.error("Error in snapshot sending process:", error); } }; @@ -235,7 +215,7 @@ export const DevicesTable = observer(() => {
)} diff --git a/src/widgets/MediaViewer/ThreeView.tsx b/src/widgets/MediaViewer/ThreeView.tsx index 79296b0..699e770 100644 --- a/src/widgets/MediaViewer/ThreeView.tsx +++ b/src/widgets/MediaViewer/ThreeView.tsx @@ -3,11 +3,9 @@ import { OrbitControls, Stage, useGLTF } from "@react-three/drei"; import { useEffect, Suspense } from "react"; import { Box, CircularProgress, Typography } from "@mui/material"; -// Утилита для очистки кеша GLTF const clearGLTFCache = (url?: string) => { try { if (url) { - // Если это blob URL, очищаем его из кеша if (url.startsWith("blob:")) { useGLTF.clear(url); } else { @@ -19,29 +17,23 @@ const clearGLTFCache = (url?: string) => { } }; -// Утилита для проверки типа файла const isValid3DFile = (url: string): boolean => { try { const urlObj = new URL(url); const pathname = urlObj.pathname.toLowerCase(); const searchParams = urlObj.searchParams; - // Проверяем расширение файла в пути const validExtensions = [".glb", ".gltf"]; const hasValidExtension = validExtensions.some((ext) => pathname.endsWith(ext) ); - // Проверяем параметры запроса на наличие типа файла const fileType = searchParams.get("type") || searchParams.get("format"); const hasValidType = fileType && ["glb", "gltf"].includes(fileType.toLowerCase()); - // Если это blob URL, считаем его валидным (пользователь выбрал файл) const isBlobUrl = url.startsWith("blob:"); - // Если это URL с токеном и нет явного расширения, считаем валидным - // (предполагаем что сервер вернет правильный файл) const hasToken = searchParams.has("token"); const isServerUrl = hasToken && !hasValidExtension; @@ -51,7 +43,7 @@ const isValid3DFile = (url: string): boolean => { return isValid; } catch (error) { console.warn("⚠️ isValid3DFile: Ошибка при проверке URL", error); - // В случае ошибки парсинга URL, считаем валидным (пусть useGLTF сам разберется) + return true; } }; @@ -63,13 +55,10 @@ type ModelViewerProps = { }; const Model = ({ fileUrl }: { fileUrl: string }) => { - // Очищаем кеш перед загрузкой новой модели useEffect(() => { - // Очищаем кеш для текущего URL clearGLTFCache(fileUrl); }, [fileUrl]); - // Проверяем валидность файла перед загрузкой (только для blob URL) if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) { console.warn("⚠️ Model: Попытка загрузить невалидный 3D файл", { fileUrl }); throw new Error(`Файл не является корректной 3D моделью: ${fileUrl}`); @@ -114,16 +103,13 @@ export const ThreeView = ({ height = "100%", width = "100%", }: ModelViewerProps) => { - // Проверяем валидность файла (только для blob URL) useEffect(() => { if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) { console.warn("⚠️ ThreeView: Невалидный 3D файл", { fileUrl }); } }, [fileUrl]); - // Очищаем кеш при размонтировании и при смене URL useEffect(() => { - // Очищаем кеш сразу при монтировании компонента clearGLTFCache(fileUrl); return () => { diff --git a/src/widgets/MediaViewer/ThreeViewErrorBoundary.tsx b/src/widgets/MediaViewer/ThreeViewErrorBoundary.tsx index e7885e1..8114f1d 100644 --- a/src/widgets/MediaViewer/ThreeViewErrorBoundary.tsx +++ b/src/widgets/MediaViewer/ThreeViewErrorBoundary.tsx @@ -35,7 +35,6 @@ export class ThreeViewErrorBoundary extends Component { props: Props, state: State ): Partial | null { - // Сбрасываем ошибку ТОЛЬКО при смене медиа (когда меняется ID в resetKey) if ( props.resetKey !== state.lastResetKey && state.lastResetKey !== undefined @@ -43,7 +42,6 @@ export class ThreeViewErrorBoundary extends Component { const oldMediaId = String(state.lastResetKey).split("-")[0]; const newMediaId = String(props.resetKey).split("-")[0]; - // Сбрасываем ошибку только если изменился ID медиа (пользователь выбрал другую модель) if (oldMediaId !== newMediaId) { return { hasError: false, @@ -52,9 +50,6 @@ export class ThreeViewErrorBoundary extends Component { }; } - // Если изменился только счетчик (нажата кнопка "Попробовать снова"), обновляем lastResetKey - // но не сбрасываем ошибку автоматически - ждем результата загрузки - return { lastResetKey: props.resetKey, }; @@ -127,15 +122,12 @@ export class ThreeViewErrorBoundary extends Component { }; handleReset = () => { - // Сначала сбрасываем состояние ошибки this.setState( { hasError: false, error: null, }, () => { - // После того как состояние обновилось, вызываем callback для изменения resetKey - // Это приведет к пересозданию компонента и новой попытке загрузки this.props.onReset?.(); } ); diff --git a/src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx b/src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx index a148144..e69de29 100644 --- a/src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx +++ b/src/widgets/SightTabs/CreateInformationTab/MediaUploadBox.tsx @@ -1,158 +0,0 @@ -// import { Box, Button, Paper, Typography } from "@mui/material"; -// import { X, Upload } from "lucide-react"; -// import { useCallback, useState } from "react"; -// import { useDropzone } from "react-dropzone"; -// import { UploadMediaDialog } from "@shared"; -// import { createSightStore } from "@shared"; - -// interface MediaUploadBoxProps { -// title: string; -// tooltip?: string; -// mediaId: string | null; -// onMediaSelect: (mediaId: string) => void; -// onMediaRemove: () => void; -// onPreviewClick: (mediaId: string) => void; -// token: string; -// type: "thumbnail" | "watermark_lu" | "watermark_rd"; -// } - -// export const MediaUploadBox = ({ -// title, -// tooltip, -// mediaId, -// onMediaSelect, -// onMediaRemove, -// onPreviewClick, -// token, -// type, -// }: MediaUploadBoxProps) => { -// const [uploadMediaOpen, setUploadMediaOpen] = useState(false); -// const [fileToUpload, setFileToUpload] = useState(null); - -// const onDrop = useCallback((acceptedFiles: File[]) => { -// if (acceptedFiles.length > 0) { -// setFileToUpload(acceptedFiles[0]); -// setUploadMediaOpen(true); -// } -// }, []); - -// const { getRootProps, getInputProps, isDragActive } = useDropzone({ -// onDrop, -// accept: { -// "image/*": [".png", ".jpg", ".jpeg", ".gif"], -// }, -// multiple: false, -// }); - -// const handleUploadComplete = async (media: { -// id: string; -// filename: string; -// media_name?: string; -// media_type: number; -// }) => { -// onMediaSelect(media.id); -// }; - -// return ( -// <> -// -// -// -// {title} -// -// -// -// -// {mediaId && ( -// -// )} -// {mediaId ? ( -// {title} { -// e.stopPropagation(); -// onPreviewClick(mediaId); -// }} -// /> -// ) : ( -//
-//
-// -//

-// {isDragActive ? "Отпустите файл здесь" : "Перетащите файл"} -//

-//
-//

или

-// -//
-// )} -//
-//
- -// { -// setUploadMediaOpen(false); -// setFileToUpload(null); -// }} -// afterUpload={handleUploadComplete} -// /> -// -// ); -// }; diff --git a/src/widgets/SightTabs/CreateLeftTab/index.tsx b/src/widgets/SightTabs/CreateLeftTab/index.tsx index f18d1b6..1a1f1bc 100644 --- a/src/widgets/SightTabs/CreateLeftTab/index.tsx +++ b/src/widgets/SightTabs/CreateLeftTab/index.tsx @@ -1,4 +1,3 @@ -// @widgets/LeftWidgetTab.tsx import { Box, Button, TextField, Paper, Typography } from "@mui/material"; import { BackButton, @@ -50,17 +49,6 @@ export const CreateLeftTab = observer( const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - // const handleMediaSelected = useCallback(() => { - // // При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА - // // сохраняя текущие heading и body. - // updateSightInfo(language, { - // left: { - // heading: data.left.heading, - // body: data.left.body, - // }, - // }); - // setIsSelectMediaDialogOpen(false); - // }, [language, data.left.heading, data.left.body]); const handleCloseArticleDialog = useCallback(() => { setIsSelectArticleDialogOpen(false); diff --git a/src/widgets/SightTabs/CreateRightTab/index.tsx b/src/widgets/SightTabs/CreateRightTab/index.tsx index bf60797..d76d2a3 100644 --- a/src/widgets/SightTabs/CreateRightTab/index.tsx +++ b/src/widgets/SightTabs/CreateRightTab/index.tsx @@ -13,28 +13,27 @@ import { languageStore, SelectArticleModal, TabPanel, - SelectMediaDialog, // Import + SelectMediaDialog, UploadMediaDialog, - Media, // Import + Media, } from "@shared"; import { LanguageSwitcher, - MediaArea, // Import - MediaAreaForSight, // Import + MediaArea, + MediaAreaForSight, ReactMarkdownComponent, ReactMarkdownEditor, DeleteModal, } from "@widgets"; -import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react"; // Import X +import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react"; import { observer } from "mobx-react-lite"; -import { useState, useEffect } from "react"; // Added useEffect +import { useState, useEffect } from "react"; import { MediaViewer } from "../../MediaViewer/index"; import { toast } from "react-toastify"; import { authInstance } from "@shared"; import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; type MediaItemShared = { - // Define if not already available from @shared id: string; filename: string; media_name?: string; @@ -52,14 +51,14 @@ export const CreateRightTab = observer( unlinkPreviewMedia, createLinkWithRightArticle, deleteRightArticleMedia, - setFileToUpload, // From store - setUploadMediaOpen, // From store - uploadMediaOpen, // From store - unlinkRightAritcle, // Corrected spelling + setFileToUpload, + setUploadMediaOpen, + uploadMediaOpen, + unlinkRightAritcle, deleteRightArticle, linkExistingRightArticle, createSight, - clearCreateSight, // For resetting form + clearCreateSight, updateRightArticles, } = createSightStore; const { language } = languageStore; @@ -78,7 +77,7 @@ export const CreateRightTab = observer( >(null); const [previewMedia, setPreviewMedia] = useState(null); - // Reset activeArticleIndex if language changes and index is out of bounds + useEffect(() => { if (sight.preview_media) { const fetchMedia = async () => { @@ -97,7 +96,7 @@ export const CreateRightTab = observer( activeArticleIndex >= sight[language].right.length ) { setActiveArticleIndex(null); - setType("media"); // Default back to media preview if selected article disappears + setType("media"); } }, [language, sight[language].right, activeArticleIndex]); @@ -113,10 +112,9 @@ export const CreateRightTab = observer( try { await createSight(language); toast.success("Достопримечательность успешно создана!"); - clearCreateSight(); // Reset form + clearCreateSight(); setActiveArticleIndex(null); setType("media"); - // Potentially navigate away: history.push('/sights-list'); } catch (error) { console.error("Failed to save sight:", error); toast.error("Ошибка при создании достопримечательности."); @@ -132,7 +130,7 @@ export const CreateRightTab = observer( handleCloseMenu(); try { const newArticleId = await createNewRightArticle(); - // Automatically select the new article if ID is returned + const newIndex = sight[language].right.findIndex( (a) => a.id === newArticleId ); @@ -140,7 +138,6 @@ export const CreateRightTab = observer( setActiveArticleIndex(newIndex); setType("article"); } else { - // Fallback if findIndex fails (should not happen if store updates correctly) setActiveArticleIndex(sight[language].right.length - 1); setType("article"); } @@ -156,7 +153,7 @@ export const CreateRightTab = observer( const linkedArticleId = await linkExistingRightArticle( selectedArticleId ); - setSelectArticleDialogOpen(false); // Close dialog + setSelectArticleDialogOpen(false); const newIndex = sight[language].right.findIndex( (a) => a.id === linkedArticleId ); @@ -174,7 +171,6 @@ export const CreateRightTab = observer( ? sight[language].right[activeArticleIndex] : null; - // Media Handling for Dialogs const handleOpenUploadMedia = () => { setUploadMediaOpen(true); }; @@ -203,7 +199,6 @@ export const CreateRightTab = observer( }; const handleMediaUploaded = async (media: MediaItemShared) => { - // After UploadMediaDialog finishes setUploadMediaOpen(false); setFileToUpload(null); if (mediaTarget === "sightPreview") { @@ -211,36 +206,25 @@ export const CreateRightTab = observer( } else if (mediaTarget === "rightArticle" && currentRightArticle) { await createLinkWithRightArticle(media, currentRightArticle.id); } - setMediaTarget(null); // Reset target + setMediaTarget(null); }; const handleDragEnd = (result: any) => { const { source, destination } = result; - // 1. Guard clause: If dropped outside any droppable area, do nothing. if (!destination) return; - // Extract source and destination indices const sourceIndex = source.index; const destinationIndex = destination.index; - // 2. Guard clause: If dropped in the same position, do nothing. if (sourceIndex === destinationIndex) return; - // 3. Create a new array with reordered articles: - // - Create a shallow copy of the current articles array. - // This is important for immutability and triggering re-renders. const newRightArticles = [...sight[language].right]; - // - Remove the dragged article from its original position. - // `splice` returns an array of removed items, so we destructure the first (and only) one. const [movedArticle] = newRightArticles.splice(sourceIndex, 1); - // - Insert the moved article into its new position. newRightArticles.splice(destinationIndex, 0, movedArticle); - // 4. Update the store with the new order: - // This will typically trigger a re-render of the component with the updated list. updateRightArticles(newRightArticles); }; @@ -254,7 +238,7 @@ export const CreateRightTab = observer( height: "100%", minHeight: "calc(100vh - 200px)", gap: 2, - paddingBottom: "70px", // Space for the save button + paddingBottom: "70px", position: "relative", }} > @@ -264,7 +248,6 @@ export const CreateRightTab = observer( - {/* Left Column: Navigation & Article List */} @@ -272,7 +255,6 @@ export const CreateRightTab = observer( { setType("media"); - // setActiveArticleIndex(null); // Optional: deselect article when switching to general media view }} className={`w-full p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300 ${ type === "media" @@ -364,7 +346,6 @@ export const CreateRightTab = observer( - {/* Main content area: Article Editor or Sight Media Preview */} {type === "article" && currentRightArticle ? ( @@ -375,7 +356,7 @@ export const CreateRightTab = observer( startIcon={} onClick={() => { if (currentRightArticle) { - unlinkRightAritcle(currentRightArticle.id); // Corrected function name + unlinkRightAritcle(currentRightArticle.id); setActiveArticleIndex(null); setType("media"); } @@ -435,7 +416,7 @@ export const CreateRightTab = observer( /> { if (files.length > 0) { @@ -507,7 +488,6 @@ export const CreateRightTab = observer( - {/* Right Column: Live Preview */} {type === "article" && activeArticleIndex !== null && ( - {/* Sticky Save Button Footer */} - {/* Modals */} setSelectArticleDialogOpen(false)} onSelectArticle={handleSelectExistingArticleAndLink} - // Pass IDs of already linked/added right articles to exclude them from selection linkedArticleIds={sight[language].right.map((article) => article.id)} /> { setUploadMediaOpen(false); - setFileToUpload(null); // Clear file if dialog is closed without upload + setFileToUpload(null); setMediaTarget(null); }} contextObjectName={sight[language].name} @@ -712,7 +689,7 @@ export const CreateRightTab = observer( ? sight[language].right[activeArticleIndex].heading : undefined } - afterUpload={handleMediaUploaded} // This will use the mediaTarget + afterUpload={handleMediaUploaded} /> article.id === newArticleId ); @@ -144,7 +144,7 @@ export const RightWidgetTab = observer( try { const linkedArticleId = await linkArticle(id); handleCloseSelectModal(); - // Automatically select the newly linked article + const newIndex = sight[language].right.findIndex( (article) => article.id === linkedArticleId ); @@ -177,30 +177,19 @@ export const RightWidgetTab = observer( const handleDragEnd = (result: DropResult) => { const { source, destination } = result; - // 1. Guard clause: If dropped outside any droppable area, do nothing. if (!destination) return; - // Extract source and destination indices const sourceIndex = source.index; const destinationIndex = destination.index; - // 2. Guard clause: If dropped in the same position, do nothing. if (sourceIndex === destinationIndex) return; - // 3. Create a new array with reordered articles: - // - Create a shallow copy of the current articles array. - // This is important for immutability and triggering re-renders. const newRightArticles = [...sight[language].right]; - // - Remove the dragged article from its original position. - // `splice` returns an array of removed items, so we destructure the first (and only) one. const [movedArticle] = newRightArticles.splice(sourceIndex, 1); - // - Insert the moved article into its new position. newRightArticles.splice(destinationIndex, 0, movedArticle); - // 4. Update the store with the new order: - // This will typically trigger a re-render of the component with the updated list. updateRightArticles(newRightArticles); }; diff --git a/src/widgets/VideoPreviewCard/index.tsx b/src/widgets/VideoPreviewCard/index.tsx index a103a7d..8ace054 100644 --- a/src/widgets/VideoPreviewCard/index.tsx +++ b/src/widgets/VideoPreviewCard/index.tsx @@ -28,9 +28,8 @@ export const VideoPreviewCard: React.FC = ({ const token = localStorage.getItem("token"); useEffect(() => {}, [isDragOver]); - // --- Click to select file --- + const handleZoneClick = () => { - // Trigger the hidden file input click fileInputRef.current?.click(); }; @@ -40,19 +39,17 @@ export const VideoPreviewCard: React.FC = ({ const file = event.target.files?.[0]; if (file) { if (file.type.startsWith("video/")) { - // Открываем диалог загрузки медиа с файлом видео onSelectVideoClick(file); } else { toast.error("Пожалуйста, выберите видео файл"); } } - // Reset the input value so selecting the same file again triggers change + event.target.value = ""; }; - // --- Drag and Drop Handlers --- const handleDragOver = (event: DragEvent) => { - event.preventDefault(); // Crucial to allow a drop + event.preventDefault(); event.stopPropagation(); setIsDragOver(true); }; @@ -64,7 +61,7 @@ export const VideoPreviewCard: React.FC = ({ }; const handleDrop = async (event: DragEvent) => { - event.preventDefault(); // Crucial to allow a drop + event.preventDefault(); event.stopPropagation(); setIsDragOver(false); @@ -72,7 +69,6 @@ export const VideoPreviewCard: React.FC = ({ if (files && files.length > 0) { const file = files[0]; if (file.type.startsWith("video/")) { - // Открываем диалог загрузки медиа с файлом видео onSelectVideoClick(file); } else { toast.error("Пожалуйста, выберите видео файл"); @@ -175,7 +171,7 @@ export const VideoPreviewCard: React.FC = ({ borderRadius: 1, cursor: "pointer", }} - onClick={handleZoneClick} // Click handler for the zone + onClick={handleZoneClick} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} @@ -189,8 +185,8 @@ export const VideoPreviewCard: React.FC = ({ color="primary" startIcon={} onClick={(e) => { - e.stopPropagation(); // Prevent `handleZoneClick` from firing - onSelectVideoClick(); // This button triggers the media selection dialog + e.stopPropagation(); + onSelectVideoClick(); }} > Выбрать файл @@ -201,7 +197,7 @@ export const VideoPreviewCard: React.FC = ({ ref={fileInputRef} onChange={handleFileInputChange} style={{ display: "none" }} - accept="video/*" // Accept only video files + accept="video/*" /> )}