Schedule-specific sidebar (ScheduleFilter)
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.
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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<ScheduleFilterProps> = ({
|
||||
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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<div className="schedule-filter" data-testid="schedule-filter">
|
||||
<section className="frame schedule-filter__frame">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="schedule-filter__form"
|
||||
data-testid="search-form"
|
||||
>
|
||||
<div className="filter-content">
|
||||
<CityAutocomplete
|
||||
label={t("SHARED.DEPARTURE_CITY")}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
value={departure}
|
||||
onChange={setDeparture}
|
||||
dictionaries={dictionaries}
|
||||
testIdPrefix="schedule-departure"
|
||||
/>
|
||||
|
||||
<div className="schedule-filter__swap">
|
||||
<button
|
||||
type="button"
|
||||
className="schedule-filter__swap-btn"
|
||||
onClick={handleSwap}
|
||||
aria-label={t("SHARED.SWAP-CITIES") || "swap"}
|
||||
data-testid="swap-cities-button"
|
||||
>
|
||||
<svg className="svg--change-city">
|
||||
<use xlinkHref="/assets/img/sprite.svg#changeCity" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CityAutocomplete
|
||||
label={t("SHARED.ARRIVAL_CITY")}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
value={arrival}
|
||||
onChange={setArrival}
|
||||
dictionaries={dictionaries}
|
||||
testIdPrefix="schedule-arrival"
|
||||
/>
|
||||
|
||||
<div className="calendar">
|
||||
<label className="label--filter">
|
||||
{t("SHARED.SCHEDULES_DATE")}
|
||||
</label>
|
||||
<Calendar
|
||||
value={dateRange}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="wrapper--time-selector compact-view" data-testid="time-selector">
|
||||
<div className="time-selector__label-value">
|
||||
<div className="time-selector__label">
|
||||
{t("SHARED.DEPARTURE_TIME")}
|
||||
</div>
|
||||
<div className="time-selector__value">
|
||||
{minutesToTime(timeRange[0])} — {minutesToTime(timeRange[1])}
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-selector">
|
||||
<Slider
|
||||
value={timeRange}
|
||||
onChange={(e: SliderChangeEvent) =>
|
||||
setTimeRange(e.value as [number, number])
|
||||
}
|
||||
range
|
||||
min={0}
|
||||
max={1440}
|
||||
step={60}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="schedule-filter__checkbox" data-testid="schedule-direct-only">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={directOnly}
|
||||
onChange={(e) => setDirectOnly(e.target.checked)}
|
||||
/>
|
||||
<span>{t("SHARED.DIRECT_FLIGHT_ONLY")}</span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
className="schedule-filter__checkbox"
|
||||
data-testid="schedule-return-flights"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={returnFlights}
|
||||
onChange={(e) => setReturnFlights(e.target.checked)}
|
||||
/>
|
||||
<span>{t("SHARED.RETURN_FLIGHT_VIEW")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="filter-button">
|
||||
<button
|
||||
type="submit"
|
||||
className="search-button"
|
||||
data-testid="search-submit"
|
||||
>
|
||||
<span>{t("SHARED.SCHEDULES_VIEW")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<ScheduleSearchPageProps> = ({ params }) => {
|
||||
]}
|
||||
contentLeft={
|
||||
<>
|
||||
<OnlineBoardFilter
|
||||
initialTab="route"
|
||||
<ScheduleFilter
|
||||
initialDeparture={outbound.departure}
|
||||
initialArrival={outbound.arrival}
|
||||
initialDate={outbound.dateFrom}
|
||||
initialDateFrom={outbound.dateFrom}
|
||||
initialDateTo={outbound.dateTo}
|
||||
initialDirectOnly={outbound.connections === 0}
|
||||
initialReturnFlights={Boolean(inbound)}
|
||||
/>
|
||||
<SearchHistory />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user