diff --git a/src/ui/flights/DurationDisplay.scss b/src/ui/flights/DurationDisplay.scss new file mode 100644 index 00000000..f3b32e3b --- /dev/null +++ b/src/ui/flights/DurationDisplay.scss @@ -0,0 +1,10 @@ +@use "../../styles/fonts" as fonts; +@use "../../styles/colors" as colors; + +.duration { + @include fonts.font-overflow; + + &--clarifying { + color: colors.$orange; + } +} diff --git a/src/ui/flights/DurationDisplay.tsx b/src/ui/flights/DurationDisplay.tsx index cb5e90a2..441bbc0b 100644 --- a/src/ui/flights/DurationDisplay.tsx +++ b/src/ui/flights/DurationDisplay.tsx @@ -1,5 +1,6 @@ import type { FC } from "react"; import { formatDuration } from "@/shared/utils/datetime/index.js"; +import "./DurationDisplay.scss"; export interface DurationDisplayProps { /** Flight duration in total minutes */ @@ -16,6 +17,6 @@ export const DurationDisplay: FC = ({ locale = "en", }) => { return ( - {formatDuration(minutes, locale)} + {formatDuration(minutes, locale)} ); }; diff --git a/src/ui/flights/FlightCard.scss b/src/ui/flights/FlightCard.scss new file mode 100644 index 00000000..5d2c5720 --- /dev/null +++ b/src/ui/flights/FlightCard.scss @@ -0,0 +1,68 @@ +@use "../../styles/variables" as vars; +@use "../../styles/colors" as colors; +@use "../../styles/fonts" as fonts; +@use "../../styles/screen" as screen; + +.flight-card { + display: flex; + align-items: center; + padding: vars.$space-xl 0; + margin: 0 vars.$space-xl; + justify-content: space-between; + + & + & { + border-top: 1px dashed colors.$border; + } + + &__number { + width: vars.$width-flight-number; + font-weight: fonts.$font-medium; + } + + &__route { + display: flex; + flex: 1; + align-items: center; + justify-content: space-between; + } + + &__departure, + &__arrival { + display: flex; + align-items: center; + gap: vars.$space-m; + width: vars.$width-dep-arr; + } + + &__duration { + width: vars.$width-flight-time; + text-align: center; + } + + &__status { + width: vars.$status-width; + text-align: right; + } + + @include screen.mobile { + flex-direction: column; + align-items: flex-start; + gap: vars.$space-m; + + &__route { + flex-direction: column; + gap: vars.$space-m; + width: 100%; + } + + &__departure, + &__arrival { + width: 100%; + } + + &__status { + width: 100%; + text-align: left; + } + } +} diff --git a/src/ui/flights/FlightCard.tsx b/src/ui/flights/FlightCard.tsx index 4cfd87eb..e21539f3 100644 --- a/src/ui/flights/FlightCard.tsx +++ b/src/ui/flights/FlightCard.tsx @@ -4,6 +4,7 @@ import { StationDisplay } from "./StationDisplay.js"; import { TimeGroup } from "./TimeGroup.js"; import { FlightStatus } from "./FlightStatus.js"; import { DurationDisplay } from "./DurationDisplay.js"; +import "./FlightCard.scss"; export interface FlightCardProps { flight: ISimpleFlight; diff --git a/src/ui/flights/FlightList.scss b/src/ui/flights/FlightList.scss new file mode 100644 index 00000000..c4be55d2 --- /dev/null +++ b/src/ui/flights/FlightList.scss @@ -0,0 +1,14 @@ +@use "../../styles/colors" as colors; +@use "../../styles/fonts" as fonts; + +.flight-list { + &--empty { + padding: 40px 20px; + text-align: center; + } + + &__empty-message { + color: colors.$light-gray; + font-size: fonts.$font-size-l; + } +} diff --git a/src/ui/flights/FlightList.tsx b/src/ui/flights/FlightList.tsx index 4f9395bc..2b5debed 100644 --- a/src/ui/flights/FlightList.tsx +++ b/src/ui/flights/FlightList.tsx @@ -2,6 +2,7 @@ import type { FC } from "react"; import type { ISimpleFlight } from "@/features/online-board/types.js"; import { FlightCard } from "./FlightCard.js"; import { FlightListSkeleton } from "./FlightListSkeleton.js"; +import "./FlightList.scss"; export interface FlightListProps { /** Array of flights to display */ diff --git a/src/ui/flights/FlightListSkeleton.scss b/src/ui/flights/FlightListSkeleton.scss new file mode 100644 index 00000000..4d4595b8 --- /dev/null +++ b/src/ui/flights/FlightListSkeleton.scss @@ -0,0 +1,52 @@ +@use "../../styles/variables" as vars; +@use "../../styles/colors" as colors; + +@keyframes skeleton-pulse { + 0% { + opacity: 0.6; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0.6; + } +} + +.flight-list-skeleton { + &__row { + display: flex; + align-items: center; + padding: vars.$space-xl; + gap: vars.$space-m; + + &:not(:last-child) { + border-bottom: 1px dashed colors.$border; + } + } + + &__cell { + height: 16px; + background-color: colors.$blue-light2; + border-radius: vars.$border-radius; + animation: skeleton-pulse 1.5s ease-in-out infinite; + + &--number { + width: vars.$width-flight-number; + } + + &--station { + width: 80px; + } + + &--time { + width: 50px; + } + + &--status { + width: vars.$status-width; + } + } +} diff --git a/src/ui/flights/FlightListSkeleton.tsx b/src/ui/flights/FlightListSkeleton.tsx index aada5ea9..71a82fca 100644 --- a/src/ui/flights/FlightListSkeleton.tsx +++ b/src/ui/flights/FlightListSkeleton.tsx @@ -1,4 +1,5 @@ import type { FC } from "react"; +import "./FlightListSkeleton.scss"; export interface FlightListSkeletonProps { /** Number of skeleton rows to display (default: 5) */ diff --git a/src/ui/flights/FlightStatus.scss b/src/ui/flights/FlightStatus.scss new file mode 100644 index 00000000..0ee4b775 --- /dev/null +++ b/src/ui/flights/FlightStatus.scss @@ -0,0 +1,37 @@ +@use "../../styles/variables" as vars; +@use "../../styles/colors" as colors; + +.flight-status { + &__content { + display: flex; + align-items: center; + + & > *:not(:last-child) { + margin-right: vars.$space-s2; + } + } + + &__indicator { + width: vars.$status-indicator-size; + height: vars.$status-indicator-size; + border-radius: 50%; + display: inline-block; + background-color: colors.$light-gray; + border: 1px solid colors.$light-gray; + + &--Finished, + &--arrived, + &--landed, + &--cancelled { + background-color: colors.$red; + border-color: colors.$red; + } + + &--InProgress, + &--in-flight, + &--departed { + background-color: colors.$green; + border-color: colors.$green; + } + } +} diff --git a/src/ui/flights/FlightStatus.tsx b/src/ui/flights/FlightStatus.tsx index 5da6c58b..5574d19f 100644 --- a/src/ui/flights/FlightStatus.tsx +++ b/src/ui/flights/FlightStatus.tsx @@ -1,5 +1,6 @@ import type { FC } from "react"; import type { FlightStatus as FlightStatusType } from "@/features/online-board/types.js"; +import "./FlightStatus.scss"; export interface FlightStatusProps { status: FlightStatusType; diff --git a/src/ui/flights/StationDisplay.scss b/src/ui/flights/StationDisplay.scss new file mode 100644 index 00000000..de2b0e06 --- /dev/null +++ b/src/ui/flights/StationDisplay.scss @@ -0,0 +1,49 @@ +@use "../../styles/screen" as screen; + +.station { + display: flex; + flex-direction: column; + font-size: 0; + + &__city { + max-width: 100%; + } + + &__old-city { + text-decoration: line-through; + } + + &__terminal { + @include screen.smTablet { + max-height: initial; + } + } + + &--right { + align-items: flex-end; + + .station__terminal { + text-align: right; + } + } + + &--mobile-right { + @include screen.mobile { + align-items: flex-end; + + .station__terminal { + text-align: right; + } + } + } + + &--mobile-left { + @include screen.gt-mobile { + align-items: flex-end; + } + } + + &--center { + align-items: center; + } +} diff --git a/src/ui/flights/StationDisplay.tsx b/src/ui/flights/StationDisplay.tsx index 7c2b319e..bbc61c0c 100644 --- a/src/ui/flights/StationDisplay.tsx +++ b/src/ui/flights/StationDisplay.tsx @@ -1,5 +1,6 @@ import type { FC } from "react"; import { useCityName } from "@/shared/hooks/useDictionaries.js"; +import "./StationDisplay.scss"; export interface StationDisplayProps { /** IATA airport code, e.g. "SVO" */ @@ -23,12 +24,12 @@ export const StationDisplay: FC = ({ const resolvedCity = cityName ?? useCityName(airportCode); return ( -
- {airportCode} +
+ {airportCode} {airportName ? ( - {airportName} + {airportName} ) : null} - {resolvedCity} + {resolvedCity}
); }; diff --git a/src/ui/flights/TimeGroup.scss b/src/ui/flights/TimeGroup.scss new file mode 100644 index 00000000..4231ffc2 --- /dev/null +++ b/src/ui/flights/TimeGroup.scss @@ -0,0 +1,171 @@ +@use "../../styles/fonts" as fonts; +@use "../../styles/colors" as colors; +@use "../../styles/variables" as vars; +@use "../../styles/screen" as screen; + +.time-group { + display: inline-flex; + + &__times { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + &__actual { + display: flex; + align-items: flex-start; + } + + &__utc { + margin-top: 2px; + margin-left: vars.$space-s; + } + + &__day-change { + margin-top: vars.$space-s; + margin-left: vars.$space-s; + } + + &__old-time, + &__scheduled--delayed { + text-decoration: line-through; + } + + &__specifying { + display: flex; + align-items: center; + } + + // Size variants + &--medium { + .time-group__day-change { + margin-top: 3px; + } + } + + &--small, + &--extra-small { + .time-group__utc { + margin-top: 0; + font-size: fonts.$font-size-xs; + line-height: 13px; + } + + .time-group__day-change { + margin-top: 1px; + } + } + + // Positioning variants + &--right { + justify-content: flex-end; + + .time-group__day-change { + order: 1; + margin-right: vars.$space-s; + margin-left: 0; + } + + .time-group__times { + order: 2; + align-items: flex-end; + } + } + + &--day-change-left { + .time-group__day-change { + order: 1; + margin-right: vars.$space-s; + margin-left: 0; + } + + .time-group__times { + order: 2; + } + } + + &--day-change-mobile-left { + @include screen.mobile { + .time-group__day-change { + order: 1; + margin-right: vars.$space-s; + margin-left: 0; + } + + .time-group__times { + order: 2; + } + } + } + + &--day-change-mobile-right { + @include screen.gt-mobile { + .time-group__day-change { + order: 1; + margin-right: vars.$space-s; + margin-left: 0; + } + + .time-group__times { + order: 2; + } + } + } + + &--mobile-right { + @include screen.mobile { + .time-group__day-change { + order: 1; + margin-right: vars.$space-s; + margin-left: 0; + } + + .time-group__times { + order: 2; + align-items: flex-end; + } + } + } + + &--mobile-left { + @include screen.gt-mobile { + .time-group__day-change { + order: 1; + margin-right: vars.$space-s; + margin-left: 0; + } + + .time-group__times { + order: 2; + align-items: flex-end; + } + } + } + + &--day-change-absolute { + position: relative; + + .time-group__day-change { + position: absolute; + top: 0; + left: 100%; + } + } + + &--mobile-right.time-group--day-change-absolute { + @include screen.mobile { + .time-group__day-change { + left: auto; + right: 100%; + } + } + } + + &--right.time-group--day-change-absolute { + .time-group__day-change { + left: auto; + right: 100%; + } + } +} diff --git a/src/ui/flights/TimeGroup.tsx b/src/ui/flights/TimeGroup.tsx index 3d0d7e29..1c3631ce 100644 --- a/src/ui/flights/TimeGroup.tsx +++ b/src/ui/flights/TimeGroup.tsx @@ -1,5 +1,6 @@ import type { FC } from "react"; import { formatTime } from "@/shared/utils/datetime/index.js"; +import "./TimeGroup.scss"; export interface TimeGroupProps { /** Scheduled time (ISO 8601 string) */