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:
@@ -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} → {inbound.arrival}
|
||||
</h2>
|
||||
<DayGroupedFlightList
|
||||
flights={inboundSimple}
|
||||
loading={inboundLoading}
|
||||
|
||||
Reference in New Issue
Block a user