From 8bf672f3fa9466de1875324e6dfad77b8b1fd977 Mon Sep 17 00:00:00 2001 From: gnezim Date: Sun, 19 Apr 2026 23:36:05 +0300 Subject: [PATCH] Schedule-specific sidebar (ScheduleFilter) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace OnlineBoardFilter on schedule pages with a dedicated ScheduleFilter that matches Angular's schedule-filter: - Город вылета / Город прилета with swap arrows - 'Показать расписание на' date range picker - 'Время вылета' time slider - 'Только прямые рейсы' checkbox (sets connections=0) - 'Показать обратные рейсы' checkbox - 'Показать расписание' submit button (blue, full-width) The OnlineBoardFilter accordion (Номер рейса + Маршрут tabs) is no longer rendered on schedule pages — Angular only ships flight-number search on the online-board side. --- .../schedule/components/ScheduleFilter.scss | 115 +++++++++ .../schedule/components/ScheduleFilter.tsx | 233 ++++++++++++++++++ .../components/ScheduleSearchPage.tsx | 10 +- 3 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 src/features/schedule/components/ScheduleFilter.scss create mode 100644 src/features/schedule/components/ScheduleFilter.tsx diff --git a/src/features/schedule/components/ScheduleFilter.scss b/src/features/schedule/components/ScheduleFilter.scss new file mode 100644 index 00000000..16aca159 --- /dev/null +++ b/src/features/schedule/components/ScheduleFilter.scss @@ -0,0 +1,115 @@ +@use "../../../styles/colors" as colors; +@use "../../../styles/variables" as vars; +@use "../../../styles/fonts" as fonts; +@use "../../../styles/shadows" as shadows; + +.schedule-filter { + &__frame { + background: colors.$white; + border-radius: 3px; + @include shadows.box-shadow-small; + } + + &__form { + padding: vars.$space-l vars.$space-xl vars.$space-xl; + } + + .filter-content { + display: flex; + flex-direction: column; + gap: vars.$space-l; + } + + .label--filter { + display: block; + font-size: 12px; + color: #6b7280; + margin-bottom: 4px; + } + + .input--filter { + width: 100%; + } + + &__swap { + display: flex; + justify-content: center; + padding: vars.$space-s 0; + } + + &__swap-btn { + background: transparent; + border: 0; + padding: 4px; + cursor: pointer; + color: colors.$blue; + border-radius: 4px; + + &:hover { background: rgba(46, 87, 255, 0.06); } + + .svg--change-city { + width: 24px; + height: 24px; + } + } + + &__checkbox { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + cursor: pointer; + font-size: 13px; + color: #1c2330; + + input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: colors.$blue; + cursor: pointer; + } + } + + // Time selector — borrowed from OnlineBoardFilter compact-view styles. + .wrapper--time-selector { + display: flex; + flex-direction: column; + gap: 8px; + } + + .time-selector__label-value { + display: flex; + justify-content: space-between; + align-items: baseline; + } + + .time-selector__label { + font-size: 12px; + color: #6b7280; + } + + .time-selector__value { + font-size: 12px; + color: #1c2330; + font-weight: fonts.$font-medium; + } + + .filter-button { + margin-top: vars.$space-l; + } + + .search-button { + width: 100%; + background: colors.$blue; + color: #fff; + border: none; + border-radius: 4px; + padding: 12px 16px; + font-size: 14px; + font-weight: fonts.$font-medium; + cursor: pointer; + transition: background-color 150ms ease; + + &:hover { background: #1c45cc; } + } +} diff --git a/src/features/schedule/components/ScheduleFilter.tsx b/src/features/schedule/components/ScheduleFilter.tsx new file mode 100644 index 00000000..ecce4a71 --- /dev/null +++ b/src/features/schedule/components/ScheduleFilter.tsx @@ -0,0 +1,233 @@ +/** + * Schedule-specific filter sidebar. + * + * Mirrors Angular's `schedule-filter` (one-way) layout: city pickers, + * date-range, departure-time slider, two checkboxes + * (`Только прямые рейсы`, `Показать обратные рейсы`), and the + * `Показать расписание` submit button. No flight-number tab — schedule + * search is route-only. + */ + +import { type FC, useState, useCallback, type FormEvent } from "react"; +import { useNavigate } from "@modern-js/runtime/router"; +import { Calendar } from "primereact/calendar"; +import { Slider, type SliderChangeEvent } from "primereact/slider"; +import { useTranslation } from "@/i18n/provider.js"; +import { useLocale } from "@/i18n/useLocale.js"; +import { CityAutocomplete } from "@/ui/city-autocomplete/index.js"; +import { useDictionaries } from "@/shared/dictionaries/index.js"; +import { buildScheduleUrl } from "../url.js"; +import type { ScheduleParams } from "../url.js"; +import "./ScheduleFilter.scss"; + +function minutesToTime(minutes: number): string { + const h = Math.floor(minutes / 60); + const m = minutes % 60; + return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; +} + +function dateToYyyymmdd(value: Date): string { + const y = value.getFullYear().toString(); + const m = (value.getMonth() + 1).toString().padStart(2, "0"); + const d = value.getDate().toString().padStart(2, "0"); + return `${y}${m}${d}`; +} + +function yyyymmddToDate(yyyymmdd?: string): Date | null { + if (!yyyymmdd || yyyymmdd.length !== 8) return null; + const y = parseInt(yyyymmdd.slice(0, 4), 10); + const m = parseInt(yyyymmdd.slice(4, 6), 10) - 1; + const d = parseInt(yyyymmdd.slice(6, 8), 10); + return new Date(y, m, d); +} + +export interface ScheduleFilterProps { + initialDeparture?: string; + initialArrival?: string; + initialDateFrom?: string; + initialDateTo?: string; + initialDirectOnly?: boolean; + initialReturnFlights?: boolean; +} + +export const ScheduleFilter: FC = ({ + initialDeparture, + initialArrival, + initialDateFrom, + initialDateTo, + initialDirectOnly = false, + initialReturnFlights = false, +}) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { locale, language } = useLocale(); + const { dictionaries } = useDictionaries(language); + + const [departure, setDeparture] = useState(initialDeparture ?? ""); + const [arrival, setArrival] = useState(initialArrival ?? ""); + const initFrom = yyyymmddToDate(initialDateFrom); + const initTo = yyyymmddToDate(initialDateTo); + const [dateRange, setDateRange] = useState<(Date | null)[]>( + initFrom && initTo ? [initFrom, initTo] : [null, null], + ); + const [timeRange, setTimeRange] = useState<[number, number]>([0, 1440]); + const [directOnly, setDirectOnly] = useState(initialDirectOnly); + const [returnFlights, setReturnFlights] = useState(initialReturnFlights); + + const handleSwap = useCallback(() => { + setDeparture(arrival); + setArrival(departure); + }, [arrival, departure]); + + const handleSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault(); + const dep = departure.trim().toUpperCase(); + const arr = arrival.trim().toUpperCase(); + if (!dep || !arr) return; + + // Default to current week if no range provided. + const now = new Date(); + const day = now.getDay(); + const monday = new Date(now); + monday.setDate(now.getDate() - ((day + 6) % 7)); + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 6); + + const from = dateRange[0] ?? monday; + const to = dateRange[1] ?? sunday; + + const params: ScheduleParams = { + type: "route", + outbound: { + departure: dep, + arrival: arr, + dateFrom: dateToYyyymmdd(from), + dateTo: dateToYyyymmdd(to), + ...(directOnly ? { connections: 0 } : {}), + }, + }; + const url = buildScheduleUrl(params); + void navigate(`/${locale}/${url}`); + }, + [departure, arrival, dateRange, directOnly, navigate, locale], + ); + + return ( +
+
+
+
+ + +
+ +
+ + + +
+ + setDateRange((e.value as (Date | null)[]) ?? [null, null])} + selectionMode="range" + dateFormat="dd.mm.yy" + placeholder={`${t("SHARED.DATE_FORMAT")} - ${t("SHARED.DATE_FORMAT")}`} + showIcon + className="input--filter" + data-testid="schedule-date-input" + inputId="schedule-date-input" + readOnlyInput + /> +
+ +
+
+
+ {t("SHARED.DEPARTURE_TIME")} +
+
+ {minutesToTime(timeRange[0])} — {minutesToTime(timeRange[1])} +
+
+
+ + setTimeRange(e.value as [number, number]) + } + range + min={0} + max={1440} + step={60} + /> +
+
+ + + + +
+ +
+ +
+
+
+
+ ); +}; diff --git a/src/features/schedule/components/ScheduleSearchPage.tsx b/src/features/schedule/components/ScheduleSearchPage.tsx index bae46aeb..dbe046e6 100644 --- a/src/features/schedule/components/ScheduleSearchPage.tsx +++ b/src/features/schedule/components/ScheduleSearchPage.tsx @@ -18,7 +18,7 @@ import { DayGroupedFlightList } from "./DayGroupedFlightList.js"; import { WeekTabs } from "./WeekTabs.js"; import { PageLayout } from "@/ui/layout/PageLayout.js"; import { PageTabs } from "@/ui/layout/PageTabs.js"; -import { OnlineBoardFilter } from "@/features/online-board/components/OnlineBoardFilter.js"; +import { ScheduleFilter } from "./ScheduleFilter.js"; import { SearchHistory } from "@/ui/layout/SearchHistory.js"; import { useDictionaries } from "@/shared/dictionaries/index.js"; import "./ScheduleSearchPage.scss"; @@ -225,11 +225,13 @@ export const ScheduleSearchPage: FC = ({ params }) => { ]} contentLeft={ <> -