diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 0c20069d..bd79a245 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -36,7 +36,7 @@ "STATUS-PLANNED": "Scheduled", "TIME_DEPARTURE": "Departure time", "TITLE": "Online Timetable", - "YOU_SEARCH": "You searched", + "YOU_SEARCH": "Previous searches", "LEG": "Leg", "TOTAL-FLYING-TIME": "Total flying time", "DETAILS-TITLE": "Flight details", @@ -316,7 +316,7 @@ "BACK-SCHEDULE": "Back to Schedule", "BAGBELT": "Baggage Carousel Number", "BOARD_SCHEDULE": "Online Timetable and Schedule", - "BUY-TICKET": "Buy", + "BUY-TICKET": "Buy a ticket", "BUY-TICKET-HIT-RATE": "Buy a ticket at the Hit Fare", "CITY_CHANGE": "Different cities", "CITY_PLACEHOLDER": "Specify city", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index af308ad6..dad64467 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -36,7 +36,7 @@ "STATUS-PLANNED": "Запланирован", "TIME_DEPARTURE": "Время отправления", "TITLE": "Онлайн-Табло", - "YOU_SEARCH": "Вы искали", + "YOU_SEARCH": "Ранее искали", "LEG": "Перелет", "TOTAL-FLYING-TIME": "Общее время в пути", "DETAILS-TITLE": "Детали рейса", @@ -316,7 +316,7 @@ "BACK-SCHEDULE": "Вернуться к Расписанию", "BAGBELT": "Номер ленты выдачи багажа", "BOARD_SCHEDULE": "Онлайн-Табло и Расписание", - "BUY-TICKET": "Купить", + "BUY-TICKET": "Купить билет", "BUY-TICKET-HIT-RATE": "Купить билет по Хит тарифу", "CITY_CHANGE": "Смена города", "CITY_PLACEHOLDER": "Укажите город", diff --git a/src/shared/hooks/useSearchHistory.test.ts b/src/shared/hooks/useSearchHistory.test.ts index ca7dc60f..2ee2aa89 100644 --- a/src/shared/hooks/useSearchHistory.test.ts +++ b/src/shared/hooks/useSearchHistory.test.ts @@ -143,7 +143,10 @@ describe("useSearchHistory", () => { expect(result.current.items).toEqual([flightItem, routeItem]); }); - it("limits history to 10 items", () => { + // TIRREDESIGN-5 / TZ §4.1.14.3.8: flight-number history keeps at most 8 + // entries independently of board / schedule sections (one section + // filling up must not evict entries from the others). + it("caps flight-number section at 8 most-recent items", () => { const { result } = renderHook(() => useSearchHistory("ru")); for (let i = 0; i < 12; i++) { @@ -156,9 +159,43 @@ describe("useSearchHistory", () => { }); } - expect(result.current.items).toHaveLength(10); - // Most recent should be first + expect(result.current.items).toHaveLength(8); expect(result.current.items[0]?.url).toBe("/flight-11"); + expect(result.current.items[7]?.url).toBe("/flight-4"); + }); + + it("caps each section independently (board + schedule + flight-number)", () => { + const { result } = renderHook(() => useSearchHistory("ru")); + + for (let i = 0; i < 10; i++) { + act(() => { + result.current.add({ + type: "flight-number", + url: `/flight-${i}`, + label: `Flight ${i}`, + }); + }); + act(() => { + result.current.add({ + type: "board-route", + url: `/departure-${i}`, + label: `Departure ${i}`, + }); + }); + act(() => { + result.current.add({ + type: "schedule-route", + url: `/schedule-${i}`, + label: `Schedule ${i}`, + }); + }); + } + + const byType = (t: string) => + result.current.items.filter((item) => item.type === t); + expect(byType("flight-number")).toHaveLength(8); + expect(byType("board-route")).toHaveLength(8); + expect(byType("schedule-route")).toHaveLength(8); }); it("namespaces by language — different langs have separate history", () => { diff --git a/src/shared/hooks/useSearchHistory.ts b/src/shared/hooks/useSearchHistory.ts index 4c27d076..53ef9d00 100644 --- a/src/shared/hooks/useSearchHistory.ts +++ b/src/shared/hooks/useSearchHistory.ts @@ -106,7 +106,16 @@ const searchHistorySchema = z.array(searchHistoryItemSchema); // Storage key // --------------------------------------------------------------------------- -const MAX_HISTORY_ITEMS = 10; +// TIRREDESIGN-5 / TZ §4.1.8: each section (board vs schedule vs flight-number) +// retains up to 8 recent searches independently. A new board-route entry +// cannot push a schedule-route entry out of the list. +const MAX_PER_SECTION = 8; + +function sectionFor(type: SearchHistoryItem["type"]): "board" | "schedule" | "flight-number" { + if (type === "schedule-route") return "schedule"; + if (type === "flight-number") return "flight-number"; + return "board"; +} function storageKey(lang: string): string { return `history_${lang}`; @@ -153,9 +162,23 @@ export function useSearchHistory(lang: string): UseSearchHistoryResult { return prev; } - // Remove any existing entry with same URL, then prepend + // Remove any existing entry with the same URL, then prepend. const filtered = prev.filter((existing) => existing.url !== item.url); - const next = [item, ...filtered].slice(0, MAX_HISTORY_ITEMS); + const combined = [item, ...filtered]; + + // Cap per section — the board/schedule/flight-number buckets each + // hold up to MAX_PER_SECTION (8) entries independently. This keeps + // the resulting list ordered by recency across sections but + // prevents a burst of board searches from evicting schedule + // history (and vice versa). + const perSectionCount = { board: 0, schedule: 0, "flight-number": 0 }; + const next: SearchHistoryItem[] = []; + for (const entry of combined) { + const bucket = sectionFor(entry.type); + if (perSectionCount[bucket] >= MAX_PER_SECTION) continue; + perSectionCount[bucket]++; + next.push(entry); + } sessionStore.set(key, next, searchHistorySchema); if (typeof window !== "undefined") {