Add Туда/Обратно direction switch to round-trip schedule page

Round-trip schedules used to render outbound and inbound lists
stacked. Mirror Angular's schedule-direction-switch: keep both
fetches running, but render only the active direction's list and add
a button group to the sticky header that swaps which one is shown.
WeekTabs track the active direction's week independently, and tab
navigation updates whichever direction is currently active.
This commit is contained in:
2026-04-20 00:21:20 +03:00
parent ddc8e9f6dc
commit b21ae2638b
2 changed files with 151 additions and 31 deletions
@@ -26,6 +26,57 @@
}
&__inbound {
border-top: 1px solid colors.$border;
border-top: 0;
}
}
// `Туда / Обратно` segmented switch in the sticky header for round-
// trip schedules. Mirrors Angular's `schedule-direction-switch`: two
// flat buttons with a plane icon, the active one filled white.
.schedule-direction-switch {
display: flex;
gap: 0;
background: #f6f9fd;
border-bottom: 1px solid colors.$border;
&__btn {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
background: transparent;
border: 0;
border-right: 1px solid colors.$border;
color: #6b7280;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 120ms ease, color 120ms ease;
&:last-child { border-right: 0; }
&:hover {
background: #eef3fa;
color: colors.$blue;
}
&--active {
background: #fff;
color: colors.$blue;
.schedule-direction-switch__icon { color: colors.$blue; }
}
}
&__icon {
color: #6b7280;
transition: color 120ms ease;
fill: currentColor;
&--flipped {
transform: rotate(180deg);
}
}
}
@@ -9,7 +9,7 @@
*/
import type { FC } from "react";
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "@modern-js/runtime/router";
import { useLocale } from "@/i18n/useLocale.js";
import { useTranslation } from "@/i18n/provider.js";
@@ -122,6 +122,15 @@ export const ScheduleSearchPage: FC<ScheduleSearchPageProps> = ({ params }) => {
const outbound = params.outbound;
const inbound = params.type === "roundtrip" ? params.inbound : undefined;
// Round-trip schedules render only one direction at a time, with a
// `Туда / Обратно` switcher in the sticky header — matches Angular's
// `schedule-direction-switch` (one-of-two button group). Default to
// outbound; selection is local UI state, not URL-bound.
const [direction, setDirection] = useState<"outbound" | "inbound">("outbound");
const activeDirection = direction === "inbound" && inbound ? inbound : outbound;
const activeKind: "outbound" | "inbound" =
direction === "inbound" && inbound ? "inbound" : "outbound";
// Persist this search into the `Вы искали` sidebar history. The hook
// dedupes by URL so re-renders / week-tab clicks won't bloat storage.
useEffect(() => {
@@ -192,7 +201,8 @@ export const ScheduleSearchPage: FC<ScheduleSearchPageProps> = ({ params }) => {
const _loading = outboundLoading || (inbound ? inboundLoading : false);
/** Navigate to a different week (Monday → Sunday range). */
/** Navigate to a different week (Monday → Sunday range). Updates the
* active direction's params and preserves the other direction. */
const handleWeekChange = useCallback(
(mondayYmd: string) => {
const monday = new Date(mondayYmd);
@@ -204,25 +214,33 @@ export const ScheduleSearchPage: FC<ScheduleSearchPageProps> = ({ params }) => {
const day = String(d.getDate()).padStart(2, "0");
return `${y}${m}${day}`;
};
const newOutbound = {
...outbound,
dateFrom: ymd(monday),
dateTo: ymd(sunday),
};
const newParams: ScheduleParams = inbound
? { type: "roundtrip", outbound: newOutbound, inbound }
: { type: "route", outbound: newOutbound };
const next = { dateFrom: ymd(monday), dateTo: ymd(sunday) };
let newParams: ScheduleParams;
if (inbound && activeKind === "inbound") {
newParams = {
type: "roundtrip",
outbound,
inbound: { ...inbound, ...next },
};
} else if (inbound) {
newParams = {
type: "roundtrip",
outbound: { ...outbound, ...next },
inbound,
};
} else {
newParams = { type: "route", outbound: { ...outbound, ...next } };
}
const url = buildScheduleUrl(newParams);
void navigate(`/${locale}/${url}`);
},
[navigate, locale, outbound, inbound],
[navigate, locale, outbound, inbound, activeKind],
);
/** Convert the current outbound `dateFrom` (yyyyMMdd) → Monday yyyy-MM-dd
* so the WeekTabs highlights the right tab. The dateFrom is whatever
* the URL ships, so floor to the surrounding Monday. */
/** Convert the active direction's `dateFrom` (yyyyMMdd) → Monday
* yyyy-MM-dd so the WeekTabs highlights the right tab. */
const selectedMonday = (() => {
const f = outbound.dateFrom;
const f = activeDirection.dateFrom;
if (!f || f.length !== 8) return "";
const date = new Date(
parseInt(f.slice(0, 4), 10),
@@ -275,10 +293,64 @@ export const ScheduleSearchPage: FC<ScheduleSearchPageProps> = ({ params }) => {
</>
}
stickyContent={
<WeekTabs
selectedMonday={selectedMonday}
onNavigate={handleWeekChange}
/>
<>
<WeekTabs
selectedMonday={selectedMonday}
onNavigate={handleWeekChange}
/>
{inbound && (
<div
className="schedule-direction-switch"
data-testid="schedule-direction-switch"
role="tablist"
>
<button
type="button"
className={`schedule-direction-switch__btn${
activeKind === "outbound"
? " schedule-direction-switch__btn--active"
: ""
}`}
onClick={() => setDirection("outbound")}
role="tab"
aria-selected={activeKind === "outbound"}
data-testid="direction-switch-outbound"
>
<svg
className="schedule-direction-switch__icon"
width="16"
height="16"
aria-hidden="true"
>
<use xlinkHref="/assets/img/sprite.svg#plane" />
</svg>
{t("SHARED.DIRECT_FLIGHTS")}
</button>
<button
type="button"
className={`schedule-direction-switch__btn${
activeKind === "inbound"
? " schedule-direction-switch__btn--active"
: ""
}`}
onClick={() => setDirection("inbound")}
role="tab"
aria-selected={activeKind === "inbound"}
data-testid="direction-switch-inbound"
>
<svg
className="schedule-direction-switch__icon schedule-direction-switch__icon--flipped"
width="16"
height="16"
aria-hidden="true"
>
<use xlinkHref="/assets/img/sprite.svg#plane" />
</svg>
{t("SHARED.RETURN_FLIGHTS")}
</button>
</div>
)}
</>
}
>
{outboundError && (
@@ -291,18 +363,15 @@ export const ScheduleSearchPage: FC<ScheduleSearchPageProps> = ({ params }) => {
)}
<section className="frame">
<div className="schedule-search__outbound" data-testid="outbound-results">
<DayGroupedFlightList
flights={outboundSimple}
loading={outboundLoading}
/>
</div>
{inbound && (
{activeKind === "outbound" ? (
<div className="schedule-search__outbound" data-testid="outbound-results">
<DayGroupedFlightList
flights={outboundSimple}
loading={outboundLoading}
/>
</div>
) : (
<div className="schedule-search__inbound" data-testid="inbound-results">
<h2>
{t("SCHEDULE.RETURN")}: {inbound.departure} &rarr; {inbound.arrival}
</h2>
<DayGroupedFlightList
flights={inboundSimple}
loading={inboundLoading}