diff --git a/src/features/online-board/components/OnlineBoardFilter.tsx b/src/features/online-board/components/OnlineBoardFilter.tsx index d2ef9df6..f6887a53 100644 --- a/src/features/online-board/components/OnlineBoardFilter.tsx +++ b/src/features/online-board/components/OnlineBoardFilter.tsx @@ -9,6 +9,7 @@ import { type FC, useState, useCallback, type FormEvent } from "react"; import { useNavigate, useParams } from "@modern-js/runtime/router"; import { Calendar } from "primereact/calendar"; +import { AutoComplete, type AutoCompleteCompleteEvent } from "primereact/autocomplete"; import { useTranslation } from "@/i18n/provider.js"; import { buildOnlineBoardUrl } from "../url.js"; import "./OnlineBoardFilter.scss"; @@ -39,6 +40,20 @@ export const OnlineBoardFilter: FC = () => { const [arrivalAirport, setArrivalAirport] = useState(""); const [routeDate, setRouteDate] = useState(new Date()); + // AutoComplete suggestions (populated by API in future; empty for now) + const [departureSuggestions, setDepartureSuggestions] = useState([]); + const [arrivalSuggestions, setArrivalSuggestions] = useState([]); + + const handleDepartureSearch = useCallback((_event: AutoCompleteCompleteEvent) => { + // TODO: call dictionary API to filter cities by query + setDepartureSuggestions([]); + }, []); + + const handleArrivalSearch = useCallback((_event: AutoCompleteCompleteEvent) => { + // TODO: call dictionary API to filter cities by query + setArrivalSuggestions([]); + }, []); + const handleTabClick = useCallback( (tab: FilterTab) => { setSelectedTab(selectedTab === tab ? null : tab); @@ -199,12 +214,14 @@ export const OnlineBoardFilter: FC = () => { - setDepartureAirport(e.target.value)} + suggestions={departureSuggestions} + completeMethod={handleDepartureSearch} + onChange={(e) => setDepartureAirport(e.value as string)} + placeholder={t("SHARED.CITY_PLACEHOLDER")} + className="input--filter" + inputClassName="input--filter" data-testid="departure-airport-input" /> @@ -212,12 +229,14 @@ export const OnlineBoardFilter: FC = () => { - setArrivalAirport(e.target.value)} + suggestions={arrivalSuggestions} + completeMethod={handleArrivalSearch} + onChange={(e) => setArrivalAirport(e.value as string)} + placeholder={t("SHARED.CITY_PLACEHOLDER")} + className="input--filter" + inputClassName="input--filter" data-testid="arrival-airport-input" /> diff --git a/src/features/popular-requests/components/PopularRequestsPanel.tsx b/src/features/popular-requests/components/PopularRequestsPanel.tsx index 0613a207..42c46340 100644 --- a/src/features/popular-requests/components/PopularRequestsPanel.tsx +++ b/src/features/popular-requests/components/PopularRequestsPanel.tsx @@ -15,6 +15,21 @@ import { PopularRequestItem } from "./PopularRequestItem.js"; import "./PopularRequestsPanel.scss"; import type { PopularRequest } from "../types.js"; +/** Build a stable React key from a popular request's discriminated fields. */ +function getRequestKey(request: PopularRequest): string { + switch (request.mode) { + case "FlightNumber": + return `fn-${request.carrier}-${request.flightNumber}`; + case "Arrival": + return `arr-${request.arrival}`; + case "Departure": + return `dep-${request.departure}`; + case "Route": + case "RouteWithBack": + return `${request.mode}-${request.departure}-${request.arrival}`; + } +} + export interface PopularRequestsPanelProps { /** Callback invoked when a user clicks a popular request. The host page * handles navigation based on the request mode and type. */ @@ -44,8 +59,8 @@ export function PopularRequestsPanel({

{t("BOARD.POPULAR-CHAPTERS")}

- {visibleRequests.map((request, index) => ( -
+ {visibleRequests.map((request) => ( +
{ const [returnDateFrom, setReturnDateFrom] = useState(addDays(today, 7)); const [returnDateTo, setReturnDateTo] = useState(addDays(today, 14)); + // AutoComplete suggestions (populated by API in future; empty for now) + const [departureSuggestions, setDepartureSuggestions] = useState([]); + const [arrivalSuggestions, setArrivalSuggestions] = useState([]); + + const handleDepartureSearch = useCallback((_event: AutoCompleteCompleteEvent) => { + setDepartureSuggestions([]); + }, []); + + const handleArrivalSearch = useCallback((_event: AutoCompleteCompleteEvent) => { + setArrivalSuggestions([]); + }, []); + const handleSubmit = useCallback( (e: FormEvent) => { e.preventDefault(); @@ -94,33 +107,37 @@ export const ScheduleStartPage: FC = () => { onSubmit={handleSubmit} >
- - {t("SHARED.DEPARTURE_CITY")} + setDepartureAirport(e.target.value)} + suggestions={departureSuggestions} + completeMethod={handleDepartureSearch} + onChange={(e) => setDepartureAirport(e.value as string)} + placeholder={t("SHARED.CITY_PLACEHOLDER")} + className="input--filter" + inputClassName="input--filter" + inputId="schedule-departure" data-testid="departure-input" />
- - {t("SHARED.ARRIVAL_CITY")} + setArrivalAirport(e.target.value)} + suggestions={arrivalSuggestions} + completeMethod={handleArrivalSearch} + onChange={(e) => setArrivalAirport(e.value as string)} + placeholder={t("SHARED.CITY_PLACEHOLDER")} + className="input--filter" + inputClassName="input--filter" + inputId="schedule-arrival" data-testid="arrival-input" />
- + setDateFrom(e.value as Date)} @@ -133,7 +150,7 @@ export const ScheduleStartPage: FC = () => {
- + setDateTo(e.value as Date)} @@ -153,14 +170,14 @@ export const ScheduleStartPage: FC = () => { onChange={(e) => setIsRoundTrip(e.target.checked)} data-testid="round-trip-toggle" /> - Round trip + {t("SHARED.RETURN_FLIGHT_VIEW")}
{isRoundTrip && ( <>
- + setReturnDateFrom(e.value as Date)} @@ -173,7 +190,7 @@ export const ScheduleStartPage: FC = () => {
- + setReturnDateTo(e.value as Date)} @@ -192,7 +209,7 @@ export const ScheduleStartPage: FC = () => { className="schedule-start__submit" data-testid="schedule-search-submit" > - Search + {t("SHARED.SEARCH")} ); diff --git a/src/routes/error/[code]/page.tsx b/src/routes/error/[code]/page.tsx index 914dc1f5..c06453c3 100644 --- a/src/routes/error/[code]/page.tsx +++ b/src/routes/error/[code]/page.tsx @@ -1,36 +1,85 @@ +import { useState, useEffect } from "react"; import { useParams } from "@modern-js/runtime/router"; +import { createI18nInstance } from "@/i18n/config.js"; +import { resolveLocaleFromPath, type Language } from "@/i18n/resolver.js"; import "./page.scss"; -const ERROR_CONFIG: Record = { +interface ErrorConfig { + titleKey: string; + descriptionKey: string; + image: string; + buyTicketKey: string; + homeKey: string; + supportKey: string; +} + +const ERROR_CONFIG: Record = { "404": { - title: "Страница не найдена", - description: - "Запрашиваемая страница не найдена или ссылка неверна.", + titleKey: "PAGE404.HEADER", + descriptionKey: "PAGE404.DESCRIPTION", image: "/assets/img/lady404.png", + buyTicketKey: "PAGE404.BUY-TICKET", + homeKey: "PAGE404.TO-HOME", + supportKey: "PAGE404.SUPPORT", }, "500": { - title: "Ошибка сервера", - description: - "При обработке запроса произошла внутренняя ошибка. Попробуйте позже.", + titleKey: "PAGE500.HEADER", + descriptionKey: "PAGE500.DESCRIPTION", image: "/assets/img/lady500.png", + buyTicketKey: "PAGE500.BUY-TICKET", + homeKey: "PAGE500.TO-HOME", + supportKey: "PAGE500.SUPPORT", }, "503": { - title: "Сервис недоступен", - description: - "Сервис временно недоступен. Попробуйте через несколько минут.", + titleKey: "PAGE500.HEADER", + descriptionKey: "PAGE500.DESCRIPTION", image: "/assets/img/lady500.png", + buyTicketKey: "PAGE500.BUY-TICKET", + homeKey: "PAGE500.TO-HOME", + supportKey: "PAGE500.SUPPORT", }, }; -const FALLBACK = { - title: "Ошибка", - description: "Произошла непредвиденная ошибка.", +const FALLBACK_CONFIG: ErrorConfig = { + titleKey: "PAGE500.HEADER", + descriptionKey: "PAGE500.DESCRIPTION", image: "/assets/img/lady500.png", + buyTicketKey: "PAGE500.BUY-TICKET", + homeKey: "PAGE500.TO-HOME", + supportKey: "PAGE500.SUPPORT", }; export default function ErrorPage(): JSX.Element { const { code } = useParams<{ code: string }>(); - const config = (code ? ERROR_CONFIG[code] : undefined) ?? FALLBACK; + const config = (code ? ERROR_CONFIG[code] : undefined) ?? FALLBACK_CONFIG; + + // Attempt to detect locale from referrer or default to "ru" + const [translations, setTranslations] = useState | null>(null); + + useEffect(() => { + const pathname = typeof window !== "undefined" ? window.location.pathname : ""; + const detected = resolveLocaleFromPath(pathname); + const locale: Language = detected ?? "ru"; + + void createI18nInstance({ locale }).then((i18n) => { + const t = (key: string) => i18n.t(key) as string; + setTranslations({ + title: t(config.titleKey), + description: t(config.descriptionKey), + buyTicket: t(config.buyTicketKey), + home: t(config.homeKey), + support: t(config.supportKey), + }); + }); + }, [config]); + + // Show content immediately (with hardcoded Russian fallback for SSR), + // then replace with i18n translations on the client + const title = translations?.title ?? config.titleKey; + const description = translations?.description ?? config.descriptionKey; + const buyTicket = translations?.buyTicket ?? "Купить билет"; + const home = translations?.home ?? "На главную"; + const support = translations?.support ?? "Поддержка"; return (
@@ -41,26 +90,26 @@ export default function ErrorPage(): JSX.Element { />
{code ?? "?"}
-
{config.title}
-
{config.description}
+
{title}
+
{description}