Match Angular form controls: time range slider, date placeholders, button colors

Online board filter: replace time input fields with PrimeReact Slider
matching Angular's p-slider range selector. Initialize dates to null
so placeholder "ДД.ММ.ГГГГ" shows instead of today's date.

Schedule filter: same time slider replacement, add missing "Только
прямые рейсы" (direct flights only) checkbox, date placeholders.

Error page: fix "На главную" button to use outlined style (transparent
bg + blue border) matching Angular's blue-home class. Remove max-width
on description text.
This commit is contained in:
2026-04-16 09:20:26 +03:00
parent 70b8f04cb3
commit 0cdf8c849d
4 changed files with 106 additions and 97 deletions
@@ -178,27 +178,24 @@
}
}
.time-selector {
.wrapper--time-selector {
margin-top: vars.$space-xl;
&__inputs {
display: flex;
align-items: center;
gap: vars.$space-m;
}
&__separator {
.time-selector__label {
font-size: fonts.$font-size-s;
color: colors.$gray;
font-size: fonts.$font-size-l;
margin-bottom: vars.$space-s;
}
.input--time {
flex: 1;
height: vars.$standard-button-height;
border: 1px solid colors.$border-input;
border-radius: vars.$border-radius;
padding: 0 vars.$space-m;
font-size: fonts.$font-size-l;
.time-selector {
padding: 0 vars.$space-s;
}
.time-selector__value {
font-size: fonts.$font-size-s;
color: colors.$gray;
margin-top: vars.$space-s;
text-align: right;
}
}
@@ -11,12 +11,19 @@
import { type FC, useState, useCallback, type FormEvent } from "react";
import { useNavigate, useParams } from "@modern-js/runtime/router";
import { Calendar } from "primereact/calendar";
import { Slider, type SliderChangeEvent } from "primereact/slider";
import { AutoComplete, type AutoCompleteCompleteEvent } from "primereact/autocomplete";
import { useTranslation } from "@/i18n/provider.js";
import { useCitySearch, type CitySuggestion } from "@/shared/hooks/useCitySearch.js";
import { buildOnlineBoardUrl } from "../url.js";
import "./OnlineBoardFilter.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")}`;
}
type AccordionTab = "flight" | "route";
function dateToYyyymmdd(value: Date): string {
@@ -48,15 +55,14 @@ export const OnlineBoardFilter: FC = () => {
// Flight number fields
const [flightNumber, setFlightNumber] = useState("");
const [flightDate, setFlightDate] = useState<Date>(new Date());
const [flightDate, setFlightDate] = useState<Date | null>(null);
const [flightNumberError, setFlightNumberError] = useState<string | null>(null);
// Route fields
const [routeDeparture, setRouteDeparture] = useState<CitySuggestion | string>("");
const [routeArrival, setRouteArrival] = useState<CitySuggestion | string>("");
const [routeDate, setRouteDate] = useState<Date>(new Date());
const [timeFrom, setTimeFrom] = useState("");
const [timeTo, setTimeTo] = useState("");
const [routeDate, setRouteDate] = useState<Date | null>(null);
const [timeRange, setTimeRange] = useState<[number, number]>([0, 1440]);
// City autocomplete search
const { suggestions: routeDepSuggestions, search: searchRouteDep } = useCitySearch();
@@ -192,8 +198,9 @@ export const OnlineBoardFilter: FC = () => {
</label>
<Calendar
value={flightDate}
onChange={(e) => setFlightDate(e.value as Date)}
onChange={(e) => setFlightDate(e.value as Date | null)}
dateFormat="dd.mm.yy"
placeholder={t("SHARED.DATE_FORMAT")}
showIcon
className="input--filter"
data-testid="date-input"
@@ -293,8 +300,9 @@ export const OnlineBoardFilter: FC = () => {
</label>
<Calendar
value={routeDate}
onChange={(e) => setRouteDate(e.value as Date)}
onChange={(e) => setRouteDate(e.value as Date | null)}
dateFormat="dd.mm.yy"
placeholder={t("SHARED.DATE_FORMAT")}
showIcon
className="input--filter"
data-testid="date-input"
@@ -303,25 +311,21 @@ export const OnlineBoardFilter: FC = () => {
</div>
</div>
<div className="time-selector" data-testid="time-selector">
<label className="label--filter">{t("SHARED.FLIGHT_TIME")}</label>
<div className="time-selector__inputs">
<input
type="time"
className="input--filter input--time"
value={timeFrom}
onChange={(e) => setTimeFrom(e.target.value)}
data-testid="time-from-input"
/>
<span className="time-selector__separator">&mdash;</span>
<input
type="time"
className="input--filter input--time"
value={timeTo}
onChange={(e) => setTimeTo(e.target.value)}
data-testid="time-to-input"
<div className="wrapper--time-selector full-view" data-testid="time-selector">
<div className="time-selector__label">{t("SHARED.FLIGHT_TIME")}</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 className="time-selector__value">
{minutesToTime(timeRange[0])} {minutesToTime(timeRange[1])}
</div>
</div>
<div className="filter-button">
@@ -10,6 +10,7 @@
import { type FC, useState, useCallback, type FormEvent } from "react";
import { useNavigate, useParams } from "@modern-js/runtime/router";
import { Calendar } from "primereact/calendar";
import { Slider, type SliderChangeEvent } from "primereact/slider";
import { AutoComplete, type AutoCompleteCompleteEvent } from "primereact/autocomplete";
import { useTranslation } from "@/i18n/provider.js";
import { useCitySearch, type CitySuggestion } from "@/shared/hooks/useCitySearch.js";
@@ -21,6 +22,12 @@ import type { PopularRequest } from "@/features/popular-requests/types.js";
import { buildScheduleUrl } from "../url.js";
import "./ScheduleStartPage.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");
@@ -44,15 +51,14 @@ export const ScheduleStartPage: FC = () => {
const [departureAirport, setDepartureAirport] = useState<CitySuggestion | string>("");
const [arrivalAirport, setArrivalAirport] = useState<CitySuggestion | string>("");
const [dateFrom, setDateFrom] = useState<Date>(today);
const [dateTo, setDateTo] = useState<Date>(addDays(today, 7));
const [timeFrom, setTimeFrom] = useState("");
const [timeTo, setTimeTo] = useState("");
const [dateFrom, setDateFrom] = useState<Date | null>(null);
const [dateTo, setDateTo] = useState<Date | null>(null);
const [timeRange, setTimeRange] = useState<[number, number]>([0, 1440]);
const [directOnly, setDirectOnly] = useState(false);
const [isRoundTrip, setIsRoundTrip] = useState(false);
const [returnDateFrom, setReturnDateFrom] = useState<Date>(addDays(today, 7));
const [returnDateTo, setReturnDateTo] = useState<Date>(addDays(today, 14));
const [returnTimeFrom, setReturnTimeFrom] = useState("");
const [returnTimeTo, setReturnTimeTo] = useState("");
const [returnDateFrom, setReturnDateFrom] = useState<Date | null>(null);
const [returnDateTo, setReturnDateTo] = useState<Date | null>(null);
const [returnTimeRange, setReturnTimeRange] = useState<[number, number]>([0, 1440]);
// City autocomplete search
const { suggestions: departureSuggestions, search: searchDeparture } = useCitySearch();
@@ -87,8 +93,8 @@ export const ScheduleStartPage: FC = () => {
const outbound: { departure: string; arrival: string; dateFrom: string; dateTo: string; timeFrom?: string; timeTo?: string } = {
departure: dep, arrival: arr, dateFrom: dateFromParam, dateTo: dateToParam,
};
if (timeFrom) outbound.timeFrom = timeFrom;
if (timeTo) outbound.timeTo = timeTo;
if (timeRange[0] > 0) outbound.timeFrom = minutesToTime(timeRange[0]).replace(":", "");
if (timeRange[1] < 1440) outbound.timeTo = minutesToTime(timeRange[1]).replace(":", "");
if (isRoundTrip) {
if (!returnDateFrom || !returnDateTo) return;
@@ -98,8 +104,8 @@ export const ScheduleStartPage: FC = () => {
const inbound: { departure: string; arrival: string; dateFrom: string; dateTo: string; timeFrom?: string; timeTo?: string } = {
departure: arr, arrival: dep, dateFrom: retDateFromParam, dateTo: retDateToParam,
};
if (returnTimeFrom) inbound.timeFrom = returnTimeFrom;
if (returnTimeTo) inbound.timeTo = returnTimeTo;
if (returnTimeRange[0] > 0) inbound.timeFrom = minutesToTime(returnTimeRange[0]).replace(":", "");
if (returnTimeRange[1] < 1440) inbound.timeTo = minutesToTime(returnTimeRange[1]).replace(":", "");
url = buildScheduleUrl({
type: "roundtrip",
@@ -115,7 +121,7 @@ export const ScheduleStartPage: FC = () => {
void navigate(`/${lang}/${url}`);
},
[departureAirport, arrivalAirport, dateFrom, dateTo, timeFrom, timeTo, isRoundTrip, returnDateFrom, returnDateTo, returnTimeFrom, returnTimeTo, navigate, lang],
[departureAirport, arrivalAirport, dateFrom, dateTo, timeRange, directOnly, isRoundTrip, returnDateFrom, returnDateTo, returnTimeRange, navigate, lang],
);
const handlePopularRequestClick = useCallback((_request: PopularRequest) => {
@@ -164,8 +170,9 @@ export const ScheduleStartPage: FC = () => {
<label htmlFor="schedule-date-from">{t("SHARED.DEPARTURE_DATE")}</label>
<Calendar
value={dateFrom}
onChange={(e) => setDateFrom(e.value as Date)}
onChange={(e) => setDateFrom(e.value as Date | null)}
dateFormat="dd.mm.yy"
placeholder={t("SHARED.DATE_FORMAT")}
showIcon
className="input--filter"
inputId="schedule-date-from"
@@ -177,8 +184,9 @@ export const ScheduleStartPage: FC = () => {
<label htmlFor="schedule-date-to">{t("SHARED.ARRIVAL_DATE")}</label>
<Calendar
value={dateTo}
onChange={(e) => setDateTo(e.value as Date)}
onChange={(e) => setDateTo(e.value as Date | null)}
dateFormat="dd.mm.yy"
placeholder={t("SHARED.DATE_FORMAT")}
showIcon
className="input--filter"
inputId="schedule-date-to"
@@ -186,27 +194,33 @@ export const ScheduleStartPage: FC = () => {
/>
</div>
<div className="schedule-start__field schedule-start__time-row">
<label htmlFor="schedule-time-from">{t("SHARED.DEPARTURE_TIME")}</label>
<div className="schedule-start__time-inputs">
<input
type="time"
id="schedule-time-from"
value={timeFrom}
onChange={(e) => setTimeFrom(e.target.value)}
className="input--filter"
data-testid="time-from-input"
/>
<span className="schedule-start__time-sep">&mdash;</span>
<input
type="time"
id="schedule-time-to"
value={timeTo}
onChange={(e) => setTimeTo(e.target.value)}
className="input--filter"
data-testid="time-to-input"
<div className="wrapper--time-selector full-view" data-testid="time-selector">
<div className="time-selector__label">{t("SHARED.DEPARTURE_TIME")}</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 className="time-selector__value">
{minutesToTime(timeRange[0])} {minutesToTime(timeRange[1])}
</div>
</div>
<div className="schedule-start__field">
<label>
<input
type="checkbox"
checked={directOnly}
onChange={(e) => setDirectOnly(e.target.checked)}
data-testid="direct-only-toggle"
/>
{t("SHARED.DIRECT_FLIGHT_ONLY")}
</label>
</div>
<div className="schedule-start__field">
@@ -249,27 +263,21 @@ export const ScheduleStartPage: FC = () => {
/>
</div>
<div className="schedule-start__field schedule-start__time-row">
<label htmlFor="schedule-return-time-from">{t("SHARED.RETURN_FLIGHT_TIME")}</label>
<div className="schedule-start__time-inputs">
<input
type="time"
id="schedule-return-time-from"
value={returnTimeFrom}
onChange={(e) => setReturnTimeFrom(e.target.value)}
className="input--filter"
data-testid="return-time-from-input"
/>
<span className="schedule-start__time-sep">&mdash;</span>
<input
type="time"
id="schedule-return-time-to"
value={returnTimeTo}
onChange={(e) => setReturnTimeTo(e.target.value)}
className="input--filter"
data-testid="return-time-to-input"
<div className="wrapper--time-selector full-view" data-testid="return-time-selector">
<div className="time-selector__label">{t("SHARED.RETURN_FLIGHT_TIME")}</div>
<div className="time-selector">
<Slider
value={returnTimeRange}
onChange={(e: SliderChangeEvent) => setReturnTimeRange(e.value as [number, number])}
range
min={0}
max={1440}
step={60}
/>
</div>
<div className="time-selector__value">
{minutesToTime(returnTimeRange[0])} {minutesToTime(returnTimeRange[1])}
</div>
</div>
</>
)}
+5 -5
View File
@@ -71,7 +71,6 @@
}
&__description {
max-width: 480px;
color: colors.$light-gray;
line-height: 1.5;
}
@@ -175,12 +174,13 @@
}
&--secondary {
background-color: colors.$blue-dark;
color: colors.$white;
border: none;
background: transparent;
color: colors.$blue-light;
border: 1.3px solid colors.$blue-light;
&:hover {
opacity: 0.9;
border-color: colors.$blue;
color: colors.$blue;
}
}