From bb49a5d609933a0b2dbf46c40d6a86677f02f058 Mon Sep 17 00:00:00 2001 From: gnezim Date: Mon, 20 Apr 2026 16:34:52 +0300 Subject: [PATCH] Fix Schedule "Details" button + search-history sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schedule: - ScheduleSearchPage wires handleFlightClick to DayGroupedFlightList so the "Детали рейса" button in the expanded flight body navigates to /{lang}/schedule/{carrier}{flightNumber}-{yyyyMMdd} (Angular's ScheduleNavigationService.toDetailsPage equivalent). Previously the Details button fired onStatus → no handler → no-op. Search history: - useSearchHistory now broadcasts a custom `afl:search-history-changed` window event on add/clear and listens for it in a useEffect. Fixes the case where a route-level component (ScheduleSearchPage) adds to storage while a sibling SearchHistory sidebar had already captured an empty initial value via useState — the sidebar now re-reads storage and shows the history without a page reload. --- .../components/ScheduleSearchPage.tsx | 22 +++++++++++++ src/shared/hooks/useSearchHistory.ts | 31 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/features/schedule/components/ScheduleSearchPage.tsx b/src/features/schedule/components/ScheduleSearchPage.tsx index 44240171..2516e8b5 100644 --- a/src/features/schedule/components/ScheduleSearchPage.tsx +++ b/src/features/schedule/components/ScheduleSearchPage.tsx @@ -25,6 +25,7 @@ import "./ScheduleSearchPage.scss"; import { JsonLdRenderer } from "@/shared/seo/json-ld.js"; import { useScheduleSearch } from "../hooks/useScheduleSearch.js"; import { buildScheduleUrl } from "../url.js"; +import { buildFlightUrlParams } from "../../online-board/url.js"; import { buildScheduleFlightListJsonLd } from "../json-ld.js"; import type { ScheduleParams } from "../url.js"; import type { IScheduleSearchRequest, ISimpleFlight } from "../types.js"; @@ -122,6 +123,25 @@ export const ScheduleSearchPage: FC = ({ params }) => { const outbound = params.outbound; const inbound = params.type === "roundtrip" ? params.inbound : undefined; + // Mirrors Angular `ScheduleNavigationService.toDetailsPage` → + // `UrlBuilder.getDetailsPageUrl` for direct/multi-leg flights: + // /{lang}/schedule/{flightSegment} + // where flightSegment is `{carrier}{flightNumber}-{yyyyMMdd}`. The + // details route's catch-all parser skips 3-char airport codes, so + // we don't need to insert them for the segment to resolve. + const handleFlightClick = useCallback( + (flight: ISimpleFlight) => { + const segment = buildFlightUrlParams({ + carrier: flight.flightId.carrier, + flightNumber: flight.flightId.flightNumber, + ...(flight.flightId.suffix ? { suffix: flight.flightId.suffix } : {}), + date: flight.flightId.date, + }); + void navigate(`/${locale}/schedule/${segment}`); + }, + [locale, navigate], + ); + // Round-trip schedules render only one direction at a time, with a // `Туда / Обратно` switcher in the sticky header — matches Angular's // `schedule-direction-switch` (one-of-two button group). Default to @@ -373,6 +393,7 @@ export const ScheduleSearchPage: FC = ({ params }) => { ) : ( @@ -380,6 +401,7 @@ export const ScheduleSearchPage: FC = ({ params }) => { )} diff --git a/src/shared/hooks/useSearchHistory.ts b/src/shared/hooks/useSearchHistory.ts index 8acb01e7..48a860bd 100644 --- a/src/shared/hooks/useSearchHistory.ts +++ b/src/shared/hooks/useSearchHistory.ts @@ -9,10 +9,18 @@ * @module */ -import { useState, useCallback } from "react"; +import { useState, useCallback, useEffect } from "react"; import { z } from "zod"; import { storage } from "@/shared/storage.js"; +// Custom event fired whenever any hook instance mutates search history. +// Other live hook instances listen for it and re-read from storage so +// the sidebar on a search page stays in sync when the same route +// performs a follow-up search (React Router reuses the page component, +// the useState initializer does NOT re-run, and the cross-tab +// `storage` event doesn't fire for same-tab writes). +const SEARCH_HISTORY_CHANGED_EVENT = "afl:search-history-changed"; + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -119,6 +127,21 @@ export function useSearchHistory(lang: string): UseSearchHistoryResult { return (storage.get(key, searchHistorySchema) ?? []) as SearchHistoryItem[]; }); + // Re-read storage whenever any instance broadcasts a change. Covers + // the case where a search-results page persists the search via one + // hook instance and the sidebar on the same mounted page uses a + // separate hook instance whose initial useState() has already run. + useEffect(() => { + if (typeof window === "undefined") return; + const handler = (): void => { + const fresh = + (storage.get(key, searchHistorySchema) ?? []) as SearchHistoryItem[]; + setItems(fresh); + }; + window.addEventListener(SEARCH_HISTORY_CHANGED_EVENT, handler); + return () => window.removeEventListener(SEARCH_HISTORY_CHANGED_EVENT, handler); + }, [key]); + const add = useCallback( (item: SearchHistoryItem) => { setItems((prev) => { @@ -132,6 +155,9 @@ export function useSearchHistory(lang: string): UseSearchHistoryResult { const next = [item, ...filtered].slice(0, MAX_HISTORY_ITEMS); storage.set(key, next, searchHistorySchema); + if (typeof window !== "undefined") { + window.dispatchEvent(new Event(SEARCH_HISTORY_CHANGED_EVENT)); + } return next; }); }, @@ -141,6 +167,9 @@ export function useSearchHistory(lang: string): UseSearchHistoryResult { const clear = useCallback(() => { storage.delete(key); setItems([]); + if (typeof window !== "undefined") { + window.dispatchEvent(new Event(SEARCH_HISTORY_CHANGED_EVENT)); + } }, [key]); return { items, add, clear };