From 31751d0e842058b280dfbed7eb86e4a4b2d24059 Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 22 Apr 2026 13:45:40 +0300 Subject: [PATCH] =?UTF-8?q?'=D0=9A=D1=83=D0=BF=D0=B8=D1=82=D1=8C=20=D0=B1?= =?UTF-8?q?=D0=B8=D0=BB=D0=B5=D1=82'=20hover=20link=20+=20anchor=20semanti?= =?UTF-8?q?cs=20(TIRREDESIGN-6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Buy action is now an instead of a
- {/* TZ §4.1.14.4.4 – Buy: visible only within 2h–330d window */} - {buyVisible && ( - + )} {/* TZ §4.1.14.4.5 – Status: visible only on the departure day */} {statusVisible && ( diff --git a/src/features/schedule/components/ScheduleSearchPage.tsx b/src/features/schedule/components/ScheduleSearchPage.tsx index f838dd30..78c65441 100644 --- a/src/features/schedule/components/ScheduleSearchPage.tsx +++ b/src/features/schedule/components/ScheduleSearchPage.tsx @@ -174,15 +174,16 @@ export const ScheduleSearchPage: FC = ({ params }) => { [locale, navigate, outbound, inbound], ); - // `Купить` button — opens Aeroflot's booking flow in a new tab, same - // URL shape as BoardDetailsHeader's BuyTicketButton: + // Builds the Aeroflot booking URL for a flight — shares shape with + // BoardDetailsHeader's BuyTicketButton: // https://www.aeroflot.ru/sb/app/{lang}-{lang}#/search?…&routes={dep}.{yyyyMMdd}.{arr} - const handleBuy = useCallback( - (flight: ISimpleFlight) => { + // Returns null when the flight lacks the data we need to assemble it. + const buyUrlFor = useCallback( + (flight: ISimpleFlight): string | null => { const legs = flight.routeType === "Direct" ? [flight.leg] : flight.legs; const firstLeg = legs[0]; const lastLeg = legs[legs.length - 1]; - if (!firstLeg || !lastLeg) return; + if (!firstLeg || !lastLeg) return null; const dep = firstLeg.departure.scheduled.airportCode; const arr = lastLeg.arrival.scheduled.airportCode; const depUtc = firstLeg.departure.times.scheduledDeparture.utc; @@ -190,10 +191,7 @@ export const ScheduleSearchPage: FC = ({ params }) => { const yyyy = depDate.getFullYear().toString(); const mm = (depDate.getMonth() + 1).toString().padStart(2, "0"); const dd = depDate.getDate().toString().padStart(2, "0"); - const url = `https://www.aeroflot.ru/sb/app/${language}-${language}#/search?adults=1&cabin=economy&children=0&infants=0&routes=${dep}.${yyyy}${mm}${dd}.${arr}&autosearch=Y`; - if (typeof window !== "undefined") { - window.open(url, "_blank", "noopener,noreferrer"); - } + return `https://www.aeroflot.ru/sb/app/${language}-${language}#/search?adults=1&cabin=economy&children=0&infants=0&routes=${dep}.${yyyy}${mm}${dd}.${arr}&autosearch=Y`; }, [language], ); @@ -499,7 +497,7 @@ export const ScheduleSearchPage: FC = ({ params }) => { flights={outboundSimple} loading={outboundLoading} onFlightClick={handleFlightClick} - onBuy={handleBuy} + buyUrlFor={buyUrlFor} />
) : ( @@ -508,7 +506,7 @@ export const ScheduleSearchPage: FC = ({ params }) => { flights={inboundSimple} loading={inboundLoading} onFlightClick={handleFlightClick} - onBuy={handleBuy} + buyUrlFor={buyUrlFor} /> )} diff --git a/src/ui/flights/FlightCard.scss b/src/ui/flights/FlightCard.scss index da2b540a..8dbcefa7 100644 --- a/src/ui/flights/FlightCard.scss +++ b/src/ui/flights/FlightCard.scss @@ -43,6 +43,7 @@ // plus `padding: $space-l $space-xl` (15/20px) — matches Angular's // vertical rhythm on each card. &__row { + position: relative; display: grid; grid-template-columns: 60px 120px 100px 1fr minmax(85px, 145px) 120px 1fr 10px; @@ -286,6 +287,41 @@ &:hover { background: colors.$orange--hover; } } + // TIRREDESIGN-6 / TZ §4.1.14.4.4: inline per-row "Купить билет" link + // that appears on hover near the arrival station. Overlays the row + // so the 8-column grid stays intact. Desktop only — touch devices get + // no hover affordance (Buy ticket remains available from details). + &__buy-link { + display: none; + position: absolute; + right: vars.$space-xl; + top: 50%; + transform: translateY(-50%); + padding: 6px 14px; + font-size: fonts.$font-size-s; + font-weight: fonts.$font-medium; + color: colors.$white; + background: colors.$orange; + border-radius: vars.$border-radius; + text-decoration: none; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease, background-color 0.2s ease; + z-index: 2; + + &:hover { background: colors.$orange--hover; } + } + + @media (hover: hover) { + &__buy-link { display: inline-flex; align-items: center; } + &__row:hover .flight-card__buy-link, + &__buy-link:focus-visible { + opacity: 1; + pointer-events: auto; + } + } + &__details-btn { background: colors.$blue; color: colors.$white; diff --git a/src/ui/flights/FlightCard.tsx b/src/ui/flights/FlightCard.tsx index a94d18d4..16eb5428 100644 --- a/src/ui/flights/FlightCard.tsx +++ b/src/ui/flights/FlightCard.tsx @@ -60,6 +60,14 @@ export interface FlightCardProps { * buttons Angular shows on the search results expansion. */ renderActions?: (flight: ISimpleFlight) => ReactNode; + /** + * Aeroflot booking URL for the per-row "Купить билет" hover link + * (TIRREDESIGN-6 / TZ §4.1.14.4.4). Rendered inside the collapsed row + * only on Schedule layouts; absent in the row flow on other directions. + * CSS shows the link on hover on desktop and hides it entirely on + * mobile / tablet. + */ + inlineBuyUrl?: string; } /** Extract the primary leg from a flight (first leg for multi-leg) */ @@ -131,6 +139,7 @@ export const FlightCard: FC = ({ direction = "route", renderExpandedBody, renderActions, + inlineBuyUrl, }) => { const { t } = useTranslation(); const { language } = useLocale(); @@ -341,6 +350,19 @@ export const FlightCard: FC = ({ /> + {inlineBuyUrl && ( + e.stopPropagation()} + > + {t("SHARED.BUY-TICKET")} + + )} + {(expandable || direction === "schedule") && (