diff --git a/src/features/flights-map/components/FlightsMapFilter.tsx b/src/features/flights-map/components/FlightsMapFilter.tsx index 79832c42..e51db39c 100644 --- a/src/features/flights-map/components/FlightsMapFilter.tsx +++ b/src/features/flights-map/components/FlightsMapFilter.tsx @@ -187,36 +187,43 @@ export const FlightsMapFilter: FC = ({ /> -
- +
+

{t("FLIGHTS-MAP.FILTER_INFO")}

+
-
); diff --git a/src/features/flights-map/components/FlightsMapStartPage.scss b/src/features/flights-map/components/FlightsMapStartPage.scss index 3536126a..482fcfb7 100644 --- a/src/features/flights-map/components/FlightsMapStartPage.scss +++ b/src/features/flights-map/components/FlightsMapStartPage.scss @@ -47,18 +47,68 @@ height: 500px; } + &__spinner, &__loader, &__error, &__empty { padding: vars.$space-xl; text-align: center; color: colors.$light-gray; + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; } &__error { color: colors.$red; } } + + // Inline loader matching Angular loader-sheet component + .page-loader__loader { + position: relative; + width: 120px; + height: 120px; + transform: scale(0.5); + + .loader-circle { + position: absolute; + left: 50%; + top: 50%; + width: 120px; + height: 120px; + border-radius: 50%; + box-shadow: inset 0 0 0 3px colors.$blue-light; + margin-left: -60px; + margin-top: -60px; + } + + .loader-line-mask { + position: absolute; + left: 50%; + top: 50%; + width: 60px; + height: 120px; + margin-left: -60px; + margin-top: -60px; + overflow: hidden; + transform-origin: 60px 60px; + animation: flights-map-rotate 1.2s infinite linear; + + .loader-line { + width: 120px; + height: 120px; + border-radius: 50%; + box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.5); + } + } + + @keyframes flights-map-rotate { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + } } // Filter panel styling for content-left column @@ -120,6 +170,16 @@ } } + &__info { + padding: vars.$space-s 0; + + p { + @include fonts.font-small(colors.$gray); + line-height: 1.5; + margin: 0; + } + } + &__toggles { display: flex; flex-direction: column; @@ -133,4 +193,10 @@ @include fonts.font-small(colors.$gray); } } + + &__toggle--disabled { + opacity: 0.5; + cursor: not-allowed !important; + pointer-events: none; + } } diff --git a/src/features/flights-map/components/FlightsMapStartPage.tsx b/src/features/flights-map/components/FlightsMapStartPage.tsx index 8a8ef6ab..9d2a65c7 100644 --- a/src/features/flights-map/components/FlightsMapStartPage.tsx +++ b/src/features/flights-map/components/FlightsMapStartPage.tsx @@ -10,6 +10,7 @@ */ import { type FC, lazy, Suspense, useState, useCallback, useMemo } from "react"; +import { useParams } from "@modern-js/runtime/router"; import { useTranslation } from "@/i18n/provider.js"; import { PageLayout } from "@/ui/layout/PageLayout.js"; import { PageTabs } from "@/ui/layout/PageTabs.js"; @@ -61,6 +62,8 @@ function addMonthsYyyymmdd(base: string, months: number): string { export const FlightsMapStartPage: FC = () => { const env = getEnv(); const { t } = useTranslation(); + const routeParams = useParams<{ lang: string }>(); + const lang = routeParams.lang ?? "ru"; const [filterState, setFilterState] = useState({ connections: false, @@ -138,6 +141,9 @@ export const FlightsMapStartPage: FC = () => { {t("FLIGHTS-MAP.TITLE")} } + breadcrumbs={[ + { label: t("FLIGHTS-MAP.TITLE"), url: `/${lang}/flights-map` }, + ]} contentLeft={ {
- Loading map... +
+
+
+
+
+
+
} > - Loading map... +
+
+
+
+
+
+
} > @@ -178,7 +194,12 @@ export const FlightsMapStartPage: FC = () => { aria-busy="true" data-testid="map-loader" > - Loading routes... +
+
+
+
+
+
)} diff --git a/src/features/schedule/components/ScheduleStartPage.scss b/src/features/schedule/components/ScheduleStartPage.scss index 2779d2c7..a4fb0e9b 100644 --- a/src/features/schedule/components/ScheduleStartPage.scss +++ b/src/features/schedule/components/ScheduleStartPage.scss @@ -151,6 +151,62 @@ } } + .schedule-start__time-row { + .schedule-start__time-inputs { + display: flex; + align-items: center; + gap: vars.$space-s; + + input[type="time"] { + @include shadows.control-border-shadow(); + height: vars.$standard-button-height; + padding: 0 vars.$space-m; + font-size: fonts.$font-size-l; + font-weight: fonts.$font-regular; + color: colors.$text-color; + flex: 1; + min-width: 0; + transition-duration: 0.2s; + + &:focus { + outline: none; + border-color: colors.$blue-light; + box-shadow: 0 0 0 0.2em colors.$focus-shadow; + } + } + } + + .schedule-start__time-sep { + color: colors.$gray; + flex-shrink: 0; + } + } + + .bottom-description-frame { + padding: vars.$space-xl !important; + margin-top: vars.$space-xl; + + .bottom-description-container { + .bottom-description-title { + margin-bottom: vars.$space-m; + } + + .bottom-description-text { + color: colors.$gray; + line-height: 1.6; + + p { + margin-bottom: vars.$space-m; + } + + a { + color: colors.$blue-light; + text-decoration: underline; + } + } + } + } + .schedule-start__submit { margin-top: vars.$space-xl; width: 100%; diff --git a/src/features/schedule/components/ScheduleStartPage.tsx b/src/features/schedule/components/ScheduleStartPage.tsx index 7b62ec72..c6c0db1c 100644 --- a/src/features/schedule/components/ScheduleStartPage.tsx +++ b/src/features/schedule/components/ScheduleStartPage.tsx @@ -15,6 +15,7 @@ import { useTranslation } from "@/i18n/provider.js"; import { useCitySearch, type CitySuggestion } from "@/shared/hooks/useCitySearch.js"; import { PageLayout } from "@/ui/layout/PageLayout.js"; import { PageTabs } from "@/ui/layout/PageTabs.js"; +import { SearchHistory } from "@/ui/layout/SearchHistory.js"; import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js"; import type { PopularRequest } from "@/features/popular-requests/types.js"; import { buildScheduleUrl } from "../url.js"; @@ -45,9 +46,13 @@ export const ScheduleStartPage: FC = () => { const [arrivalAirport, setArrivalAirport] = useState(""); const [dateFrom, setDateFrom] = useState(today); const [dateTo, setDateTo] = useState(addDays(today, 7)); + const [timeFrom, setTimeFrom] = useState(""); + const [timeTo, setTimeTo] = useState(""); const [isRoundTrip, setIsRoundTrip] = useState(false); const [returnDateFrom, setReturnDateFrom] = useState(addDays(today, 7)); const [returnDateTo, setReturnDateTo] = useState(addDays(today, 14)); + const [returnTimeFrom, setReturnTimeFrom] = useState(""); + const [returnTimeTo, setReturnTimeTo] = useState(""); // City autocomplete search const { suggestions: departureSuggestions, search: searchDeparture } = useCitySearch(); @@ -79,26 +84,38 @@ export const ScheduleStartPage: FC = () => { let url: string; + 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 (isRoundTrip) { if (!returnDateFrom || !returnDateTo) return; const retDateFromParam = dateToYyyymmdd(returnDateFrom); const retDateToParam = dateToYyyymmdd(returnDateTo); + 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; + url = buildScheduleUrl({ type: "roundtrip", - outbound: { departure: dep, arrival: arr, dateFrom: dateFromParam, dateTo: dateToParam }, - inbound: { departure: arr, arrival: dep, dateFrom: retDateFromParam, dateTo: retDateToParam }, + outbound, + inbound, }); } else { url = buildScheduleUrl({ type: "route", - outbound: { departure: dep, arrival: arr, dateFrom: dateFromParam, dateTo: dateToParam }, + outbound, }); } void navigate(`/${lang}/${url}`); }, - [departureAirport, arrivalAirport, dateFrom, dateTo, isRoundTrip, returnDateFrom, returnDateTo, navigate, lang], + [departureAirport, arrivalAirport, dateFrom, dateTo, timeFrom, timeTo, isRoundTrip, returnDateFrom, returnDateTo, returnTimeFrom, returnTimeTo, navigate, lang], ); const handlePopularRequestClick = useCallback((_request: PopularRequest) => { @@ -169,6 +186,29 @@ export const ScheduleStartPage: FC = () => { />
+
+ +
+ setTimeFrom(e.target.value)} + className="input--filter" + data-testid="time-from-input" + /> + + setTimeTo(e.target.value)} + className="input--filter" + data-testid="time-to-input" + /> +
+
+
+ +
+ +
+ setReturnTimeFrom(e.target.value)} + className="input--filter" + data-testid="return-time-from-input" + /> + + setReturnTimeTo(e.target.value)} + className="input--filter" + data-testid="return-time-to-input" + /> +
+
)} @@ -232,7 +295,15 @@ export const ScheduleStartPage: FC = () => { {t("SCHEDULE.TITLE")} } - contentLeft={scheduleFilter} + breadcrumbs={[ + { label: t("SCHEDULE.TITLE"), url: `/${lang}/schedule` }, + ]} + contentLeft={ + <> + {scheduleFilter} + + + } >

{t("SCHEDULE.SCHEDULE-START")}

@@ -269,6 +340,20 @@ export const ScheduleStartPage: FC = () => {
+ +
+
+

+ {t("SCHEDULE.SCHEDULE-BOTTOM-DESCRIPTION")} +

+
+
+
);