From 385a6e55ee97166931df7a34aa26e409d178966f Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 6 May 2026 14:10:31 +0300 Subject: [PATCH] Fix flights map calendar lower bound --- .../flights-map/calendarRange.test.ts | 19 ++++++++++ src/features/flights-map/calendarRange.ts | 37 +++++++++++++++++-- .../components/FlightsMapFilter.test.tsx | 19 ++++++++++ .../components/FlightsMapFilter.tsx | 6 ++- .../components/FlightsMapStartPage.tsx | 1 + 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/features/flights-map/calendarRange.test.ts b/src/features/flights-map/calendarRange.test.ts index 3c2e3fce..7f5f0649 100644 --- a/src/features/flights-map/calendarRange.test.ts +++ b/src/features/flights-map/calendarRange.test.ts @@ -40,6 +40,25 @@ describe("getMinDate / getMaxDate", () => { const max = getMaxDate(); expect(max.getTime()).toBe(expected.getTime()); }); + + it("derives date-only bounds from the provided yyyyMMdd anchor", () => { + const min = getMinDate("20260506"); + const max = getMaxDate("20260506"); + + expect(min.toISOString()).toBe(new Date(2026, 4, 5).toISOString()); + expect(max.toISOString()).toBe(new Date(2026, 10, 6).toISOString()); + expect(min.getHours()).toBe(0); + expect(max.getHours()).toBe(0); + }); + + it("falls back to the runtime clock when the provided anchor is invalid", () => { + const min = getMinDate("20269999"); + const expected = addDays(new Date(), -1); + + expect(min.getFullYear()).toBe(expected.getFullYear()); + expect(min.getMonth()).toBe(expected.getMonth()); + expect(min.getDate()).toBe(expected.getDate()); + }); }); describe("buildDisabledDates", () => { diff --git a/src/features/flights-map/calendarRange.ts b/src/features/flights-map/calendarRange.ts index 6af20423..fbe3a32b 100644 --- a/src/features/flights-map/calendarRange.ts +++ b/src/features/flights-map/calendarRange.ts @@ -12,19 +12,33 @@ import { mapWindowBounds } from "@/shared/dateWindow.js"; /** Today with time set to 00:00:00 local. */ -export function today(): Date { +export function today(baseYyyymmdd?: string): Date { + if (baseYyyymmdd) { + const parsed = parseYyyymmdd(baseYyyymmdd); + if (parsed) return parsed; + } const d = new Date(); d.setHours(0, 0, 0, 0); return d; } /** minDate = today - 1 day (Angular parity). */ -export function getMinDate(): Date { +export function getMinDate(baseYyyymmdd?: string): Date { + if (baseYyyymmdd) { + const d = today(baseYyyymmdd); + d.setDate(d.getDate() - 1); + return d; + } return mapWindowBounds()[0]; } /** maxDate = today + 6 months (Angular parity). */ -export function getMaxDate(): Date { +export function getMaxDate(baseYyyymmdd?: string): Date { + if (baseYyyymmdd) { + const d = today(baseYyyymmdd); + d.setMonth(d.getMonth() + 6); + return d; + } return mapWindowBounds()[1]; } @@ -92,3 +106,20 @@ function toYyyymmdd(d: Date): string { const day = d.getDate().toString().padStart(2, "0"); return `${y}${m}${day}`; } + +function parseYyyymmdd(value: string): Date | null { + if (!/^\d{8}$/.test(value)) return null; + const y = Number(value.slice(0, 4)); + const m = Number(value.slice(4, 6)); + const d = Number(value.slice(6, 8)); + const parsed = new Date(y, m - 1, d); + parsed.setHours(0, 0, 0, 0); + if ( + parsed.getFullYear() !== y || + parsed.getMonth() !== m - 1 || + parsed.getDate() !== d + ) { + return null; + } + return parsed; +} diff --git a/src/features/flights-map/components/FlightsMapFilter.test.tsx b/src/features/flights-map/components/FlightsMapFilter.test.tsx index 0fc955d3..70fd8448 100644 --- a/src/features/flights-map/components/FlightsMapFilter.test.tsx +++ b/src/features/flights-map/components/FlightsMapFilter.test.tsx @@ -92,6 +92,25 @@ describe("FlightsMapFilter — Calendar wiring", () => { expect(max.getTime()).toBe(expectedMax.getTime()); }); + it("uses the provided SSR today anchor for date-only calendar bounds", () => { + const onChange = vi.fn(); + render( + , + ); + + const min = lastCalendarProps!["minDate"] as Date; + const max = lastCalendarProps!["maxDate"] as Date; + + expect(min.toISOString()).toBe(new Date(2026, 4, 5).toISOString()); + expect(max.toISOString()).toBe(new Date(2026, 10, 6).toISOString()); + expect(min.getHours()).toBe(0); + expect(min.getMinutes()).toBe(0); + }); + it("disables every date when availableDays is empty", () => { const onChange = vi.fn(); render( diff --git a/src/features/flights-map/components/FlightsMapFilter.tsx b/src/features/flights-map/components/FlightsMapFilter.tsx index 2e8de136..d535cd36 100644 --- a/src/features/flights-map/components/FlightsMapFilter.tsx +++ b/src/features/flights-map/components/FlightsMapFilter.tsx @@ -25,6 +25,7 @@ import type { IFlightsMapFilterState } from "../types.js"; export interface FlightsMapFilterProps { value: IFlightsMapFilterState; availableDays?: string[]; + today?: string; onChange: (state: IFlightsMapFilterState) => void; } @@ -50,6 +51,7 @@ function dateToYyyymmdd(value: Date): string { export const FlightsMapFilter: FC = ({ value, availableDays, + today: todayYmd, onChange, }) => { const { t } = useTranslation(); @@ -76,8 +78,8 @@ export const FlightsMapFilter: FC = ({ ); }, [dictionaries, onChange, value]); - const minDate = useMemo(() => getMinDate(), []); - const maxDate = useMemo(() => getMaxDate(), []); + const minDate = useMemo(() => getMinDate(todayYmd), [todayYmd]); + const maxDate = useMemo(() => getMaxDate(todayYmd), [todayYmd]); const disabledDates = useMemo( () => buildDisabledDates(minDate, maxDate, availableDays ?? []), diff --git a/src/features/flights-map/components/FlightsMapStartPage.tsx b/src/features/flights-map/components/FlightsMapStartPage.tsx index 0c13e3cf..0d063571 100644 --- a/src/features/flights-map/components/FlightsMapStartPage.tsx +++ b/src/features/flights-map/components/FlightsMapStartPage.tsx @@ -393,6 +393,7 @@ export const FlightsMapStartPage: FC = ({ }