From 65e776273df1c35257c04f3860ec01022cd86572 Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 6 May 2026 14:38:17 +0300 Subject: [PATCH] Fix map calendar relative date labels --- .../components/FlightsMapFilter.test.tsx | 86 ++++++++++++++++++- .../components/FlightsMapFilter.tsx | 70 +++++++++++++++ 2 files changed, 154 insertions(+), 2 deletions(-) diff --git a/src/features/flights-map/components/FlightsMapFilter.test.tsx b/src/features/flights-map/components/FlightsMapFilter.test.tsx index 70fd8448..29a8606d 100644 --- a/src/features/flights-map/components/FlightsMapFilter.test.tsx +++ b/src/features/flights-map/components/FlightsMapFilter.test.tsx @@ -2,7 +2,7 @@ * @vitest-environment jsdom */ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { FlightsMapFilter } from "./FlightsMapFilter.js"; import type { IFlightsMapFilterState } from "../types.js"; @@ -40,7 +40,12 @@ vi.mock("@modern-js/runtime/router", () => ({ vi.mock("@/i18n/provider.js", () => ({ useTranslation: () => ({ - t: (key: string) => key, + t: (key: string) => + key === "SHARED.TODAY" + ? "Сегодня" + : key === "SHARED.TOMORROW" + ? "Завтра" + : key, i18n: { language: "ru" }, }), })); @@ -201,6 +206,83 @@ describe("FlightsMapFilter — Calendar wiring", () => { }); }); +describe("FlightsMapFilter — TIRREDESIGN-22: calendar today/tomorrow labels", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2026, 4, 6, 12, 0, 0)); + lastCalendarProps = null; + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("formats today's selected date as a word", () => { + render( + , + ); + + const formatDateTime = lastCalendarProps!["formatDateTime"] as ( + date: Date, + ) => string; + + expect(formatDateTime(new Date(2026, 4, 6))).toBe("Сегодня"); + }); + + it("formats tomorrow's selected date as a word", () => { + render( + , + ); + + const formatDateTime = lastCalendarProps!["formatDateTime"] as ( + date: Date, + ) => string; + + expect(formatDateTime(new Date(2026, 4, 7))).toBe("Завтра"); + }); + + it("keeps normal dates in dd.MM.yyyy format", () => { + render( + , + ); + + const formatDateTime = lastCalendarProps!["formatDateTime"] as ( + date: Date, + ) => string; + + expect(formatDateTime(new Date(2026, 4, 8))).toBe("08.05.2026"); + }); + + it("parses typed today/tomorrow words back to dates", () => { + render( + , + ); + + const parseDateTime = lastCalendarProps!["parseDateTime"] as ( + text: string, + ) => Date; + + expect(parseDateTime("Сегодня").toISOString()).toBe( + new Date(2026, 4, 6).toISOString(), + ); + expect(parseDateTime("Завтра").toISOString()).toBe( + new Date(2026, 4, 7).toISOString(), + ); + }); +}); + // --------------------------------------------------------------------------- // TZ §4.1.24.2 R16: Calendar disabled when Город вылета is empty // --------------------------------------------------------------------------- diff --git a/src/features/flights-map/components/FlightsMapFilter.tsx b/src/features/flights-map/components/FlightsMapFilter.tsx index d535cd36..6543b746 100644 --- a/src/features/flights-map/components/FlightsMapFilter.tsx +++ b/src/features/flights-map/components/FlightsMapFilter.tsx @@ -44,6 +44,64 @@ function dateToYyyymmdd(value: Date): string { return `${y}${m}${d}`; } +function addDays(base: Date, days: number): Date { + const date = new Date(base); + date.setDate(date.getDate() + days); + date.setHours(0, 0, 0, 0); + return date; +} + +function isSameDay(a: Date, b: Date): boolean { + return ( + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() + ); +} + +function formatDateInputValue(value: Date, t: (key: string) => string): string { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (isSameDay(value, today)) return t("SHARED.TODAY"); + if (isSameDay(value, addDays(today, 1))) return t("SHARED.TOMORROW"); + + const d = value.getDate().toString().padStart(2, "0"); + const m = (value.getMonth() + 1).toString().padStart(2, "0"); + return `${d}.${m}.${value.getFullYear()}`; +} + +function parseDateInputValue(value: string, t: (key: string) => string): Date | null { + const text = value.trim(); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + if (text.localeCompare(t("SHARED.TODAY"), undefined, { sensitivity: "accent" }) === 0) { + return today; + } + + if ( + text.localeCompare(t("SHARED.TOMORROW"), undefined, { sensitivity: "accent" }) === 0 + ) { + return addDays(today, 1); + } + + const match = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/.exec(text); + if (!match) return null; + + const day = Number(match[1]); + const month = Number(match[2]) - 1; + const year = Number(match[3]); + const date = new Date(year, month, day); + date.setHours(0, 0, 0, 0); + + return date.getFullYear() === year && + date.getMonth() === month && + date.getDate() === day + ? date + : null; +} + /** * Filter component for the flights map. Controls departure, arrival, * connections, domestic/international toggles, and date selection. @@ -153,6 +211,16 @@ export const FlightsMapFilter: FC = ({ [value, onChange], ); + const formatDateTime = useCallback( + (date: Date) => formatDateInputValue(date, t), + [t], + ); + + const parseDateTime = useCallback( + (text: string) => parseDateInputValue(text, t) ?? new Date(Number.NaN), + [t], + ); + return (
@@ -294,6 +362,8 @@ export const FlightsMapFilter: FC = ({ maxDate={maxDate} disabledDates={disabledDates} dateFormat="dd.mm.yy" + formatDateTime={formatDateTime} + parseDateTime={parseDateTime} placeholder={t("SHARED.DATE_FORMAT")} showIcon disabled={!value.departure}