Fix Schedule "Details" button + search-history sync

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.
This commit is contained in:
2026-04-20 16:34:52 +03:00
parent d44f97d312
commit bb49a5d609
2 changed files with 52 additions and 1 deletions
@@ -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<ScheduleSearchPageProps> = ({ 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<ScheduleSearchPageProps> = ({ params }) => {
<DayGroupedFlightList
flights={outboundSimple}
loading={outboundLoading}
onFlightClick={handleFlightClick}
/>
</div>
) : (
@@ -380,6 +401,7 @@ export const ScheduleSearchPage: FC<ScheduleSearchPageProps> = ({ params }) => {
<DayGroupedFlightList
flights={inboundSimple}
loading={inboundLoading}
onFlightClick={handleFlightClick}
/>
</div>
)}
+30 -1
View File
@@ -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 };