Initial commit: Aeroflot Flights Web Angular 12 application

This commit is contained in:
2026-04-03 10:10:52 +03:00
commit 2342f2e66e
1311 changed files with 128350 additions and 0 deletions
@@ -0,0 +1,3 @@
<div class="app-show-debug p-print-none" *ngIf="settings.showDebugVersion">
{{ 'SHARED.TEST-VERSION' | translate }}
</div>
@@ -0,0 +1,11 @@
import { Component, Inject } from '@angular/core';
import { AppSettings } from '@app/shared/models-legacy';
import { APP_SETTINGS } from '@app/shared/services';
@Component({
selector: 'app-show-debug',
templateUrl: './app-show-debug.component.html',
})
export class AppShowDebugComponent {
constructor(@Inject(APP_SETTINGS) public settings: AppSettings) {
}
}
@@ -0,0 +1,4 @@
<div class="app-version p-print-none" [style.visibility]="isVisible? 'visible': 'hidden'">
<div>version - &nbsp;</div>
{{ apiVersion }}
</div>
@@ -0,0 +1,26 @@
.app-version {
position: fixed;
right: 0;
top: 0;
padding: 2px 16px;
background-color: #f37b09;
color: #fff;
font-size: 12px;
z-index: 100000;
opacity: 0.6;
transition-duration: 0.2s;
display: flex;
cursor: pointer;
}
.app-version:hover {
opacity: 1;
}
.app-version > div {
display: none;
}
.app-version:hover > div {
display: inline-block;
}
@@ -0,0 +1,23 @@
import { Component, OnInit } from '@angular/core';
import { NetworkService } from '@shared/services/network.service';
import { AppSettingsService } from '@shared/services';
@Component({
selector: 'app-version',
templateUrl: './app-version.component.html',
styleUrls: ['./app-version.component.scss'],
})
export class AppVersionComponent implements OnInit {
apiVersion: string;
isVisible: boolean;
constructor(private networkService: NetworkService, private settingsService: AppSettingsService) {}
async ngOnInit() {
this.apiVersion = await this.networkService.getApiVersion();
this.isVisible = (await this.settingsService.getSettings()).showDebugVersion;
if (this.isVisible) {
document.querySelector<HTMLElement>('.banner--top')!.style.display='block';
}
}
}
@@ -0,0 +1,94 @@
import { BuyTicketLogic } from './buy-ticket.logic';
import { FlightModel } from '../../../shared/models-legacy';
import { RouteTypeLegacy } from '../../../shared/enumerators';
import * as moment from 'moment';
describe('BuyLogic', () => {
let logic: BuyTicketLogic;
const in5Hours = moment().add(5, 'hour').toDate();
const in7Hours = moment().add(7, 'hour').toDate();
let window$: Window;
const invalid: FlightModel = {
routeType: RouteTypeLegacy.Direct,
leg: {
departure: { scheduled: { airportCode: 'RYZ' }, times: { scheduled: { utc: moment().add(100500, 'hour').toDate() } } },
arrival: { scheduled: { airportCode: 'PRG' } },
},
} as any;
const direct: FlightModel = {
routeType: RouteTypeLegacy.Direct,
leg: {
departure: { scheduled: { airportCode: 'RYZ' }, times: { scheduled: { utc: in5Hours, local: in5Hours } } },
arrival: { scheduled: { airportCode: 'PRG' } },
},
} as any;
const returning: FlightModel = {
routeType: RouteTypeLegacy.Direct,
leg: {
departure: { scheduled: { airportCode: 'RYZ' }, times: { scheduled: { utc: in7Hours, local: in7Hours } } },
arrival: { scheduled: { airportCode: 'PRG' } },
},
} as any;
beforeEach(() => {
window$ = {} as any;
const settings = {
buyPeriod: { min: -1, max: -10 },
} as any;
logic = new BuyTicketLogic(settings, window$);
});
it('should return isAnyAvailable for empty flights', () => {
expect(logic.isAnyAvailable()).toBeFalse();
expect(logic.isAnyAvailable(undefined as any, null as any)).toBeFalse();
});
it('should return isAvailable for valid flight', () => {
expect(logic.isAvailable(direct)).toBeTrue();
});
it('should return isAvailable true for valid flight', () => {
expect(logic.isAvailable(direct)).toBeTrue();
});
it('should return isAvailable false for valid flight', () => {
expect(logic.isAvailable(invalid)).toBeFalse();
});
it('should return isAnyAvailable true for 1 valid of 2 flight', () => {
expect(logic.isAnyAvailable(invalid, direct)).toBeTrue();
expect(logic.isAnyAvailable(direct, invalid)).toBeTrue();
});
it('should open single url', () => {
const blank = { focus: jasmine.createSpy().and.callThrough() };
window$.open = jasmine
.createSpy('open')
.withArgs(
`https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=RYZ.${moment(
in5Hours,
).format('YYYYMMDD')}.PRG&autosearch=Y`,
'_blank',
)
.and.returnValue(blank);
logic.buyAll(direct);
expect(window$.open).toHaveBeenCalled();
expect(blank.focus).toHaveBeenCalled();
});
it('should open double url', () => {
const blank = { focus: jasmine.createSpy().and.callThrough() };
const expectedUrl = `https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=RYZ.${moment(
in5Hours,
).format('YYYYMMDD')}.PRG-RYZ.${moment(in7Hours).format('YYYYMMDD')}.PRG&autosearch=Y`;
window$.open = jasmine.createSpy('open').withArgs(expectedUrl, '_blank').and.returnValue(blank);
logic.buyAll(direct, returning);
expect(window$.open).toHaveBeenCalled();
expect(blank.focus).toHaveBeenCalled();
});
});
@@ -0,0 +1 @@
<button class="color orange" pButton type="button" label="{{ 'SHARED.BUY-TICKET' | translate }}" (click)="buy(); $event.stopPropagation()"></button>
@@ -0,0 +1,7 @@
:host {
display: flex;
button {
flex: 1;
}
}
@@ -0,0 +1,47 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BuyTicketLogic } from './buy-ticket.logic';
import { BuyTicketButtonComponent } from './buy-ticket-button.component';
import { FlightModel } from '@shared/models-legacy';
import { getMockPipe } from '@shared/pipes/pipe.mock';
describe('BuyTicketButtonComponent', () => {
let component: BuyTicketButtonComponent;
let fixture: ComponentFixture<BuyTicketButtonComponent>;
let logic: BuyTicketLogic;
const direct: FlightModel = {} as any;
const returning: FlightModel = {} as any;
beforeEach(async () => {
logic = {} as any;
await TestBed.configureTestingModule({
declarations: [BuyTicketButtonComponent, getMockPipe('translate')],
providers: [
{
provide: BuyTicketLogic,
useFactory: () => logic,
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BuyTicketButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should buy', () => {
logic.buyAll = jasmine
.createSpy('buyAll')
.withArgs(direct, returning)
.and.callThrough();
component.direct = direct;
component.returning = returning;
component.buy();
expect(logic.buyAll).toHaveBeenCalled();
});
});
@@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
import { FlightModel } from '@app/shared/models-legacy';
import { BuyTicketLogic } from './buy-ticket.logic';
@Component({
selector: 'buy-ticket-button',
templateUrl: './buy-ticket-button.component.html',
styleUrls: ['./buy-ticket-button.component.scss']
})
export class BuyTicketButtonComponent {
@Input() direct: FlightModel;
@Input() returning: FlightModel;
constructor(private logic: BuyTicketLogic) {}
buy() {
this.logic.buyAll(this.direct, this.returning);
}
}
@@ -0,0 +1,82 @@
import { Inject, Injectable } from '@angular/core';
import { FlightStatusLegacy } from '@app/shared/enumerators';
import { AppSettings, FlightModel, getFirstLeg, getLastLeg } from '@app/shared/models-legacy';
import { APP_SETTINGS, WINDOW } from '@app/shared/services';
import * as moment from 'moment';
@Injectable({ providedIn: 'root' })
export class BuyTicketLogic {
constructor(@Inject(APP_SETTINGS) public settings: AppSettings, @Inject(WINDOW) private window$: Window) {}
isAnyAvailable(...flights: (FlightModel | undefined)[]) {
flights = (flights || []).filter((f) => !!f);
if (!flights.length) return false;
for (const flight of flights) if (this.isAvailable(flight)) return true;
return false;
}
isAvailable(flight?: FlightModel) {
const { min, max } = this.settings.buyPeriod;
if (!flight) return false;
if (flight.status === FlightStatusLegacy.cancelled) return false;
if (flight.status == FlightStatusLegacy.inFlight) return false;
const date = getFirstLeg(flight).departure.times.scheduled.utc;
const now = moment();
const from = moment(date).add(max, 'hour'); // max before flight
const to = moment(date).add(min, 'hour'); // min before flight
if (from < now && now < to) return true;
// console.log(`Buy ticket button is not available. departure utc: ${date}, from: ${from}, now: ${now}, to: ${to}`);
return false;
}
private open(url: string) {
this.window$.open(url, '_blank').focus();
}
public buyAll(direct: FlightModel, returning?: FlightModel) {
if (!direct) return;
const url = returning ? this.getLinkWithReturn(direct, returning) : this.getLink(direct);
this.open(url);
}
private getLink(flight: FlightModel) {
const firstLeg = getFirstLeg(flight);
const lastLeg = getLastLeg(flight);
const depAirport = firstLeg.departure.scheduled.airportCode;
const arrAirport = lastLeg.arrival.scheduled.airportCode;
const date = moment(firstLeg.departure.times.scheduled.local).format('YYYYMMDD');
const params = `${depAirport}.${date}.${arrAirport}`;
return `https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=${params}&autosearch=Y`;
}
private getLinkWithReturn(flight1: FlightModel, flight2: FlightModel) {
const firstLeg1 = getFirstLeg(flight1);
const lastLeg1 = getLastLeg(flight1);
const depAirport1 = firstLeg1.departure.scheduled.airportCode;
const arrAirport1 = lastLeg1.arrival.scheduled.airportCode;
const date1 = moment(firstLeg1.departure.times.scheduled.local).format('YYYYMMDD');
const firstLeg2 = getFirstLeg(flight2);
const lastLeg2 = getLastLeg(flight2);
const depAirport2 = firstLeg2.departure.scheduled.airportCode;
const arrAirport2 = lastLeg2.arrival.scheduled.airportCode;
const date2 = moment(firstLeg2.departure.times.scheduled.local).format('YYYYMMDD');
const params = `${depAirport1}.${date1}.${arrAirport1}-${depAirport2}.${date2}.${arrAirport2}`;
return `https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=${params}&autosearch=Y`;
}
}
@@ -0,0 +1,18 @@
<div [ngClass]="timeGroupClasses">
<text *ngIf="caption" color="light-gray" size="12" class="captioned-time-group__caption">
{{ caption | translate }}
</text>
<time-group
[bold]="bold"
[dayChangePosition]="dayChangePosition"
[dayChangeLayout]="dayChangeLayout"
[size]="size"
[scheduled]="times"
[withUTC]="withUTC"
[align]="align"
>
<text *ngIf="withDate" size="12" color="blue" class="captioned-time-group__date">
{{ date }}
</text>
</time-group>
</div>
@@ -0,0 +1,33 @@
@use 'src/styles/colors' as *;
@use 'src/styles/fonts' as *;
@use 'src/styles/screen' as *;
:host {
// prevents component from growing in height due to font-size inheritance
font-size: 0;
}
.captioned-time-group {
display: inline-flex;
flex-direction: column;
&__caption {
margin-bottom: 2px;
}
}
.captioned-time-group--right {
align-items: flex-end;
}
.captioned-time-group--mobile-right {
@include mobile {
align-items: flex-end;
}
}
.captioned-time-group--mobile-left {
@include gt-mobile {
align-items: flex-end;
}
}
@@ -0,0 +1,37 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { TimeGroupBaseComponent } from '@modules/components/time-group/time-group-base.component';
import { DATE_FORMAT } from '@shared/helpers';
import { ITimesSet } from '@typings/times';
import * as moment from 'moment';
@Component({
selector: 'captioned-time-group',
templateUrl: './captioned-time-group.component.html',
styleUrls: ['./captioned-time-group.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CaptionedTimeGroupComponent extends TimeGroupBaseComponent {
@Input() times: ITimesSet;
@Input() withDate = false;
@Input() caption: string;
constructor() {
super();
this.size = 'extra-small';
}
get date() {
const [date] = this.times.local.split('T');
return date
? moment(date).format(DATE_FORMAT)
: moment(this.times.local).format(DATE_FORMAT);
}
get timeGroupClasses() {
return {
'captioned-time-group': true,
[`captioned-time-group--${this.align}`]: this.align !== 'left',
};
}
}
@@ -0,0 +1,8 @@
<div
class="day-change-square-legacy"
pTooltip="{{ time?.dayChange | dayChange: time?.local }}"
tooltipPosition="top"
tooltipStyleClass="afl-tooltip"
>
{{ time?.dayChange?.title }}
</div>
@@ -0,0 +1,12 @@
@use './src/styles/framework' as *;
.day-change-square-legacy {
border: 1px solid $blue-icon;
color: $blue-light;
border-radius: $border-radius;
font-size: 10px;
font-weight: $font-bold;
width: $label-shift-width;
background-color: $white;
text-align: center;
}
@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
import { IFlightTime } from '@app/shared/models-legacy';
@Component({
selector: 'day-change-square-legacy',
templateUrl: './day-change-square-legacy.component.html',
styleUrls: ['./day-change-square-legacy.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class DayChangeSquareLegacy {
@Input() time: IFlightTime;
}
@@ -0,0 +1,8 @@
<div
class="day-change-square"
pTooltip="{{ times.dayChange | dayChange: times.local }}"
tooltipPosition="top"
tooltipStyleClass="afl-tooltip"
>
{{ times.dayChange.title }}
</div>
@@ -0,0 +1,16 @@
@use 'src/styles/colors' as *;
@use 'src/styles/fonts' as *;
.day-change-square {
box-sizing: border-box;
padding: 0 3px;
border: 1px solid $blue-icon;
border-radius: 2px;
font-size: $font-size-xs;
line-height: 11px;
text-align: center;
color: $blue-light;
font-weight: $font-bold;
}
@@ -0,0 +1,11 @@
import { Component, Input } from '@angular/core';
import { ITimesSet } from '@typings/times';
@Component({
selector: 'day-change-square',
templateUrl: './day-change-square.component.html',
styleUrls: ['./day-change-square.component.scss'],
})
export class DayChangeSquareComponent {
@Input() times: ITimesSet;
}
@@ -0,0 +1,12 @@
<div class="detail-header-badge-flight-number">
<div>
<div data-testid="flight-details-number">
{{ flight | flightNumber }}
</div>
<div class="description" *ngIf="showCodeSharing">
{{ legs | codesharing }}
</div>
</div>
<operator-logo [caption]="!icons.round" [round]="icons.round" [large]="icons.large" [flight]="flight"></operator-logo>
</div>
<flight-status-button [small]="true" [flight]="flight" *ngIf="canShowStatus && shouldShowStatus"></flight-status-button>
@@ -0,0 +1,35 @@
@use './src/styles/layouts' as *;
@use './src/styles/colors' as *;
@use './src/styles/variables' as *;
@use './src/styles/fonts' as *;
@use './src/styles/screen' as *;
:host {
@include v-spacing($space-m);
display: flex;
flex-direction: column;
justify-content: space-between;
.detail-header-badge-flight-number {
display: flex;
align-items: flex-start;
font-weight: $font-medium;
font-size: $font-size-xl;
@include h-spacing($space-m);
& > div {
@include v-spacing($space-s);
.description {
@include mobile() {
white-space: normal !important;
}
}
}
@include mobile() {
flex: 1;
justify-content: space-between;
}
}
}
@@ -0,0 +1,37 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { getMockPipe } from '@shared/pipes/pipe.mock';
import { FlightStatusLogic } from '../flight-status-button/flight-status.logic';
import { DetailsHeaderBadgeComponent } from './details-header-badge.component';
describe('DetailsHeaderBadgeComponent', () => {
let component: DetailsHeaderBadgeComponent;
let fixture: ComponentFixture<DetailsHeaderBadgeComponent>;
let logic: FlightStatusLogic;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
DetailsHeaderBadgeComponent,
getMockPipe('flightNumber'),
getMockPipe('codesharing'),
],
providers: [
{
provide: FlightStatusLogic,
useFactory: () => logic,
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DetailsHeaderBadgeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,40 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { RouteTypeLegacy } from '@app/shared/enumerators';
import { FlightModel, getLegs, Leg } from '@app/shared/models-legacy';
import { FlightStatusLogic } from '../flight-status-button/flight-status.logic';
@Component({
selector: 'details-header-badge',
templateUrl: './details-header-badge.component.html',
styleUrls: ['./details-header-badge.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DetailsHeaderBadgeComponent implements OnChanges {
@Input() flight: FlightModel;
@Input() canShowStatus = true;
@Input() icons = { round: true, large: false };
shouldShowStatus: boolean;
RouteType = RouteTypeLegacy;
legs: Leg[];
showCodeSharing: boolean;
constructor(private statusLogic: FlightStatusLogic) {}
async ngOnChanges(changes: SimpleChanges) {
if (this.flight && 'flight' in changes) {
this.legs = getLegs(this.flight);
this.shouldShowStatus = this.statusLogic.available(this.flight);
this.showCodeSharing = this.hasCodeSharing();
}
}
private hasCodeSharing() {
return this.legs.some((leg) => leg.hasCodesharing);
}
}
@@ -0,0 +1 @@
<details-header-badge [canShowStatus]="canShowStatus" [icons]="icons" *ngFor="let flight of flights" [flight]="flight"></details-header-badge>
@@ -0,0 +1,27 @@
@use './src/styles/layouts' as *;
@use './src/styles/colors' as *;
@use './src/styles/variables' as *;
@use './src/styles/fonts' as *;
@use './src/styles/screen' as *;
:host {
display: flex;
@include gt-mobile() {
& > *:not(:last-child) {
padding-right: $space-xl;
margin-right: $space-xl;
border-right: 1px solid $border;
}
}
@include mobile() {
flex: 1;
flex-wrap: wrap;
@include v-spacing($space-m);
details-header-badge {
flex: 100%;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DetailsHeaderBadgesComponent } from './details-header-badges.component';
describe('DetailsHeaderBadgesComponent', () => {
let component: DetailsHeaderBadgesComponent;
let fixture: ComponentFixture<DetailsHeaderBadgesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DetailsHeaderBadgesComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DetailsHeaderBadgesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FlightModel } from '@app/shared/models-legacy';
@Component({
selector: 'details-header-badges',
templateUrl: './details-header-badges.component.html',
styleUrls: ['./details-header-badges.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DetailsHeaderBadgesComponent {
@Input() flights: FlightModel[];
@Input() canShowStatus = true;
@Input() icons = { round: true, large: false };
}
@@ -0,0 +1,2 @@
<span *ngIf="!this.clarifying" class="duration">{{ duration | duration }}</span>
<span *ngIf="this.clarifying" class="duration duration--clarifying">{{ 'FLIGHT-STATUSES.Unknown' | translate }}</span>
@@ -0,0 +1,9 @@
@use './src/styles/framework' as *;
.duration {
@include font-overflow;
&--clarifying {
color: $orange;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { getMockPipe } from '@shared/pipes/pipe.mock';
import { DurationComponent } from './duration.component';
describe('DurationComponent', () => {
let component: DurationComponent;
let fixture: ComponentFixture<DurationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DurationComponent, getMockPipe('duration')],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DurationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { Duration, getTotalMinutes } from '@app/shared/models-legacy';
@Component({
selector: 'duration',
templateUrl: './duration.component.html',
styleUrls: ['./duration.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DurationComponent implements OnChanges {
@Input() duration: Duration;
clarifying = false;
ngOnChanges() {
const minutes = getTotalMinutes(this.duration);
this.clarifying = minutes < 15;
}
}
@@ -0,0 +1,39 @@
<print-button
class="flight-actions__action"
*ngIf="print"
[flight]="flight"
></print-button>
<share-button
class="flight-actions__action"
*ngIf="share"
[flight]="flight"
[viewType]="viewType"
></share-button>
<div class="k-space" *ngIf="wide"></div>
<buy-ticket-button
class="flight-actions__action"
*ngIf="buy && canBuy"
[direct]="flight"
></buy-ticket-button>
<registration-button
class="flight-actions__action"
*ngIf="register && canRegister"
[flight]="flight"
></registration-button>
<flight-status-button
class="flight-actions__action"
*ngIf="status && canViewStatus"
[flight]="flight"
></flight-status-button>
<flight-details-button
class="flight-actions__action"
*ngIf="details"
[flight]="$any(flight)"
(toDetails)="handleToDetailsEvent($event)"
></flight-details-button>
@@ -0,0 +1,58 @@
@use './src/styles/layouts' as *;
@use './src/styles/variables' as *;
@use './src/styles/screen' as *;
@mixin main-buttons() {
buy-ticket-button,
registration-button,
flight-status-button,
flight-details-button {
@content();
}
}
@mixin small-buttons() {
print-button,
share-button {
@content();
}
}
:host {
display: flex;
justify-content: flex-end;
& > .k-space {
flex: 1;
@include mobile() {
display: none;
}
}
@include gt-mobile() {
@include h-spacing($space-m);
@include main-buttons() {
width: 150px;
}
}
@include mobile() {
flex-wrap: wrap;
justify-content: start;
@include v-spacing($space-l, true);
@include main-buttons() {
order: 1;
flex: 100%;
}
@include small-buttons() {
order: 2;
}
share-button {
margin-bottom: 0;
}
}
}
@@ -0,0 +1,40 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FlightModel } from '../../../shared/models-legacy';
import { BuyTicketLogic } from '../buy-ticket-button/buy-ticket.logic';
import { FlightStatusLogic } from '../flight-status-button/flight-status.logic';
import { RegistrationLogic } from '../registration-button/registration.logic';
import { FlightActionsComponent } from './flight-actions.component';
describe('FlightActionsComponent', () => {
let component: FlightActionsComponent;
let fixture: ComponentFixture<FlightActionsComponent>;
let registration: RegistrationLogic;
let buying: BuyTicketLogic;
let statuses: FlightStatusLogic;
const flight: FlightModel = {} as any;
beforeEach(async () => {
registration = { isAnyAvailable: jasmine.createSpy().withArgs(flight).and.returnValue(Promise.resolve(true))} as any;
buying = {available: jasmine.createSpy().withArgs(flight).and.returnValue(true) } as any;
statuses = {available: jasmine.createSpy().withArgs(flight).and.returnValue(Promise.resolve(true)) } as any;
await TestBed.configureTestingModule({
declarations: [FlightActionsComponent],
providers: [
{ provide: RegistrationLogic, useFactory: () => registration },
{ provide: BuyTicketLogic, useFactory: () => buying },
{ provide: FlightStatusLogic, useFactory: () => statuses },
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FlightActionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,60 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
import { ViewType } from '@app/shared/enumerators/flight-request-type.enum';
import { FlightModel } from '@app/shared/models-legacy';
import { IFlight } from '@typings/flight/flight';
import { BuyTicketLogic } from '../buy-ticket-button/buy-ticket.logic';
import { RegistrationLogic } from '../registration-button/registration.logic';
import { FlightStatusLogic } from '../flight-status-button/flight-status.logic';
@Component({
selector: 'flight-actions',
templateUrl: './flight-actions.component.html',
styleUrls: ['./flight-actions.component.scss'],
host: {
class: 'p-print-none',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FlightActionsComponent implements OnChanges {
@Input() print = false; // hidden in scope of 7541
@Input() share = true;
@Input() details = true;
@Input() register = true;
@Input() status = true;
@Input() buy = true;
@Input() viewType: ViewType;
@Input() flight: FlightModel;
@Input() wide = false;
@Output() toDetails = new EventEmitter<IFlight>();
canBuy: boolean;
canRegister: boolean;
canViewStatus: boolean;
constructor(
private logic: BuyTicketLogic,
private registration: RegistrationLogic,
private statuses: FlightStatusLogic,
) {}
ngOnChanges(changes: SimpleChanges) {
if (this.flight && 'flight' in changes) {
this.canBuy = this.logic.isAnyAvailable(this.flight);
this.canRegister = this.registration.isAnyAvailable(this.flight);
this.canViewStatus = this.statuses.available(this.flight);
}
}
handleToDetailsEvent(flight: IFlight) {
this.toDetails.emit(flight);
}
}
@@ -0,0 +1,21 @@
<ng-container *ngFor="let leg of legs; let index = index; let last = last">
<div class="leg-point-brief">
<time-group-legacy
[times]="getLatest(leg, 'departure')"
[baseline]="getBaseline(leg, 'departure')"
></time-group-legacy>
<station [station]="$any(leg.departure)"></station>
</div>
<div class="leg-point-brief">
<time-group-legacy
[times]="getLatest(leg, 'arrival')"
[baseline]="getBaseline(leg, 'arrival')"
></time-group-legacy>
<station [station]="$any(leg.arrival)"></station>
</div>
<transfer [leg]="leg" [viewType]="viewType" *ngIf="!last && leg.next">
</transfer>
</ng-container>
@@ -0,0 +1,24 @@
@use './src/styles/variables' as *;
@use './src/styles/colors' as *;
@use './src/styles/layouts' as *;
@use './src/styles/fonts' as *;
:host {
display: block;
.leg-point-brief {
padding: $space-xl 0;
margin: 0 $space-xl;
display: flex;
justify-content: space-between;
time-group-legacy {
font-size: $font-size-xl2;
width: 90px;
}
}
}
.leg-point-brief + .leg-point-brief {
border-top: 1px dashed $border;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FlightBriefComponent } from './flight-brief.component';
describe('FlightBriefComponent', () => {
let component: FlightBriefComponent;
let fixture: ComponentFixture<FlightBriefComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FlightBriefComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FlightBriefComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,38 @@
import { Component, Input } from '@angular/core';
import { ViewType } from '@app/shared/enumerators/flight-request-type.enum';
import { ArrivalTimesModel, DepartureTimesModel, Leg } from '@app/shared/models-legacy';
import { StationProperty } from '../timeline/timeline.component';
const defaults = {} as ArrivalTimesModel | DepartureTimesModel;
@Component({
selector: 'flight-brief',
templateUrl: './flight-brief.component.html',
styleUrls: ['./flight-brief.component.scss'],
})
export class FlightBriefComponent {
@Input() legs: Leg[];
@Input() canChange = false;
viewType: ViewType;
/// returns latest or scheduled in case the time can change
/// returns scheduled in case the time can not change
getLatest(leg: Leg, property: StationProperty) {
const { latest, scheduled } = this.getTimes(leg, property);
return this.canChange ? latest || scheduled : scheduled;
}
/// returns scheduled in case the time can change
/// returns undefined in case the time can not change
getBaseline(leg: Leg, property: StationProperty) {
if (!this.canChange) return;
const { scheduled } = this.getTimes(leg, property);
return scheduled;
}
getType() {
return this.canChange ? 'actual' : 'scheduled';
}
private getTimes(leg: Leg, property: StationProperty) {
if (!leg) return defaults;
return leg[property]?.times || defaults;
}
}
@@ -0,0 +1,8 @@
<button
data-testid="flight-details-button"
class="color blue"
pButton
type="button"
label="{{ 'SHARED.FLIGHT-DETAILS' | translate }}"
(click)="openDetails(); $event.stopPropagation()"
></button>
@@ -0,0 +1,7 @@
:host {
display: flex;
button {
flex: 1;
}
}
@@ -0,0 +1,33 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterHandlerService } from '@shared/services/router-handler.service';
import { getMockPipe } from '@shared/pipes/pipe.mock';
import { FlightDetailsButtonComponent } from './flight-details-button.component';
describe('FlightDetailsButtonComponent', () => {
let component: FlightDetailsButtonComponent;
let fixture: ComponentFixture<FlightDetailsButtonComponent>;
let router: RouterHandlerService;
beforeEach(async () => {
router = {} as any;
await TestBed.configureTestingModule({
declarations: [
FlightDetailsButtonComponent,
getMockPipe('translate'),
],
providers: [
{ provide: RouterHandlerService, useFactory: () => router },
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FlightDetailsButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,17 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { IFlight } from '@typings/flight/flight';
@Component({
selector: 'flight-details-button',
templateUrl: './flight-details-button.component.html',
styleUrls: ['./flight-details-button.component.scss'],
})
export class FlightDetailsButtonComponent {
@Input() flight: IFlight;
@Output() toDetails = new EventEmitter<IFlight>();
openDetails() {
this.toDetails.emit(this.flight);
}
}
@@ -0,0 +1,6 @@
<a class="flight-details-icon" target="_blank" [href]="href">
<img class="flight-details-icon__image" [src]="src" />
<div class="flight-details-icon__description">
<ng-content></ng-content>
</div>
</a>
@@ -0,0 +1,58 @@
@use "src/styles/framework" as *;
.flight-details-icon {
display: flex;
flex-direction: column;
align-items: center;
padding: $space-xl;
width: 120px;
cursor: pointer;
border: none;
text-decoration: none;
border-radius: $border-radius;
transition-duration: 0.2s;
&:hover {
background-color: $blue-extra-light;
}
@include mobile {
flex-direction: row;
padding: $space-m;
width: 110px;
}
&__image {
width: 32px;
height: 32px;
margin-bottom: $space-s2;
@include smTablet {
margin-bottom: 4px;
}
@include mobile {
margin-bottom: 0;
margin-right: $space-m;
}
}
&__description {
font-size: $font-size-s;
line-height: 16px;
font-weight: $font-medium;
color: $blue-light;
white-space: normal !important;
text-align: center;
@include mobile {
font-size: $font-size-xs;
line-height: 13px;
text-align: left;
}
}
}
@@ -0,0 +1,13 @@
import { Component, Input } from "@angular/core";
@Component({
selector: 'flight-details-icon',
templateUrl: './flight-details-icon.component.html',
styleUrls: ['./flight-details-icon.component.scss']
})
export class FlightDetailsIconComponent {
@Input() href;
@Input() src;
constructor() { }
}
@@ -0,0 +1,8 @@
<div class="flight-details-section">
<div class="flight-details-section__caption">
<ng-content select="[caption]"></ng-content>
</div>
<div class="flight-details-section__content">
<ng-content></ng-content>
</div>
</div>
@@ -0,0 +1,35 @@
@use './src/styles/framework' as *;
.flight-details-section {
display: flex;
align-items: flex-start;
@include mobile {
flex-direction: column;
}
&__caption {
display: flex;
flex-shrink: 0;
margin-right: $space-xl;
width: 210px;
font-size: $font-size-s;
line-height: 16px;
color: $gray;
@include mobile {
margin-right: 0;
margin-bottom: $space-m;
}
}
&__content {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
@include h-deep-spacing($space-xl);
}
}
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'flight-details-section',
templateUrl: './flight-details-section.component.html',
styleUrls: ['./flight-details-section.component.scss'],
})
export class FlightDetailsSectionComponent {
constructor() {}
}
@@ -0,0 +1,16 @@
<div [ngClass]="flightEventsClasses">
<div *ngIf="changeRoute" class="flight-events__event" role="changeRoute">
<change-icon></change-icon>
<div class="flight-events__event-description" *ngIf="showDescription">
{{ 'SHARED.ROUTE-CHANGE' | translate }}
</div>
</div>
<!--ВОЗВРАТ В АЭРОПОРТ ВЫЛЕТА-->
<div *ngIf="reroute" class="flight-events__event" role="reroute">
<return-icon></return-icon>
<div class="flight-events__event-description" *ngIf="showDescription">
{{ 'SHARED.RETURN' | translate }}
</div>
</div>
</div>
@@ -0,0 +1,49 @@
@use './src/styles/framework' as *;
@mixin column-layout() {
flex-direction: column;
align-items: flex-start;
@include h-spacing(0);
@include v-spacing($space-s2);
}
.flight-events {
display: flex;
@include h-spacing($space-m);
&__event {
display: flex;
align-items: center;
height: 28px;
padding: 0 $space-m;
border-radius: $border-radius;
border: 1px solid $border;
@include h-spacing($space-m);
}
&__event-description {
font-size: $font-size-s;
line-height: 16px;
color: $gray;
}
&--column {
@include column-layout;
}
&--column-mobile {
@include mobile {
@include column-layout;
}
}
&--row-mobile {
@include gt-mobile {
@include column-layout;
}
}
}
@@ -0,0 +1,32 @@
import {
ChangeDetectionStrategy,
Component,
Input,
ViewEncapsulation,
} from '@angular/core';
export type IFlightEventsDirection =
| 'row'
| 'row-mobile'
| 'column'
| 'column-mobile';
@Component({
selector: 'flight-events',
templateUrl: './flight-events.component.html',
styleUrls: ['./flight-events.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FlightEventsComponent {
@Input() reroute = false;
@Input() changeRoute = false;
@Input() showDescription = false;
@Input() direction: IFlightEventsDirection = 'row';
get flightEventsClasses() {
return {
'flight-events': true,
[`flight-events--${this.direction}`]: this.direction !== 'row',
};
}
}
@@ -0,0 +1,7 @@
<div class="flight-props-caption">
<ng-content select="[caption]"></ng-content>
</div>
<div class="flight-props-body">
<ng-content select="[body]"></ng-content>
</div>
<arrow-down-icon *ngIf="expandable"></arrow-down-icon>
@@ -0,0 +1,80 @@
@use './src/styles/variables' as *;
@use './src/styles/screen' as *;
@use './src/styles/colors' as *;
@use './src/styles/fonts' as *;
@use './src/styles/layouts' as *;
.flight-props {
display: flex;
color: $text-color;
font-weight: $font-medium;
position: relative;
&-caption {
display: flex;
flex-shrink: 0;
@include h-spacing($space-xl);
@include mobile() {
flex: 100%;
align-items: center;
height: 24px;
}
}
&-body {
flex: 1;
display: grid;
column-gap: $space-l;
row-gap: $space-l;
@include mobile() {
grid-template-columns: 3fr 2fr;
margin-top: $space-xl;
&:empty {
margin-top: 0;
}
}
@include tablets() {
grid-template-columns: repeat(4, var(--property-column-width, 1fr));
}
@include desktop() {
grid-template-columns: repeat(5, var(--property-column-width, 1fr));
}
}
@include gt-mobile() {
@include h-spacing($space-m);
}
@include mobile() {
flex-wrap: wrap;
}
time-note {
grid-column-start: 1;
grid-column-end: 3; // span for 2 columns
}
&.equal-columns {
.flight-props-body {
@include mobile() {
grid-template-columns: 1fr 1fr;
}
}
}
arrow-down-icon {
@include mobile {
position: absolute;
right: 20px;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FlightPropsComponent } from './flight-props.component';
describe('FlightPropsComponent', () => {
let component: FlightPropsComponent;
let fixture: ComponentFixture<FlightPropsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FlightPropsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FlightPropsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,15 @@
import { Component, Input, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'flight-props',
templateUrl: './flight-props.component.html',
styleUrls: ['./flight-props.component.scss'],
encapsulation: ViewEncapsulation.None,
host: {
class: 'flight-props',
},
})
export class FlightPropsComponent {
@Input() columnWidth = '1fr';
@Input() expandable = false;
}
@@ -0,0 +1,11 @@
<button
class="flight-status-button color"
[ngClass]="{ 'blue-light': !small, 'blue-glow': small, small: small }"
pButton
type="button"
label="{{ 'SHARED.DETAILS' | translate }}"
pTooltip="{{ 'SHARED.DETAILS-TOOLTIP' | translate }}"
tooltipPosition="top"
tooltipStyleClass="afl-tooltip"
(click)="open(); $event.stopPropagation()"
></button>
@@ -0,0 +1,15 @@
@use './src/styles/colors' as *;
:host {
display: flex;
button {
&:not(.small) {
flex: 1;
}
}
.flight-status-button {
padding: 0 7px;
}
}
@@ -0,0 +1,102 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { getMockPipe } from '@shared/pipes/pipe.mock';
import { FlightStatusButtonComponent } from './flight-status-button.component';
import { RouteTypeLegacy } from '@shared/enumerators';
import { AirlineCodes } from '@shared/airlines';
import { FlightStatusLogic } from './flight-status.logic';
import { RouterHandlerService } from '@shared/services/router-handler.service';
import { FlightModel } from '@shared/models-legacy';
describe('FlightStatusButtonComponent', () => {
let component: FlightStatusButtonComponent;
let fixture: ComponentFixture<FlightStatusButtonComponent>;
let navigate: RouterHandlerService;
let logic: FlightStatusLogic;
const aeroflotFlight: FlightModel = {
routeType: RouteTypeLegacy.Direct,
operatingBy: { scheduled: AirlineCodes.Aeroflot },
} as any;
const pobedayFlight: FlightModel = {
routeType: RouteTypeLegacy.Direct,
operatingBy: { scheduled: AirlineCodes.Pobeda },
} as any;
const rossiyaFlight: FlightModel = {
routeType: RouteTypeLegacy.Direct,
operatingBy: { scheduled: AirlineCodes.Rossiya },
} as any;
const auroraFlight: FlightModel = {
routeType: RouteTypeLegacy.Direct,
operatingBy: { scheduled: AirlineCodes.Aurora },
} as any;
const airfranceFlight: FlightModel = {
routeType: RouteTypeLegacy.Direct,
operatingBy: { scheduled: AirlineCodes.AirFrance },
} as any;
let window$: any;
beforeEach(async () => {
logic = {} as any;
navigate = {} as any;
await TestBed.configureTestingModule({
declarations: [
FlightStatusButtonComponent,
getMockPipe('translate'),
],
providers: [
{ provide: RouterHandlerService, useFactory: () => navigate },
{ provide: FlightStatusLogic, useFactory: () => logic },
],
}).compileComponents();
fixture = TestBed.createComponent(FlightStatusButtonComponent);
component = fixture.componentInstance;
component.window$ = window$ = {} as any;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should open aeroflot, but not open airfrance', () => {
logic.available = jasmine
.createSpy('available')
.withArgs(airfranceFlight)
.and.returnValue(false)
.withArgs(aeroflotFlight)
.and.returnValue(true);
navigate.toBoardDetailsWithBlank = jasmine
.createSpy('toBoardDetailsWithBlank')
.withArgs(aeroflotFlight)
.and.callThrough();
component.flight = {
routeType: RouteTypeLegacy.Connecting,
flights: [airfranceFlight, aeroflotFlight],
} as any;
component.open();
expect(navigate.toBoardDetailsWithBlank).toHaveBeenCalled();
});
it('should open pobeda and aurora on its own sites and open rossia on native details', () => {
logic.available = jasmine.createSpy('available').and.returnValue(true);
navigate.toBoardDetailsWithBlank = jasmine
.createSpy('toBoardDetailsWithBlank')
.withArgs(rossiyaFlight)
.and.callThrough();
window$.open = jasmine
.createSpy('open')
.withArgs('https://www.pobeda.aero', '_blank')
.and.callThrough()
.withArgs('https://www.flyaurora.ru', '_blank')
.and.callThrough();
component.flight = {
routeType: RouteTypeLegacy.Connecting,
flights: [pobedayFlight, auroraFlight, rossiyaFlight],
} as any;
component.open();
expect(window$.open).toHaveBeenCalledTimes(2);
expect(navigate.toBoardDetailsWithBlank).toHaveBeenCalled();
});
});
@@ -0,0 +1,32 @@
import { Component, Input } from '@angular/core';
import { getAirline } from '@app/shared/airlines';
import { FlightModel, getFlights } from '@app/shared/models-legacy';
import { RouterHandlerService } from '@app/shared/services/router-handler.service';
import { FlightStatusLogic } from './flight-status.logic';
@Component({
selector: 'flight-status-button',
templateUrl: './flight-status-button.component.html',
styleUrls: ['./flight-status-button.component.scss'],
})
export class FlightStatusButtonComponent {
@Input() flight: FlightModel;
@Input() small = false;
window$ = window;
constructor(private navigate: RouterHandlerService, private logic: FlightStatusLogic) {}
open() {
getFlights(this.flight).forEach((flight) => this.openOne(flight));
}
private openOne(flight: FlightModel) {
if (!this.logic.available(flight)) return;
const { canViewStatus, canViewNativeStatus, url } = getAirline(flight) || {};
if (!canViewStatus) return;
if (canViewNativeStatus) return this.navigate.toBoardDetailsWithBlank(flight);
return this.window$.open(url, '_blank');
}
}
@@ -0,0 +1,104 @@
import { FlightStatusLogic } from './flight-status.logic';
import { RouteTypeLegacy } from '../../../shared/enumerators';
import * as moment from 'moment';
import { AppSettings } from '../../../shared/models-legacy';
describe('FlightStatusLogic', () => {
let logic: FlightStatusLogic;
const settings: AppSettings = {
flightStatusAvailableFrom: 24,
} as any;
beforeEach(() => {
logic = new FlightStatusLogic(settings);
});
it('should be not available for empty', () => {
expect(logic.available(null as any)).toBeFalse();
});
it('should be not available for far future', () => {
expect(
logic.available({
operatingBy: {
scheduled: 'SU',
},
routeType: RouteTypeLegacy.Direct,
dateToSearchBy: moment().add(5, 'day').toDate(),
leg: {
departure: {
times: {
scheduled: {
utc: moment().add(5, 'day').toDate(),
},
},
},
},
} as any),
).toBeFalse();
});
it('should be available for less than 1 day', () => {
expect(
logic.available({
operatingBy: {
scheduled: 'SU',
},
routeType: RouteTypeLegacy.Direct,
dateToSearchBy: moment().add(1, 'minute').toDate(),
leg: {
departure: {
times: {
scheduled: {
utc: moment().add(1, 'minute').toDate(),
},
},
},
},
} as any),
).toBeTrue();
});
it('should not be available for not SU', () => {
expect(
logic.available({
operatingBy: {
scheduled: 'AF',
},
routeType: RouteTypeLegacy.Direct,
dateToSearchBy: moment().add(1, 'minute').toDate(),
leg: {
departure: {
times: {
scheduled: {
utc: moment().add(1, 'minute').toDate(),
},
},
},
},
} as any),
).toBeFalse();
});
['SU', 'HZ', 'FV', 'DP'].forEach((airline) => {
it(`should be available for not ${airline}`, () => {
expect(
logic.available({
operatingBy: {
scheduled: airline,
},
routeType: RouteTypeLegacy.Direct,
dateToSearchBy: moment().add(1, 'minute').toDate(),
leg: {
departure: {
times: {
scheduled: {
utc: moment().add(1, 'minute').toDate(),
},
},
},
},
} as any),
).toBeTrue();
});
});
});
@@ -0,0 +1,54 @@
import { Inject, Injectable } from '@angular/core';
import { AppSettings, FlightModel, getFirstFlight, getFirstLeg } from '@app/shared/models-legacy';
import { APP_SETTINGS } from '@app/shared/services';
import { airlines } from '@shared/airlines';
import * as moment from 'moment';
const airlinesWithStatus = new Set(airlines.filter((a) => a.canViewStatus).map((a) => a.code));
@Injectable({ providedIn: 'root' })
export class FlightStatusLogic {
constructor(@Inject(APP_SETTINGS) public settings: AppSettings) {}
available(flight: FlightModel) {
if (!flight) return false;
const leg = getFirstLeg(flight);
flight = getFirstFlight(flight);
// 1. only for SU
const airline = (flight.operatingBy || leg.operatingBy)?.scheduled;
if (!airlinesWithStatus.has(airline)) {
// console.log(`Кнопка "Стату рейса" недоступна для рейса ${flight.hash} потому, что компания не та`);
return false;
}
const departure = leg.departure.times.scheduled.local;
const flightDate = flight.dateToSearchBy;
// 2. only today
const now = moment(); // 25 Nov 2021 22:00 (17 hours in advance, should be fine)
if (!now.isSame(moment(flightDate), 'day')) {
// console.log(
// `Кнопка "Стату рейса" недоступна для рейса ${
// flight.hash
// } потому, что другой день (Отправление: ${departure} (лок. время аэропорта), дата рейса: ${flightDate} (тоже локальное аэропорта), сейчас: ${now.toDate()} (время пользователя))`,
// );
return false;
}
// 3. only 24h ahead
// 24hours
const from = this.settings.flightStatusAvailableFrom;
// 25 Nov 2021 15:00 // 26 Nov 2021 15:00
const availableFrom = moment(departure).add(-from, 'hour');
if (now > availableFrom) return true; // now already passed
// console.log(
// `Кнопка "Стату рейса" недоступна для рейса ${
// flight.hash
// } потому, что еще вермя не пришло. сейсас - ${now.toDate()}, будет доступно с ${availableFrom.toDate()} (за ${
// this.settings.flightStatusAvailableFrom
// }ч)`,
// );
return false;
}
}
@@ -0,0 +1,12 @@
import { FlightStatusLegacy } from '@app/shared/enumerators';
export const statusColors = {
[FlightStatusLegacy.scheduled]: 'blue',
[FlightStatusLegacy.delayed]: 'orange',
[FlightStatusLegacy.cancelled]: 'red',
[FlightStatusLegacy.arrived]: 'green',
[FlightStatusLegacy.inFlight]: 'green',
[FlightStatusLegacy.landed]: 'green',
[FlightStatusLegacy.sent]: 'green',
[FlightStatusLegacy.unknown]: 'green',
};
@@ -0,0 +1,7 @@
<svg class="svg--plane" ngClass="{{ color }}">
<path
data-name="Path 143"
d="M6.334 16.625h1.583l3.958-6.334h4.355a1.188 1.188 0 0 0 0-2.375h-4.355L7.917 1.583H6.334l1.983 6.333H3.963L2.771 6.333H1.583l.792 2.771-.792 2.771h1.188l1.187-1.583h4.359Z"
/>
<path data-name="Path 144" d="M19 0v19H0V0Z" fill="none" />
</svg>

After

Width:  |  Height:  |  Size: 348 B

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FlightStatusIconComponent } from './flight-status-icon.component';
describe('FlightStatusIconComponent', () => {
let component: FlightStatusIconComponent;
let fixture: ComponentFixture<FlightStatusIconComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FlightStatusIconComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FlightStatusIconComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,23 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FlightStatusLegacy } from '@app/shared/enumerators';
import { statusColors } from './flight-status-colors';
@Component({
selector: 'flight-status-icon',
templateUrl: './flight-status-icon.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FlightStatusIconComponent implements OnChanges {
@Input() status: FlightStatusLegacy;
@Input() isSchedule: boolean;
color: string;
ngOnChanges(changes: SimpleChanges): void {
if (!changes.status) {
return;
}
this.color = this.isSchedule ? statusColors[FlightStatusLegacy.scheduled] : statusColors[this.status];
}
}
@@ -0,0 +1,8 @@
<property class="flight-status" description="{{ 'BOARDING-STATUSES.STATUS' | translate }}">
<div class="flight-status__content">
<span [ngClass]="indicatorClasses"></span>
<text weight="medium">
{{ 'BOARDING-STATUSES.' + status | translate }}
</text>
</div>
</property>
@@ -0,0 +1,31 @@
@use "src/styles/variables" as vars;
@use "src/styles/colors";
@use "src/styles/layouts";
.flight-status {
&__content {
display: flex;
align-items: center;
@include layouts.h-spacing(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 {
background-color: colors.$red;
border-color: colors.$red;
}
&--InProgress {
background-color: colors.$green;
border-color: colors.$green;
}
}
}
@@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
import { FlightTransitionStatus } from '@typings/flight/flight-transition';
@Component({
selector: 'flight-status',
templateUrl: './flight-status.component.html',
styleUrls: ['./flight-status.component.scss'],
})
export class FlightStatusComponent {
@Input() status: FlightTransitionStatus;
get indicatorClasses() {
return {
'flight-status__indicator': true,
[`flight-status__indicator--${this.status}`]:
this.status !== FlightTransitionStatus.EXPECTED,
};
}
}
@@ -0,0 +1,28 @@
<div class="transition">
<text weight="medium" color="light-gray" size="12" class="transition__title">
{{ title | translate }}
</text>
<flight-status class="transition__status" [status]="transition.status"></flight-status>
<captioned-time-group class="transition__beginning-time" [times]="transition.start" caption="SHARED.TIME-START"></captioned-time-group>
<captioned-time-group class="transition__finish-time" [times]="transition.end" caption="SHARED.TIME-END"></captioned-time-group>
<property [wrappable]="true" [ngClass]="gatePropertyClasses" description="{{ 'SHARED.NUMBER-EXIT' | translate }}" *ngIf="gate">
<text weight="medium">{{ gate }}</text>
</property>
<property
[wrappable]="true"
[ngClass]="dispatchPropertyClasses"
description="{{ 'SHARED.LANDING-TRANSFER' | translate }}"
*ngIf="dispatch"
>
<text weight="medium">{{ 'DISPATCH.' + dispatch | translate }}</text>
</property>
<property [wrappable]="true" [ngClass]="bagBeltPropertyClasses" *ngIf="bagBelt" description="{{ 'SHARED.BAGBELT' | translate }}">
<text weight="medium">{{ bagBelt }}</text>
</property>
</div>
@@ -0,0 +1,63 @@
@use 'src/styles/screen';
@use 'src/styles/variables' as vars;
@use '/src/styles/grid-sizes' as gridSizes;
@mixin intermediateScreens() {
@media (min-width: vars.$media-breakpoint-desktop-min) and (max-width: 1180px) {
@content;
}
@media (min-width: vars.$media-breakpoint-small-tablet-min) and (max-width: 880px) {
@content;
}
}
@mixin templateColumns($title: gridSizes.$title-column-width) {
grid-template-columns:
[title] $title
[status] gridSizes.$status-width
[beginning-time] gridSizes.$time-width
[finish-time] minmax(gridSizes.$time-width, 1fr)
[first-property] minmax(min-content, gridSizes.$time-group-width)
[second-property] minmax(min-content, 160px);
}
.transition {
display: grid;
grid-gap: gridSizes.$gap-medium;
@include templateColumns;
@include screen.tablets {
grid-gap: gridSizes.$gap-small;
@include templateColumns(gridSizes.$title-column-width-tablets);
}
&__first-property {
grid-area: 1 / 5 / 2 / 6; // fifth column of the first row
@include intermediateScreens {
grid-area: 2 / 2 / 3 / 3; // second column of the second row
}
}
&__second-property {
grid-area: 1 / 6 / 2 / 7; // sixth column of the first row
@include intermediateScreens {
grid-area: 2 / 3 / 3 / 4; // third column of the second row
}
}
&__third-property {
grid-area: 2 / 2 / 3 / 4; // second and third columns of the second row
@include intermediateScreens {
grid-area: 2 / 4 / 3 / 6; // fourth and fifth columns of the second row
}
}
&__bag-belt.transition__first-property {
grid-area: 1 / 5 / 2 / 7; // fifth and sixth columns of the first row
}
}
@@ -0,0 +1,41 @@
@use "src/styles/screen";
@use "src/styles/grid-sizes" as gridSizes;
.transition {
@include screen.mobile {
grid-gap: gridSizes.$gap-small;
grid-template-columns: repeat(2, minmax(gridSizes.$time-width, 1fr));
&__title {
grid-area: 1 / 1 / 2 / 2; // first column of first row
}
&__status {
grid-area: 2 / 1 / 3 / 2; // first column of second row
}
&__beginning-time {
grid-area: 3 / 1 / 4 / 2; // first column of third row
}
&__finish-time {
grid-area: 3 / 2 / 4 / 3; // second column of third row
}
&__first-property {
grid-area: 4 / 1 / 5 / 2; // first column of fourth row
}
&__second-property {
grid-area: 4 / 2 / 5 / 3; // second column of fourth row
}
&__third-property {
grid-area: 5 / 1 / 6 / 3; // first and second columns of fifth row
}
&__bag-belt.transition__first-property {
grid-area: 4 / 1 / 5 / 3; // first and second columns of fourth row
}
}
}
@@ -0,0 +1,58 @@
import { Component, Input } from '@angular/core';
import { DispatchType } from '@typings/enums';
import { IFlightTransition } from '@typings/flight/flight-transition';
@Component({
selector: 'flight-transition',
templateUrl: './flight-transition.component.html',
styleUrls: [
'./flight-transition.component.layout.scss',
'./flight-transition.component.mobile-layout.scss',
],
})
export class FlightTransitionComponent {
@Input() transition: IFlightTransition;
@Input() title: string;
@Input() dispatch: DispatchType;
@Input() gate: string;
@Input() bagBelt: string;
get gatePropertyClasses() {
return {
transition__gate: true,
'transition__first-property': !!this.gate,
};
}
get dispatchPropertyClasses() {
// if there is no gate we should show dispatch in the first property area
const isFirstProperty = !this.gate;
// if there is a gate we should show dispatch in the second property area
const isSecondProperty = !!this.gate;
return {
transition__dispatch: true,
'transition__first-property': isFirstProperty,
'transition__second-property': isSecondProperty && !!this.dispatch,
};
}
get bagBeltPropertyClasses() {
// if there is no dispatch and gate we should show bag belt in the first property area
const isFirstProperty = !this.dispatch && !this.gate;
// There is dispatch and gate - we should show bag belt in the third property area
const isThirdProperty = this.dispatch && this.gate;
// if there is dispatch or gate we should show bag belt in the second property area
const isSecondProperty = !isFirstProperty && !isThirdProperty;
return {
'transition__bag-belt': true,
'transition__first-property': isFirstProperty,
'transition__second-property': isSecondProperty,
'transition__third-property': isThirdProperty && !!this.bagBelt,
};
}
}
@@ -0,0 +1,93 @@
import { CaptionedTimeGroupComponent } from '@app/modules/components/captioned-time-group/captioned-time-group.component';
import { FlightStatusComponent } from '@app/modules/components/flight-status/flight-status.component';
import { PropertyComponent } from '@app/modules/components/property/property.component';
import { TextComponent } from '@toolkit/text/text.component';
import { Meta, moduleMetadata, Story } from '@storybook/angular';
import { TooltipModule } from 'primeng/tooltip';
import {
FlightTransitionStatus,
IFlightTransition,
} from '../../../../typings/flight/flight-transition';
import { TimeGroupComponent } from '../time-group/time-group.component';
import { FlightTransitionComponent } from './flight-transition.component';
export default {
title: 'Components/FlightTransition',
component: FlightTransitionComponent,
decorators: [
moduleMetadata({
declarations: [
TextComponent,
TimeGroupComponent,
FlightStatusComponent,
CaptionedTimeGroupComponent,
PropertyComponent,
],
imports: [TooltipModule],
}),
],
argTypes: {
transition: {
control: 'object',
},
title: {
control: 'text',
},
dispatch: {
options: ['Bus', 'Bridge', 'None'],
mapping: {
Bus: 'Bus',
Bridge: 'Bridge',
None: undefined,
},
control: {
type: 'select',
labels: {
Bus: 'Bus',
Bridge: 'Bridge',
None: 'None',
},
},
},
gate: {
control: 'text',
},
bagBelt: {
control: 'text',
},
},
} as Meta;
const deboarding: IFlightTransition = {
status: FlightTransitionStatus.SPECIFIED,
start: {
utc: '2022-04-08T17:05:00Z',
local: '2022-04-08T20:08:00+03:03',
localTime: '20:08',
tzOffset: 183,
dayChange: {
value: 0,
title: '',
},
},
end: {
utc: '2022-04-08T17:35:00Z',
local: '2022-04-08T20:38:00+03:03',
localTime: '20:38',
tzOffset: 183,
dayChange: {
value: 0,
title: '',
},
},
isActual: true,
};
export const Default: Story<FlightTransitionComponent> = (args) => {
return {
props: args,
};
};
Default.args = {
transition: deboarding,
};
@@ -0,0 +1,14 @@
export * from './page-title/page-title.component';
export * from './page-tabs/page-tabs.component';
export * from '@toolkit/time-selector/time-selector.component';
export * from '@components/search-history/search-history.component';
export * from '@components/page/breadcrumds/page-breadcrumbs.component';
export * from './flight-events/flight-events.component';
export * from './share-panel/share-panel.component';
export * from './app-version/app-version.component';
export * from './page-loader/page-loader.component';
export * from './time-note/time-note.component';
export * from './app-show-debug/app-show-debug.component';
export * from './page-footer-notes/page-footer-notes.component';
export * from './day-change-square-legacy/day-change-square-legacy.component';
export * from './transfer-inline-extended/transfer-inline-extended.component';
@@ -0,0 +1,5 @@
<share-button [flight]="flight" [viewType]="viewType"></share-button>
<span class="description last-update__description">
<span>{{ 'SHARED.LAST-UPDATE' | translate }}:</span>
<span class="time">&nbsp;{{ flight?.lastUpdate | aflDate: 'with-time' }}</span>
</span>
@@ -0,0 +1,28 @@
@use './src/styles/screen';
:host {
display: flex;
align-items: center;
@include screen.gt-mobile() {
justify-content: flex-end;
}
@include screen.mobile() {
justify-content: space-between;
}
.last-update__description {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
}
share-button {
@include screen.gt-mobile() {
display: none;
}
}
}
@@ -0,0 +1,28 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { getMockPipe } from '@shared/pipes/pipe.mock';
import { LastUpdateComponent } from './last-update.component';
describe('LastUpdateComponent', () => {
let component: LastUpdateComponent;
let fixture: ComponentFixture<LastUpdateComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
LastUpdateComponent,
getMockPipe('translate'),
getMockPipe('aflDate'),
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LastUpdateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ViewType } from '@app/shared/enumerators/flight-request-type.enum';
import { FlightModel } from '@app/shared/models-legacy';
@Component({
selector: 'last-update',
templateUrl: './last-update.component.html',
styleUrls: ['./last-update.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LastUpdateComponent {
@Input() viewType: ViewType;
@Input() flight: FlightModel;
}
@@ -0,0 +1,15 @@
<a
*ngIf="viewType === ViewType.Onlineboard"
href="https://onlineboard.aeroflot.ru?utm_medium=site&utm_campaign=new_onlineboard_version&utm_source=new_onlineboard&utm_content=button"
target="_blank"
>
{{ 'SHARED.GO-TO-PREVIOUS-VERSION' | translate }}
</a>
<a
*ngIf="viewType === ViewType.Schedule"
href="https://www.aeroflot.ru/old_schedule/schedule?utm_medium=site&utm_campaign=new_onlineboard_version&utm_source=new_schedule&utm_content=button"
target="_blank"
>
{{ 'SHARED.GO-TO-PREVIOUS-VERSION' | translate }}
</a>
@@ -0,0 +1,15 @@
:host {
a {
display: inline-flex;
text-align: center;
border: none;
background-color: #e2740b;
padding: 3px 10px 4px 10px;
color: #fff;
border-radius: 3px;
font-size: 12px;
margin-bottom: 10px;
height: 25px;
line-height: 1.6;
}
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LinkToOldVersionComponent } from './link-to-old-version.component';
import { getMockPipe } from '@shared/pipes/pipe.mock';
describe('LinkToOldVersionComponent', () => {
let component: LinkToOldVersionComponent;
let fixture: ComponentFixture<LinkToOldVersionComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LinkToOldVersionComponent, getMockPipe('translate')],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LinkToOldVersionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,12 @@
import { Component, Input } from '@angular/core';
import { ViewType } from '@app/shared/enumerators/flight-request-type.enum';
@Component({
selector: 'link-to-old-version',
templateUrl: './link-to-old-version.component.html',
styleUrls: ['./link-to-old-version.component.scss'],
})
export class LinkToOldVersionComponent {
ViewType = ViewType;
@Input() viewType: ViewType;
}
@@ -0,0 +1,8 @@
<div class="note">
<text size="12" mobileSize="10" color="light-gray" class="note__symbol">
{{ 'SHARED.NOTE-SYMBOL' | translate }}
</text>
<text size="12" mobileSize="10" color="light-gray" class="note__text">
<ng-content></ng-content>
</text>
</div>
@@ -0,0 +1,19 @@
@use "src/styles/variables" as vars;
@use "src/styles/screen";
.note {
display: flex;
flex-wrap: nowrap;
::ng-deep .text {
line-height: 17px !important;
}
&__symbol {
margin-right: vars.$space-s;
}
&__text {
white-space: pre-line;
}
}
@@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'note',
templateUrl: './note.component.html',
styleUrls: ['./note.component.scss'],
})
export class NoteComponent {}
@@ -0,0 +1,4 @@
<operator-logo [flight]="flight" [round]="round"></operator-logo>
<text size="12" color="light-gray" *ngIf="showModel" [ngClass]="modelClasses">
{{ leg?.equipment?.aircraft?.actual?.title || leg?.equipment?.aircraft?.scheduled?.title }}
</text>
@@ -0,0 +1,30 @@
@use './src/styles/variables' as *;
@use './src/styles/layouts' as *;
@use './src/styles/screen' as *;
operator-logo-and-model {
display: flex;
flex-direction: column;
@include gt-mobile() {
@include v-spacing($space-s);
}
.aircraft-name {
&--right {
align-self: flex-end;
}
&--mobile-right {
@include mobile {
align-self: flex-end;
}
}
&--mobile-left {
@include mobile {
align-self: flex-start;
}
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OperatorLogoAndModelComponent } from './operator-logo-and-model.component';
describe('OperatorLogoAndModelComponent', () => {
let component: OperatorLogoAndModelComponent;
let fixture: ComponentFixture<OperatorLogoAndModelComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ OperatorLogoAndModelComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(OperatorLogoAndModelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,31 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { FlightModel, getFirstLeg, Leg } from '@app/shared/models-legacy';
import { IAlign } from '@typings/common/align';
@Component({
selector: 'operator-logo-and-model',
templateUrl: './operator-logo-and-model.component.html',
styleUrls: ['./operator-logo-and-model.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class OperatorLogoAndModelComponent implements OnChanges {
@Input() flight: FlightModel;
@Input() showModel = true;
@Input() round = false;
@Input() modelAlign: IAlign = 'left';
leg: Leg;
ngOnChanges(changes: SimpleChanges) {
if (this.flight && 'flight' in changes) {
this.leg = getFirstLeg(this.flight);
}
}
get modelClasses() {
return {
'aircraft-name': true,
[`aircraft-name--${this.modelAlign}`]: this.modelAlign !== 'left',
};
}
}
@@ -0,0 +1,11 @@
<div class="description" *ngIf="caption">{{ 'SHARED.AVIACOMPANY' | translate }}</div>
<div
data-testid="flight-company-logo"
[pTooltip]="operatingBy"
tooltipPosition="top"
tooltipStyleClass="afl-tooltip"
class="company-logo"
[ngClass]="classes"
>
<!--empty-->
</div>
@@ -0,0 +1,19 @@
@use './src/styles/variables' as *;
@use './src/styles/layouts' as *;
@use './src/styles/screen' as *;
operator-logo {
display: block;
& > div.company-logo {
background-repeat: no-repeat;
background-size: contain;
background-position: left top;
}
@include v-spacing($space-s);
@include mobile() {
& > div.description {
display: none;
}
}
}
@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OperatorLogoComponent } from './operator-logo.component';
import { LocalizationService } from '@shared/services';
describe('OperatorLogoComponent', () => {
let component: OperatorLogoComponent;
let fixture: ComponentFixture<OperatorLogoComponent>;
let localization: LocalizationService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OperatorLogoComponent],
providers: [{ provide: LocalizationService, useFactory: () => localization }]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(OperatorLogoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,52 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { FlightModel } from '@app/shared/models-legacy';
import { Language } from '@shared/enumerators';
import { LocalizationService } from '@shared/services';
@Component({
selector: 'operator-logo',
templateUrl: './operator-logo.component.html',
styleUrls: ['./operator-logo.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class OperatorLogoComponent implements OnChanges {
@Input() flight: FlightModel;
@Input() round = false;
@Input() large = false;
@Input() caption = false;
operatingBy: string;
classes = {
round: false,
large: false,
ru: false,
};
constructor(private localization: LocalizationService) {}
ngOnChanges(changes: SimpleChanges) {
if (this.flight && 'flight' in changes) {
const { actual, scheduled } = this.flight.operatingBy || {};
this.operatingBy = (actual || scheduled)?.replace('/', '');
}
this.classes = this.computeClasses();
}
private computeClasses() {
return {
round: this.round,
large: this.large,
ru: this.localization.Language === Language.ru,
...this.getLogoClass(this.operatingBy),
};
}
private getLogoClass(operatingBy?: string) {
return operatingBy
? {
[`company-logo--${operatingBy}`]: true,
}
: {};
}
}
@@ -0,0 +1,8 @@
<section class="page-empty">
<div class="page-empty__title">
{{ 'SHARED.FLIGHTS-NOT-FOUND' | translate }}
</div>
<div class="page-empty__text">
{{ 'SHARED.FLIGHTS-NOT-FOUND-TEXT' | translate }}
</div>
</section>
@@ -0,0 +1,42 @@
@use 'src/styles/framework' as *;
.page-empty {
padding: 50px 40px 50px 124px;
background-color: $white;
background-image: url('~src/assets/img/icon-not-found.svg');
background-position: 40px center;
background-repeat: no-repeat;
background-size: 55px auto;
@include mobile {
padding: 124px 40px 50px 40px;
background-position: center 40px;
}
&__title {
margin-bottom: $space-s;
font-size: $font-size-xl;
font-weight: $font-bold;
color: $text-color;
@include mobile {
text-align: center;
}
}
&__text {
max-width: 80%;
font-size: $font-size-m;
line-height: 22px;
color: $gray;
@include mobile {
text-align: center;
max-width: initial;
}
}
}
@@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'page-empty-list',
templateUrl: './page-empty-list.component.html',
styleUrls: ['./page-empty-list.component.scss'],
})
export class PageEmptyListComponent {}
@@ -0,0 +1,8 @@
export interface AirportModel {
city_code: string;
name: string;
code: string;
has_afl_flights: boolean;
location: { lat: number; lon: number };
title: any;
}
@@ -0,0 +1,11 @@
export interface CityModel {
code: string;
name: string;
location: { lat: number; lon: number };
title: any;
airports: any[];
has_afl_flights: boolean;
country_code: string;
countryName: string;
isAirport: boolean;
}

Some files were not shown because too many files have changed in this diff Show More