Files
flights_web/docs/superpowers/plans/2026-04-03-phase1-foundation.md
T
gnezim 729603d27c fix: resolve build issues with ModernJS v3 + Module Federation
- Switch from @module-federation/modern-js to @module-federation/modern-js-v3 (v3 compatible)
- Rename App.tsx to AppProviders.tsx to avoid hasApp detection that blocks nested route discovery
- Move runtime.router config from modern.config.ts to modern.runtime.ts (v3 API)
- Fix PostCSS config type annotation
- Enable streaming SSR mode successfully
2026-04-03 23:34:20 +03:00

59 KiB

Phase 1: Foundation -- Project Setup & Shared Infrastructure

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Set up the ModernJS React application with Module Federation 2.0, shared types, API client, state management, i18n, theming, routing shell, and SSR -- producing a working app skeleton that feature plans build on.

Architecture: Single ModernJS SSR remote micro-frontend with Rspack bundler. Module Federation 2.0 exposes feature entry points. TanStack Query for server state, Zustand for client state, CSS Modules + postcss-prefix-selector for style isolation, react-i18next for i18n.

Tech Stack: ModernJS, Rspack, React 18+, TanStack Query v5, Zustand, PrimeReact, react-i18next, CSS Modules, postcss-prefix-selector

Related plans:

  • Phase 2: Online Board feature (depends on this plan)
  • Phase 3: Schedule feature
  • Phase 4: Flights Map feature
  • Phase 5: Cross-cutting (SignalR, SEO, Analytics, Performance)

Task 1: Initialize ModernJS Project

Files:

  • Create: react-app/package.json
  • Create: react-app/modern.config.ts
  • Create: react-app/tsconfig.json

The React app lives in react-app/ alongside the existing ClientApp/ Angular app.

  • Step 1: Scaffold ModernJS project
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
npx @modern-js/create@latest react-app --lang-type ts --package-manager npm

Select these options when prompted:

  • Framework: Web App

  • Package Manager: npm

  • Language: TypeScript

  • Build Tool: Rspack

  • Step 2: Verify the project scaffolded correctly

cd react-app && ls -la

Expected: package.json, modern.config.ts, tsconfig.json, src/ directory exist.

  • Step 3: Install core dependencies
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npm install @tanstack/react-query@^5 zustand@^5 react-i18next@^15 i18next@^24 primereact@^10 primeicons@^7 postcss-prefix-selector@^2 @module-federation/modern-js@latest
  • Step 4: Install dev dependencies
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npm install -D @types/react @types/react-dom prettier eslint
  • Step 5: Verify project builds
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npm run build

Expected: Build succeeds with no errors.

  • Step 6: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/
git commit -m "feat: scaffold ModernJS React app with Rspack"

Task 2: Configure Module Federation 2.0

Files:

  • Create: react-app/module-federation.config.ts

  • Modify: react-app/modern.config.ts

  • Step 1: Create Module Federation config

Create react-app/module-federation.config.ts:

import { createModuleFederationConfig } from '@module-federation/modern-js';

export default createModuleFederationConfig({
    name: 'afl_flights',
    filename: 'static/remoteEntry.js',
    exposes: {
        './App': './src/App.tsx',
        './OnlineBoard': './src/features/online-board/index.ts',
        './Schedule': './src/features/schedule/index.ts',
        './FlightsMap': './src/features/flights-map/index.ts',
        './PopularRequests': './src/features/popular-requests/index.ts',
    },
    shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
    },
});
  • Step 2: Configure ModernJS with MF, SSR, and CSS Modules

Replace react-app/modern.config.ts with:

import { appTools, defineConfig } from '@modern-js/app-tools';
import { moduleFederationPlugin } from '@module-federation/modern-js';

export default defineConfig({
    runtime: {
        router: true,
    },
    server: {
        ssr: {
            mode: 'stream',
        },
    },
    output: {
        cssModules: {
            localIdentName: '[local]--[hash:base64:5]',
            namedExport: true,
        },
    },
    tools: {
        postcss: {
            postcssOptions: {
                plugins: [
                    require('postcss-prefix-selector')({
                        prefix: '.afl-flights',
                        transform(
                            prefix: string,
                            selector: string,
                            prefixedSelector: string,
                        ) {
                            if (
                                selector.startsWith(':root') ||
                                selector.startsWith('@')
                            ) {
                                return selector;
                            }
                            return prefixedSelector;
                        },
                    }),
                ],
            },
        },
    },
    plugins: [appTools({ bundler: 'rspack' }), moduleFederationPlugin()],
});
  • Step 3: Verify build still works with MF config
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npm run build

Expected: Build succeeds. mf-manifest.json is generated in the output directory.

  • Step 4: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/module-federation.config.ts react-app/modern.config.ts
git commit -m "feat: configure Module Federation 2.0 with SSR streaming"

Task 3: Create Directory Structure

Files:

  • Create directories and placeholder index.ts files for the entire project structure

  • Step 1: Create all directories

cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app/src

mkdir -p routes/onlineboard
mkdir -p routes/schedule
mkdir -p routes/flights-map
mkdir -p routes/error

mkdir -p features/online-board/{components,hooks,services}
mkdir -p features/schedule/{components,hooks,services}
mkdir -p features/flights-map/{components,hooks,services}
mkdir -p features/popular-requests/{components,hooks,services}

mkdir -p shared/{api,hooks,stores,types,utils,seo,analytics}
mkdir -p shared/config
mkdir -p ui/{calendar-input,card,date-tabs,time-selector,toggle-switch,icons}
mkdir -p i18n/locales
mkdir -p styles/theme
  • Step 2: Create placeholder feature entry points

Create react-app/src/features/online-board/index.ts:

export { OnlineBoard } from './components/OnlineBoard';

Create react-app/src/features/schedule/index.ts:

export { Schedule } from './components/Schedule';

Create react-app/src/features/flights-map/index.ts:

export { FlightsMap } from './components/FlightsMap';

Create react-app/src/features/popular-requests/index.ts:

export { PopularRequests } from './components/PopularRequests';

Create placeholder components for each feature so MF exposes resolve:

Create react-app/src/features/online-board/components/OnlineBoard.tsx:

export function OnlineBoard() {
    return <div>Online Board -- placeholder</div>;
}

Create react-app/src/features/schedule/components/Schedule.tsx:

export function Schedule() {
    return <div>Schedule -- placeholder</div>;
}

Create react-app/src/features/flights-map/components/FlightsMap.tsx:

export function FlightsMap() {
    return <div>Flights Map -- placeholder</div>;
}

Create react-app/src/features/popular-requests/components/PopularRequests.tsx:

export function PopularRequests() {
    return <div>Popular Requests -- placeholder</div>;
}
  • Step 3: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/
git commit -m "feat: create project directory structure with placeholder features"

Task 4: Port TypeScript Types

Files:

  • Create: react-app/src/shared/types/enums.ts
  • Create: react-app/src/shared/types/times.ts
  • Create: react-app/src/shared/types/operating-by.ts
  • Create: react-app/src/shared/types/equipment.ts
  • Create: react-app/src/shared/types/airport-info.ts
  • Create: react-app/src/shared/types/language.ts
  • Create: react-app/src/shared/types/locations.ts
  • Create: react-app/src/shared/types/flight-id.ts
  • Create: react-app/src/shared/types/flight-station.ts
  • Create: react-app/src/shared/types/flight-transition.ts
  • Create: react-app/src/shared/types/flight-leg.ts
  • Create: react-app/src/shared/types/flight.ts
  • Create: react-app/src/shared/types/responses.ts
  • Create: react-app/src/shared/types/popular-request.ts
  • Create: react-app/src/shared/types/common.ts
  • Create: react-app/src/shared/types/index.ts

These are direct ports from ClientApp/src/typings/ with Angular-specific imports removed.

  • Step 1: Create enum types

Create react-app/src/shared/types/enums.ts:

export enum OperationStatus {
    SCHEDULED = 'Scheduled',
    IN_PROGRESS = 'InProgress',
    FINISHED = 'Finished',
}

export enum DispatchType {
    BUS = 'Bus',
    BRIDGE = 'Bridge',
}

export enum RouteType {
    DIRECT = 'Direct',
    MULTI_LEG = 'MultiLeg',
    CONNECTING = 'Connecting',
}

export enum FlightStatus {
    SCHEDULED = 'Scheduled',
    SENT = 'Sent',
    IN_FLIGHT = 'InFlight',
    LANDED = 'Landed',
    ARRIVED = 'Arrived',
    DELAYED = 'Delayed',
    CANCELLED = 'Cancelled',
    UNKNOWN = 'Unknown',
}

export enum ScheduleDirection {
    OUTBOUND = 'OUTBOUND',
    INBOUND = 'INBOUND',
}

export enum RequestMode {
    FLIGHT_NUMBER = 'FlightNumber',
    ROUTE = 'Route',
    SCHEDULE_ROUTE_BOTH_DIRECTIONS = 'RouteWithBack',
    DEPARTURE = 'Departure',
    ARRIVAL = 'Arrival',
}

export enum FlightTransitionStatus {
    FINISHED = 'Finished',
    EXPECTED = 'Expected',
    IN_PROGRESS = 'InProgress',
    SPECIFIED = 'Specified',
    SCHEDULED = 'Scheduled',
}
  • Step 2: Create base types

Create react-app/src/shared/types/times.ts:

export type IDayChange = {
    value: number;
    title: string;
};

export type ITimesSet = {
    dayChange: IDayChange;
    local: string;
    localTime: string;
    tzOffset: number;
    utc: string;
};

export type IFlightTimes = {
    unix: number;
    utc: string;
};

Create react-app/src/shared/types/operating-by.ts:

export type IOperator = {
    carrier: string;
    flightNumber: string;
    isMarketing: boolean;
};

export type IOperatingBy = {
    scheduled: string;
    actual: string;
    operators: IOperator[];
};

Create react-app/src/shared/types/equipment.ts:

export type IFlightLegEquipment = Record<string, unknown>;

Create react-app/src/shared/types/airport-info.ts:

export type IAirportInfo = {
    airport: string;
    airportCode: string;
    city: string;
    cityCode: string;
    countryCode: string;
    country?: string;
};

Create react-app/src/shared/types/language.ts:

export type ILanguagesMap = {
    de: string;
    en: string;
    es: string;
    fr: string;
    it: string;
    ja: string;
    ko: string;
    ru: string;
    zh: string;
};

export type SupportedLanguage = keyof ILanguagesMap;

export const SUPPORTED_LANGUAGES: SupportedLanguage[] = [
    'ru', 'en', 'de', 'fr', 'es', 'it', 'ja', 'ko', 'zh',
];

Create react-app/src/shared/types/locations.ts:

import type { ILanguagesMap } from './language';

export type ILocation = {
    lat: number;
    lon: number;
};

export type ICity = {
    code: string;
    country_code: string;
    location: ILocation;
    title: ILanguagesMap;
};

export type IAirport = {
    city_code: string;
    code: string;
    has_afl_flights: string;
    location: ILocation;
    title: ILanguagesMap;
};
  • Step 3: Create flight types

Create react-app/src/shared/types/flight-id.ts:

export type IFlightId = {
    carrier: string;
    date: string;
    flightNumber: string;
    suffix: string;
    dateLT?: string;
};

export type IParsedFlightId = {
    carrier: string;
    date: Date;
    dateLT?: string;
    flightNumber: string;
    suffix: string;
    departure?: string;
    arrival?: string;
};

Create react-app/src/shared/types/flight-transition.ts:

import type { FlightTransitionStatus } from './enums';
import type { ITimesSet } from './times';

export type IFlightTransitions = {
    registration: IFlightTransition;
    boarding: IFlightTransition;
    deboarding: IFlightTransition;
};

export type IFlightTransition = {
    end: ITimesSet;
    isActual: boolean;
    start: ITimesSet;
    status: FlightTransitionStatus;
};

Create react-app/src/shared/types/flight-station.ts:

import type { IAirportInfo } from './airport-info';
import type { DispatchType, OperationStatus } from './enums';
import type { ITimesSet } from './times';

export type IFlightDepartureStationTimes = {
    scheduledDeparture: ITimesSet;
    estimatedBlockOff?: ITimesSet;
    actualBlockOff?: ITimesSet;
};

export type IFlightArrivalStationTimes = {
    scheduledArrival: ITimesSet;
    estimatedBlockOn?: ITimesSet;
    actualBlockOn?: ITimesSet;
};

export type IFlightLegStation = {
    scheduled: IAirportInfo;
    latest?: IAirportInfo;
    dispatch?: DispatchType;
    gate?: string;
    terminal?: string;
};

export type IFlightLegDepartureStation = IFlightLegStation & {
    checkingStatus: OperationStatus;
    parkingStand?: string;
    times: IFlightDepartureStationTimes;
};

export type IFlightLegArrivalStation = IFlightLegStation & {
    times: IFlightArrivalStationTimes;
    bagBelt?: string;
};

Create react-app/src/shared/types/flight-leg.ts:

import type { FlightStatus } from './enums';
import type { IFlightLegEquipment } from './equipment';
import type {
    IFlightLegArrivalStation,
    IFlightLegDepartureStation,
} from './flight-station';
import type { IFlightTransitions } from './flight-transition';
import type { IOperatingBy } from './operating-by';

export type IFlightLegFlags = {
    checkinAvailable: boolean;
    returnToAirport: boolean;
    routeChanged: boolean;
};

export type IFlightLeg = {
    arrival: IFlightLegArrivalStation;
    dayChange: number;
    departure: IFlightLegDepartureStation;
    equipment: IFlightLegEquipment;
    flags: IFlightLegFlags;
    flyingTime: string;
    index: number;
    operatingBy: IOperatingBy;
    status: FlightStatus;
    updated: string;
    transition?: IFlightTransitions;
};

Create react-app/src/shared/types/flight.ts:

import type { FlightStatus, RouteType } from './enums';
import type { IFlightId } from './flight-id';
import type { IFlightLeg } from './flight-leg';
import type { IOperatingBy } from './operating-by';

export type IFlightBase = {
    flightId: IFlightId;
    flyingTime: string;
    operatingBy: IOperatingBy;
    id: string;
    status: FlightStatus;
};

export type IDirectFlight = IFlightBase & {
    routeType: RouteType.DIRECT;
    leg: IFlightLeg;
};

export type IMultiLegFlight = IFlightBase & {
    routeType: RouteType.MULTI_LEG;
    legs: IFlightLeg[];
};

export type IConnectingFlight = {
    flights: (IDirectFlight | IMultiLegFlight)[];
    routeType: RouteType.CONNECTING;
    flyingTime: string;
    status: FlightStatus;
};

export type ISimpleFlight = IDirectFlight | IMultiLegFlight;
export type IFlight = ISimpleFlight | IConnectingFlight;
  • Step 4: Create response and request types

Create react-app/src/shared/types/responses.ts:

import type { IFlight, ISimpleFlight } from './flight';

export type IBoardResponse = {
    data: {
        partners: string[];
        routes: ISimpleFlight[];
        daysOfFlight: string[];
    };
};

export type IDaysResponse = {
    days: string;
};

export type IScheduleResponse = IFlight[];
export type IScheduleDetailsResponse = IBoardResponse;

export type IDestinationsResponse = {
    data: {
        routes: IDestinationResponse[];
    };
};

export type IDestinationResponse = {
    isDirect: boolean;
    route: string[];
};

Create react-app/src/shared/types/popular-request.ts:

import type { RequestMode } from './enums';

export type IPopularRequestType = 'Schedule' | 'Onlineboard';

type IRequestRouteMode =
    | RequestMode.ROUTE
    | RequestMode.SCHEDULE_ROUTE_BOTH_DIRECTIONS;

export type IPopularRequest =
    | IPopularRouteRequest
    | IPopularArrivalRequest
    | IPopularDepartureRequest
    | IPopularFlightNumberRequest;

export type IPopularRouteRequest = {
    mode: IRequestRouteMode;
    departure: string;
    arrival: string;
    type: IPopularRequestType;
};

export type IPopularArrivalRequest = {
    mode: RequestMode.ARRIVAL;
    arrival: string;
    type: 'Onlineboard';
};

export type IPopularDepartureRequest = {
    mode: RequestMode.DEPARTURE;
    departure: string;
    type: 'Onlineboard';
};

export type IPopularFlightNumberRequest = {
    mode: RequestMode.FLIGHT_NUMBER;
    carrier: string;
    flightNumber: string;
    type: 'Onlineboard';
};

Create react-app/src/shared/types/common.ts:

export type IPosition = 'absolute' | 'static';
export type ITooltipPosition = 'top' | 'bottom';
export type IComponentSize = 'large' | 'medium' | 'small' | 'extra-small';
export type IAlign = 'left' | 'right' | 'mobile-left' | 'mobile-right';
export type ITextAlign = IAlign | 'center' | 'justify';

export type IUrlTimeRange = {
    timeFrom?: string;
    timeTo?: string;
};

export type IStringValues<T> = {
    [key in keyof T]: string;
};
  • Step 5: Create barrel export

Create react-app/src/shared/types/index.ts:

export * from './enums';
export * from './times';
export * from './operating-by';
export * from './equipment';
export * from './airport-info';
export * from './language';
export * from './locations';
export * from './flight-id';
export * from './flight-transition';
export * from './flight-station';
export * from './flight-leg';
export * from './flight';
export * from './responses';
export * from './popular-request';
export * from './common';
  • Step 6: Verify types compile
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npx tsc --noEmit --project tsconfig.json

Expected: No type errors.

  • Step 7: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/shared/types/
git commit -m "feat: port TypeScript type definitions from Angular app"

Task 5: Environment Configuration

Files:

  • Create: react-app/src/shared/config/environment.ts

  • Step 1: Create environment config

Create react-app/src/shared/config/environment.ts:

export type Environment = {
    apiRootUrl: string;
    wsRootUrl: string;
    mapApiUrl: string;
    mapAttribution: string;
    production: boolean;
    urlForChatBot: string;
    urlForTrackerHub: string;
    appInsights: {
        instrumentationKey: string;
        application: string;
        category: string;
        env: string;
        loggingLevelTelemetry: number;
        disableAjaxTracking: boolean;
    };
    refreshPauseMin: number;
    refreshStopMin: number;
    boardCalendarDatesEnabledCountBack: number;
    boardCalendarDatesEnabledCountForward: number;
    scheduleCalendarDatesEnabledCountBack: number;
    scheduleCalendarDatesEnabledCountForward: number;
    onlineRegistrationEnabledDaysBefore: number;
    onlineRegistrationEnabledHoursBefore: number;
    buyTicketEnabledDaysBeforeStart?: number;
    buyTicketEnabledHoursBeforeStop?: number;
    buyTicketEnabledDaysBeforeStop?: number;
    features: {
        flightsMap: boolean;
    };
};

const environments: Record<string, Environment> = {
    development: {
        apiRootUrl: '/api',
        wsRootUrl: '/flights',
        mapApiUrl: '/map/api/tile/{z}/{x}/{y}.jpeg',
        mapAttribution: '',
        production: false,
        urlForChatBot:
            'https://vi.aeroflot.ru/chat/widget.js?entityId=09588b62-1b79-4eff-ab7b-cfbefd4b4f74&accountId=251b09d7-390f-4c92-9a58-c22d20684b35',
        urlForTrackerHub:
            'http://platform.yc.webzavod.ru/tracker/hub',
        appInsights: {
            instrumentationKey: '',
            application: '',
            category: '',
            env: '',
            loggingLevelTelemetry: 1,
            disableAjaxTracking: true,
        },
        refreshPauseMin: 15,
        refreshStopMin: 60,
        boardCalendarDatesEnabledCountBack: 1,
        boardCalendarDatesEnabledCountForward: 14,
        scheduleCalendarDatesEnabledCountBack: 1,
        scheduleCalendarDatesEnabledCountForward: 330,
        onlineRegistrationEnabledDaysBefore: 1,
        onlineRegistrationEnabledHoursBefore: 1,
        features: {
            flightsMap: false,
        },
    },
    production: {
        apiRootUrl: '/api',
        wsRootUrl: '/flights',
        mapApiUrl: '/map/api/tile/{z}/{x}/{y}.jpeg',
        mapAttribution: '',
        production: true,
        urlForChatBot:
            'https://vi.aeroflot.ru/chat/widget.js?entityId=09588b62-1b79-4eff-ab7b-cfbefd4b4f74&accountId=251b09d7-390f-4c92-9a58-c22d20684b35',
        urlForTrackerHub:
            'https://platform.aeroflot.ru/tracker/hub',
        appInsights: {
            instrumentationKey: '',
            application: '',
            category: '',
            env: '',
            loggingLevelTelemetry: 1,
            disableAjaxTracking: true,
        },
        refreshPauseMin: 15,
        refreshStopMin: 60,
        boardCalendarDatesEnabledCountBack: 1,
        boardCalendarDatesEnabledCountForward: 14,
        scheduleCalendarDatesEnabledCountBack: 1,
        scheduleCalendarDatesEnabledCountForward: 330,
        onlineRegistrationEnabledDaysBefore: 1,
        onlineRegistrationEnabledHoursBefore: 1,
        buyTicketEnabledDaysBeforeStart: 330,
        buyTicketEnabledHoursBeforeStop: 6,
        buyTicketEnabledDaysBeforeStop: 1,
        features: {
            flightsMap: true,
        },
    },
};

const envName = process.env.NODE_ENV === 'production'
    ? 'production'
    : 'development';

export const environment: Environment = environments[envName]
    ?? environments.development;
  • Step 2: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/shared/config/
git commit -m "feat: add environment configuration"

Task 6: API Client & Endpoint Builder

Files:

  • Create: react-app/src/shared/api/endpoint.ts

  • Create: react-app/src/shared/api/client.ts

  • Create: react-app/src/shared/api/formatter.ts

  • Create: react-app/src/shared/api/index.ts

  • Step 1: Create date/time formatter

Create react-app/src/shared/api/formatter.ts:

export function formatDate(date: Date): string {
    return date.toISOString().replace(/\.\d{3}Z$/, '');
}

export function formatDateOnly(date: Date): string {
    return date.toISOString().split('T')[0];
}

export function formatTime(time: string): string {
    if (time === '2400') {
        return '23:59:59';
    }
    const hours = time.substring(0, 2);
    const minutes = time.substring(2, 4);
    return `${hours}:${minutes}:00`;
}
  • Step 2: Create endpoint builder

Create react-app/src/shared/api/endpoint.ts:

import { environment } from '../config/environment';

const DEFAULT_SCOPE = 'flights';
const DEFAULT_VERSION = 'v1.1';

export function buildEndpoint(
    path: string,
    lang: string,
    scope: string = DEFAULT_SCOPE,
    version: string = DEFAULT_VERSION,
): string {
    return `${environment.apiRootUrl}/${scope}/${version}/${lang}/${path}`;
}

export function buildBoardEndpoint(lang: string): string {
    return buildEndpoint('board', lang);
}

export function buildBoardDetailsEndpoint(lang: string): string {
    return buildEndpoint('onlineboard/details', lang);
}

export function buildBoardDaysEndpoint(
    lang: string,
    date: string,
    param: string,
): string {
    return buildEndpoint(`days/${date}/31/${param}/board`, lang);
}

export function buildScheduleEndpoint(lang: string): string {
    return buildEndpoint('schedule/1', lang);
}

export function buildScheduleDaysEndpoint(
    lang: string,
    date: string,
    param: string,
): string {
    return buildEndpoint(`days/${date}/382/${param}/schedule`, lang);
}

export function buildDestinationsEndpoint(lang: string): string {
    return buildEndpoint('destinations/1', lang);
}

export function buildFlightsMapDaysEndpoint(
    lang: string,
    date: string,
    param: string,
): string {
    return buildEndpoint(
        `days/${date}/200/${param}/flights-map`,
        lang,
    );
}

export function buildPopularRequestsEndpoint(): string {
    return `${environment.apiRootUrl}/Requests/1/getpopular`;
}
  • Step 3: Create HTTP client

Create react-app/src/shared/api/client.ts:

export class ApiError extends Error {
    constructor(
        public status: number,
        message: string,
    ) {
        super(message);
        this.name = 'ApiError';
    }
}

export async function fetchJson<T>(
    url: string,
    params?: Record<string, string>,
): Promise<T> {
    const searchParams = params
        ? '?' + new URLSearchParams(params).toString()
        : '';
    const response = await fetch(`${url}${searchParams}`);
    if (!response.ok) {
        throw new ApiError(
            response.status,
            `API error: ${response.status} ${response.statusText}`,
        );
    }
    return response.json() as Promise<T>;
}
  • Step 4: Create barrel export

Create react-app/src/shared/api/index.ts:

export { fetchJson, ApiError } from './client';
export {
    buildBoardEndpoint,
    buildBoardDetailsEndpoint,
    buildBoardDaysEndpoint,
    buildScheduleEndpoint,
    buildScheduleDaysEndpoint,
    buildDestinationsEndpoint,
    buildFlightsMapDaysEndpoint,
    buildPopularRequestsEndpoint,
} from './endpoint';
export {
    formatDate,
    formatDateOnly,
    formatTime,
} from './formatter';
  • Step 5: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/shared/api/
git commit -m "feat: add API client, endpoint builder, and date formatter"

Task 7: URL Parameter Utilities

Files:

  • Create: react-app/src/shared/utils/url-params.ts

  • Create: react-app/src/shared/utils/validators.ts

  • Create: react-app/src/shared/utils/index.ts

  • Step 1: Create validators

Create react-app/src/shared/utils/validators.ts:

const IATA_STATION_RE = /^[A-Z]{3}$/;
const FLIGHT_NUMBER_RE = /^\d{1,4}[A-Za-z]?$/;
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const TIME_RE = /^\d{4}$/;

export function isValidStation(code: string): boolean {
    return IATA_STATION_RE.test(code);
}

export function isValidFlightNumber(num: string): boolean {
    return FLIGHT_NUMBER_RE.test(num);
}

export function isValidDate(date: string): boolean {
    if (!DATE_RE.test(date)) return false;
    const parsed = new Date(date);
    return !isNaN(parsed.getTime());
}

export function isValidTime(time: string): boolean {
    if (!TIME_RE.test(time)) return false;
    const hours = parseInt(time.substring(0, 2), 10);
    const minutes = parseInt(time.substring(2, 4), 10);
    return hours >= 0 && hours <= 24 && minutes >= 0 && minutes <= 59;
}

export function isValidTimeRange(from: string, to: string): boolean {
    return isValidTime(from) && isValidTime(to);
}
  • Step 2: Create URL parameter parser

Create react-app/src/shared/utils/url-params.ts:

export type BoardFlightParams = {
    flightNumber: string;
    flightDate: string;
};

export type BoardDepartureParams = {
    departure: string;
    date: string;
    timeFrom: string;
    timeTo: string;
};

export type BoardArrivalParams = {
    arrival: string;
    date: string;
    timeFrom: string;
    timeTo: string;
};

export type BoardRouteParams = {
    departure: string;
    arrival: string;
    date: string;
    timeFrom: string;
    timeTo: string;
};

export function parseFlightParams(
    params: string,
): BoardFlightParams | null {
    const parts = params.split('-');
    if (parts.length < 2) return null;
    const flightDate = parts.slice(-1)[0];
    const flightNumber = parts.slice(0, -1).join('-');
    return { flightNumber, flightDate };
}

export function parseDepartureParams(
    params: string,
): BoardDepartureParams | null {
    const parts = params.split('-');
    if (parts.length < 3) return null;
    const departure = parts[0];
    const date = parts[1];
    const timeFrom = parts[2]?.substring(0, 4) ?? '0000';
    const timeTo = parts[2]?.substring(4, 8) ?? '2400';
    return { departure, date, timeFrom, timeTo };
}

export function parseArrivalParams(
    params: string,
): BoardArrivalParams | null {
    const parts = params.split('-');
    if (parts.length < 3) return null;
    const arrival = parts[0];
    const date = parts[1];
    const timeFrom = parts[2]?.substring(0, 4) ?? '0000';
    const timeTo = parts[2]?.substring(4, 8) ?? '2400';
    return { arrival, date, timeFrom, timeTo };
}

export function parseRouteParams(
    params: string,
): BoardRouteParams | null {
    const parts = params.split('-');
    if (parts.length < 4) return null;
    const departure = parts[0];
    const arrival = parts[1];
    const date = parts[2];
    const timeFrom = parts[3]?.substring(0, 4) ?? '0000';
    const timeTo = parts[3]?.substring(4, 8) ?? '2400';
    return { departure, arrival, date, timeFrom, timeTo };
}

export function buildFlightUrl(
    flightNumber: string,
    date: string,
): string {
    return `/onlineboard/flight/${flightNumber}-${date}`;
}

export function buildRouteUrl(
    departure: string,
    arrival: string,
    date: string,
    timeFrom: string,
    timeTo: string,
): string {
    return `/onlineboard/route/${departure}-${arrival}-${date}-${timeFrom}${timeTo}`;
}

export function buildDepartureUrl(
    departure: string,
    date: string,
    timeFrom: string,
    timeTo: string,
): string {
    return `/onlineboard/departure/${departure}-${date}-${timeFrom}${timeTo}`;
}

export function buildArrivalUrl(
    arrival: string,
    date: string,
    timeFrom: string,
    timeTo: string,
): string {
    return `/onlineboard/arrival/${arrival}-${date}-${timeFrom}${timeTo}`;
}
  • Step 3: Create barrel export

Create react-app/src/shared/utils/index.ts:

export * from './validators';
export * from './url-params';
  • Step 4: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/shared/utils/
git commit -m "feat: add URL parameter parsers and validators"

Task 8: Zustand Stores

Files:

  • Create: react-app/src/shared/stores/settings.ts

  • Create: react-app/src/shared/stores/user-location.ts

  • Create: react-app/src/shared/stores/search-history.ts

  • Create: react-app/src/shared/stores/online-board-filters.ts

  • Create: react-app/src/shared/stores/schedule-filters.ts

  • Create: react-app/src/shared/stores/flights-map-filters.ts

  • Create: react-app/src/shared/stores/index.ts

  • Step 1: Create settings store

Create react-app/src/shared/stores/settings.ts:

import { create } from 'zustand';
import type { Environment } from '../config/environment';
import { environment } from '../config/environment';

type SettingsState = Environment & {
    loaded: boolean;
    setSettings: (settings: Partial<Environment>) => void;
};

export const useSettingsStore = create<SettingsState>((set) => ({
    ...environment,
    loaded: false,
    setSettings: (settings) =>
        set((state) => ({ ...state, ...settings, loaded: true })),
}));
  • Step 2: Create filter stores

Create react-app/src/shared/stores/online-board-filters.ts:

import { create } from 'zustand';

type OnlineBoardFiltersState = {
    flightNumber: string;
    suffix: string;
    timeFrom: string;
    timeTo: string;
    routeDate: string;
    departure: string;
    arrival: string;
    setFlightNumber: (num: string, suffix?: string) => void;
    setTimeRange: (from: string, to: string) => void;
    setRouteDate: (date: string) => void;
    setDeparture: (dep: string) => void;
    setArrival: (arr: string) => void;
    clear: () => void;
};

const initialState = {
    flightNumber: '',
    suffix: '',
    timeFrom: '0000',
    timeTo: '2400',
    routeDate: '',
    departure: '',
    arrival: '',
};

export const useOnlineBoardFilters = create<OnlineBoardFiltersState>(
    (set) => ({
        ...initialState,
        setFlightNumber: (num, suffix = '') =>
            set({ flightNumber: num, suffix }),
        setTimeRange: (from, to) =>
            set({ timeFrom: from, timeTo: to }),
        setRouteDate: (date) => set({ routeDate: date }),
        setDeparture: (dep) => set({ departure: dep }),
        setArrival: (arr) => set({ arrival: arr }),
        clear: () => set(initialState),
    }),
);

Create react-app/src/shared/stores/schedule-filters.ts:

import { create } from 'zustand';

type ScheduleFiltersState = {
    departure: string;
    arrival: string;
    dateFrom: string;
    dateTo: string;
    timeFrom: string;
    timeTo: string;
    returnDateFrom: string;
    returnDateTo: string;
    directOnly: boolean;
    withReturn: boolean;
    setDeparture: (dep: string) => void;
    setArrival: (arr: string) => void;
    setDateRange: (from: string, to: string) => void;
    setTimeRange: (from: string, to: string) => void;
    setReturnDateRange: (from: string, to: string) => void;
    setDirectOnly: (val: boolean) => void;
    setWithReturn: (val: boolean) => void;
    clear: () => void;
};

const initialState = {
    departure: '',
    arrival: '',
    dateFrom: '',
    dateTo: '',
    timeFrom: '0000',
    timeTo: '2400',
    returnDateFrom: '',
    returnDateTo: '',
    directOnly: false,
    withReturn: false,
};

export const useScheduleFilters = create<ScheduleFiltersState>(
    (set) => ({
        ...initialState,
        setDeparture: (dep) => set({ departure: dep }),
        setArrival: (arr) => set({ arrival: arr }),
        setDateRange: (from, to) =>
            set({ dateFrom: from, dateTo: to }),
        setTimeRange: (from, to) =>
            set({ timeFrom: from, timeTo: to }),
        setReturnDateRange: (from, to) =>
            set({ returnDateFrom: from, returnDateTo: to }),
        setDirectOnly: (val) => set({ directOnly: val }),
        setWithReturn: (val) => set({ withReturn: val }),
        clear: () => set(initialState),
    }),
);

Create react-app/src/shared/stores/flights-map-filters.ts:

import { create } from 'zustand';

type FlightsMapFiltersState = {
    departure: string;
    setDeparture: (dep: string) => void;
    clear: () => void;
};

export const useFlightsMapFilters = create<FlightsMapFiltersState>(
    (set) => ({
        departure: '',
        setDeparture: (dep) => set({ departure: dep }),
        clear: () => set({ departure: '' }),
    }),
);
  • Step 3: Create user location and search history stores

Create react-app/src/shared/stores/user-location.ts:

import { create } from 'zustand';

type UserLocationState = {
    lat: number | null;
    lon: number | null;
    detected: boolean;
    error: string | null;
    detect: () => void;
};

export const useUserLocationStore = create<UserLocationState>(
    (set) => ({
        lat: null,
        lon: null,
        detected: false,
        error: null,
        detect: () => {
            if (typeof navigator === 'undefined' || !navigator.geolocation) {
                set({ error: 'Geolocation not supported', detected: true });
                return;
            }
            navigator.geolocation.getCurrentPosition(
                (pos) =>
                    set({
                        lat: pos.coords.latitude,
                        lon: pos.coords.longitude,
                        detected: true,
                        error: null,
                    }),
                (err) =>
                    set({
                        error: err.message,
                        detected: true,
                    }),
            );
        },
    }),
);

Create react-app/src/shared/stores/search-history.ts:

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

type SearchEntry = {
    query: string;
    url: string;
    timestamp: number;
};

type SearchHistoryState = {
    entries: SearchEntry[];
    addEntry: (query: string, url: string) => void;
    clear: () => void;
};

const MAX_ENTRIES = 10;

export const useSearchHistoryStore = create<SearchHistoryState>()(
    persist(
        (set) => ({
            entries: [],
            addEntry: (query, url) =>
                set((state) => ({
                    entries: [
                        { query, url, timestamp: Date.now() },
                        ...state.entries.filter(
                            (e) => e.url !== url,
                        ),
                    ].slice(0, MAX_ENTRIES),
                })),
            clear: () => set({ entries: [] }),
        }),
        { name: 'afl-search-history' },
    ),
);
  • Step 4: Create barrel export

Create react-app/src/shared/stores/index.ts:

export { useSettingsStore } from './settings';
export { useOnlineBoardFilters } from './online-board-filters';
export { useScheduleFilters } from './schedule-filters';
export { useFlightsMapFilters } from './flights-map-filters';
export { useUserLocationStore } from './user-location';
export { useSearchHistoryStore } from './search-history';
  • Step 5: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/shared/stores/
git commit -m "feat: add Zustand stores for settings, filters, and user state"

Task 9: i18n Setup

Files:

  • Create: react-app/src/i18n/config.ts

  • Copy: ClientApp/src/assets/i18n/*.json -> react-app/src/i18n/locales/

  • Step 1: Copy existing translation files

cp /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/ClientApp/src/assets/i18n/*.json /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app/src/i18n/locales/
  • Step 2: Create i18n configuration

Create react-app/src/i18n/config.ts:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import ru from './locales/ru.json';
import en from './locales/en.json';
import de from './locales/de.json';
import fr from './locales/fr.json';
import es from './locales/es.json';
import it from './locales/it.json';
import ja from './locales/ja.json';
import ko from './locales/ko.json';
import zh from './locales/zh.json';

const resources = {
    ru: { translation: ru },
    en: { translation: en },
    de: { translation: de },
    fr: { translation: fr },
    es: { translation: es },
    it: { translation: it },
    ja: { translation: ja },
    ko: { translation: ko },
    zh: { translation: zh },
};

i18n.use(initReactI18next).init({
    resources,
    lng: 'ru',
    fallbackLng: 'en',
    interpolation: {
        escapeValue: false,
    },
});

export default i18n;
  • Step 3: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/i18n/
git commit -m "feat: set up react-i18next with existing translation files"

Task 10: Theme Tokens & Style Isolation

Files:

  • Create: react-app/src/styles/theme/tokens.css

  • Create: react-app/src/styles/global.module.css

  • Step 1: Create CSS custom property tokens

Create react-app/src/styles/theme/tokens.css:

/* Tier 1 -- Primitive tokens */
.afl-flights {
    --afl-blue-500: #003DA5;
    --afl-blue-600: #002D7A;
    --afl-blue-100: #E6EFF8;
    --afl-gray-50: #FAFAFA;
    --afl-gray-100: #F5F5F5;
    --afl-gray-200: #EEEEEE;
    --afl-gray-300: #E0E0E0;
    --afl-gray-400: #BDBDBD;
    --afl-gray-500: #9E9E9E;
    --afl-gray-600: #757575;
    --afl-gray-700: #616161;
    --afl-gray-800: #424242;
    --afl-gray-900: #1A1A1A;
    --afl-white: #FFFFFF;
    --afl-red-500: #E31E24;
    --afl-green-500: #2E7D32;
    --afl-orange-500: #F57C00;
}

/* Tier 2 -- Semantic tokens */
.afl-flights {
    --afl-primary: var(--afl-blue-500);
    --afl-primary-hover: var(--afl-blue-600);
    --afl-primary-light: var(--afl-blue-100);
    --afl-surface: var(--afl-white);
    --afl-surface-alt: var(--afl-gray-50);
    --afl-text-color: var(--afl-gray-900);
    --afl-text-secondary: var(--afl-gray-600);
    --afl-border-color: var(--afl-gray-300);
    --afl-error: var(--afl-red-500);
    --afl-success: var(--afl-green-500);
    --afl-warning: var(--afl-orange-500);
    --afl-border-radius: 8px;
    --afl-border-radius-sm: 4px;
    --afl-spacing-unit: 4px;
}

/* Tier 3 -- Component tokens */
.afl-flights {
    --afl-btn-bg: var(--afl-primary);
    --afl-btn-text: var(--afl-white);
    --afl-btn-hover-bg: var(--afl-primary-hover);
    --afl-card-bg: var(--afl-surface);
    --afl-card-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    --afl-input-border: var(--afl-border-color);
    --afl-input-focus-border: var(--afl-primary);
}

/* Dark theme variant */
.afl-flights[data-theme='dark'] {
    --afl-surface: #1E1E2E;
    --afl-surface-alt: #2A2A3E;
    --afl-text-color: #E0E0E0;
    --afl-text-secondary: #A0A0A0;
    --afl-border-color: #404050;
    --afl-primary: #4DA3FF;
    --afl-primary-hover: #3A8FE8;
    --afl-primary-light: #1A2A40;
}

/* PrimeReact token mapping */
.afl-flights {
    --p-primary-color: var(--afl-primary);
    --p-primary-hover-color: var(--afl-primary-hover);
    --p-surface-0: var(--afl-surface);
}
  • Step 2: Create global scoped styles

Create react-app/src/styles/global.module.css:

.aflFlights {
    font-family: 'Aeroflot Sans', -apple-system, BlinkMacSystemFont,
        'Segoe UI', Roboto, sans-serif;
    font-size: 16px;
    line-height: 1.5;
    color: var(--afl-text-color, #1a1a1a);
    direction: ltr;
    text-align: left;
    isolation: isolate;
    container-type: inline-size;
    container-name: flights-root;
}

.aflFlights *,
.aflFlights *::before,
.aflFlights *::after {
    box-sizing: border-box;
}
  • Step 3: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/styles/
git commit -m "feat: add three-tier CSS token system and style isolation"

Task 11: App Shell & Root Layout

Files:

  • Create: react-app/src/App.tsx

  • Create: react-app/src/routes/layout.tsx

  • Create: react-app/src/routes/error/404.tsx

  • Step 1: Create App shell component

Create react-app/src/App.tsx:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { PrimeReactProvider } from 'primereact/api';
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n/config';
import './styles/theme/tokens.css';
import styles from './styles/global.module.css';

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            staleTime: 30_000,
            refetchOnWindowFocus: true,
        },
    },
});

type AppProps = {
    lang?: string;
    theme?: 'light' | 'dark';
    children: React.ReactNode;
};

export function AppProviders({ lang, theme = 'light', children }: AppProps) {
    if (lang && lang !== i18n.language) {
        i18n.changeLanguage(lang);
    }

    return (
        <QueryClientProvider client={queryClient}>
            <I18nextProvider i18n={i18n}>
                <PrimeReactProvider value={{ appendTo: 'self' }}>
                    <div
                        className={`afl-flights ${styles.aflFlights}`}
                        data-theme={theme}
                    >
                        {children}
                    </div>
                </PrimeReactProvider>
            </I18nextProvider>
        </QueryClientProvider>
    );
}
  • Step 2: Create root layout

Create react-app/src/routes/layout.tsx:

import { Outlet } from '@modern-js/runtime/router';
import { Suspense } from 'react';
import { AppProviders } from '../App';

export default function RootLayout() {
    return (
        <AppProviders>
            <Suspense fallback={<div>Loading...</div>}>
                <Outlet />
            </Suspense>
        </AppProviders>
    );
}
  • Step 3: Create 404 page

Create react-app/src/routes/error/404.tsx:

import { useTranslation } from 'react-i18next';

export default function NotFoundPage() {
    const { t } = useTranslation();
    return (
        <div>
            <h1>404</h1>
            <p>{t('ERROR.NOT-FOUND', 'Page not found')}</p>
        </div>
    );
}
  • Step 4: Verify dev server starts
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npm run dev

Expected: Dev server starts, page loads at the dev server URL.

  • Step 5: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/App.tsx react-app/src/routes/
git commit -m "feat: add app shell with providers and root layout"

Task 12: Routing Shell with Language Redirects

Files:

  • Create: react-app/src/routes/page.tsx (root redirect)

  • Create: react-app/src/routes/onlineboard/page.tsx

  • Create: react-app/src/routes/schedule/page.tsx

  • Create: react-app/src/routes/flights-map/page.tsx

  • Create: react-app/src/shared/hooks/useFeatureFlag.ts

  • Step 1: Create root page redirect

Create react-app/src/routes/page.tsx:

import { Navigate } from '@modern-js/runtime/router';

export default function RootPage() {
    return <Navigate to="/onlineboard" replace />;
}
  • Step 2: Create feature flag hook

Create react-app/src/shared/hooks/useFeatureFlag.ts:

import { useSettingsStore } from '../stores';
import type { Environment } from '../config/environment';

export function useFeatureFlag(flag: keyof Environment['features']): boolean {
    return useSettingsStore((state) => state.features[flag]);
}
  • Step 3: Create placeholder feature pages

Create react-app/src/routes/onlineboard/page.tsx:

import { OnlineBoard } from '../../features/online-board';

export default function OnlineBoardPage() {
    return <OnlineBoard />;
}

Create react-app/src/routes/schedule/page.tsx:

import { Schedule } from '../../features/schedule';

export default function SchedulePage() {
    return <Schedule />;
}

Create react-app/src/routes/flights-map/page.tsx:

import { Navigate } from '@modern-js/runtime/router';
import { FlightsMap } from '../../features/flights-map';
import { useSettingsStore } from '../../shared/stores';

export default function FlightsMapPage() {
    const flightsMapEnabled = useSettingsStore(
        (state) => state.features.flightsMap,
    );

    if (!flightsMapEnabled) {
        return <Navigate to="/onlineboard" replace />;
    }

    return <FlightsMap />;
}
  • Step 4: Verify routing works
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npm run dev

Visit the dev server URL:

  • Root / should redirect to /onlineboard

  • /onlineboard should show "Online Board -- placeholder"

  • /schedule should show "Schedule -- placeholder"

  • /flights-map should redirect to /onlineboard (feature flag is false in dev)

  • Step 5: Commit

cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/routes/ react-app/src/shared/hooks/
git commit -m "feat: add routing shell with feature flag guard"

Task 13: SEO Components

Files:

  • Create: react-app/src/shared/seo/MetaTags.tsx

  • Create: react-app/src/shared/seo/FlightJsonLd.tsx

  • Create: react-app/src/shared/seo/index.ts

  • Step 1: Create MetaTags component

Create react-app/src/shared/seo/MetaTags.tsx:

import type { SupportedLanguage } from '../types/language';
import { SUPPORTED_LANGUAGES } from '../types/language';

type MetaTagsProps = {
    title: string;
    description: string;
    canonical?: string;
    noRobots?: boolean;
    ogImage?: string;
    locale?: SupportedLanguage;
};

const LOCALE_MAP: Record<SupportedLanguage, string> = {
    ru: 'ru_RU',
    en: 'en_US',
    de: 'de_DE',
    fr: 'fr_FR',
    es: 'es_ES',
    it: 'it_IT',
    ja: 'ja_JP',
    ko: 'ko_KR',
    zh: 'zh_CN',
};

export function MetaTags({
    title,
    description,
    canonical,
    noRobots = false,
    ogImage,
    locale = 'ru',
}: MetaTagsProps) {
    return (
        <>
            <title>{title}</title>
            <meta name="description" content={description} />
            <meta
                name="robots"
                content={noRobots ? 'noindex,nofollow' : 'index,follow'}
            />
            <meta property="og:title" content={title} />
            <meta property="og:description" content={description} />
            <meta property="og:type" content="website" />
            <meta property="og:locale" content={LOCALE_MAP[locale]} />
            {canonical && (
                <>
                    <link rel="canonical" href={canonical} />
                    <meta property="og:url" content={canonical} />
                </>
            )}
            {ogImage && (
                <>
                    <meta property="og:image" content={ogImage} />
                    <meta property="og:image:width" content="1200" />
                    <meta property="og:image:height" content="630" />
                    <meta
                        name="twitter:card"
                        content="summary_large_image"
                    />
                    <meta name="twitter:image" content={ogImage} />
                </>
            )}
            {SUPPORTED_LANGUAGES.filter((l) => l !== locale).map(
                (l) => (
                    <meta
                        key={l}
                        property="og:locale:alternate"
                        content={LOCALE_MAP[l]}
                    />
                ),
            )}
        </>
    );
}
  • Step 2: Create JSON-LD component

Create react-app/src/shared/seo/FlightJsonLd.tsx:

Note: JSON-LD content is trusted (constructed from our own API data, not user input), so the inline script is safe.

import type { ISimpleFlight } from '../types';
import { RouteType } from '../types';

function getFirstLeg(flight: ISimpleFlight) {
    return flight.routeType === RouteType.DIRECT
        ? flight.leg
        : flight.legs[0];
}

function getLastLeg(flight: ISimpleFlight) {
    return flight.routeType === RouteType.DIRECT
        ? flight.leg
        : flight.legs[flight.legs.length - 1];
}

function buildFlightSchema(flight: ISimpleFlight) {
    const firstLeg = getFirstLeg(flight);
    const lastLeg = getLastLeg(flight);
    return {
        '@type': 'Flight',
        flightNumber: `${flight.flightId.carrier}-${flight.flightId.flightNumber}`,
        airline: {
            '@type': 'Airline',
            name: 'Aeroflot',
            iataCode: flight.flightId.carrier,
        },
        departureAirport: {
            '@type': 'Airport',
            name: firstLeg.departure.scheduled.airport,
            iataCode: firstLeg.departure.scheduled.airportCode,
        },
        arrivalAirport: {
            '@type': 'Airport',
            name: lastLeg.arrival.scheduled.airport,
            iataCode: lastLeg.arrival.scheduled.airportCode,
        },
        departureTime:
            firstLeg.departure.times.scheduledDeparture.utc,
        arrivalTime: lastLeg.arrival.times.scheduledArrival.utc,
        estimatedFlightDuration: flight.flyingTime,
        departureTerminal: firstLeg.departure.terminal,
        arrivalTerminal: lastLeg.arrival.terminal,
    };
}

type FlightJsonLdProps = {
    flight: ISimpleFlight;
};

export function FlightJsonLd({ flight }: FlightJsonLdProps) {
    const schema = {
        '@context': 'https://schema.org',
        ...buildFlightSchema(flight),
    };

    return (
        <script type="application/ld+json">
            {JSON.stringify(schema)}
        </script>
    );
}

type FlightListJsonLdProps = {
    flights: ISimpleFlight[];
};

export function FlightListJsonLd({ flights }: FlightListJsonLdProps) {
    const schema = {
        '@context': 'https://schema.org',
        '@type': 'ItemList',
        itemListElement: flights.map((f, i) => ({
            '@type': 'ListItem',
            position: i + 1,
            item: buildFlightSchema(f),
        })),
    };

    return (
        <script type="application/ld+json">
            {JSON.stringify(schema)}
        </script>
    );
}
  • Step 3: Create barrel export

Create react-app/src/shared/seo/index.ts:

export { MetaTags } from './MetaTags';
export { FlightJsonLd, FlightListJsonLd } from './FlightJsonLd';
  • Step 4: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/shared/seo/
git commit -m "feat: add MetaTags, JSON-LD, and OpenGraph SEO components"

Task 14: TanStack Query Hooks

Files:

  • Create: react-app/src/shared/hooks/queries.ts

  • Modify: react-app/src/shared/hooks/index.ts

  • Step 1: Create query hooks

Create react-app/src/shared/hooks/queries.ts:

import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { fetchJson } from '../api/client';
import {
    buildBoardEndpoint,
    buildBoardDetailsEndpoint,
    buildBoardDaysEndpoint,
    buildScheduleEndpoint,
    buildScheduleDaysEndpoint,
    buildDestinationsEndpoint,
    buildFlightsMapDaysEndpoint,
    buildPopularRequestsEndpoint,
} from '../api/endpoint';
import type {
    IBoardResponse,
    IDaysResponse,
    IScheduleResponse,
    IScheduleDetailsResponse,
    IDestinationsResponse,
    IPopularRequest,
} from '../types';

export function useFlightsQuery(params: {
    date: string;
    departure?: string;
    arrival?: string;
}) {
    const { i18n } = useTranslation();
    const lang = i18n.language;
    return useQuery({
        queryKey: ['flights', 'board', params.date, params.departure, params.arrival],
        queryFn: () =>
            fetchJson<IBoardResponse>(buildBoardEndpoint(lang), {
                date: params.date,
                ...(params.departure && { departure: params.departure }),
                ...(params.arrival && { arrival: params.arrival }),
            }),
        staleTime: Infinity,
        enabled: !!params.date,
    });
}

export function useFlightDetailsQuery(params: {
    flightNumber: string;
    date: string;
}) {
    const { i18n } = useTranslation();
    const lang = i18n.language;
    return useQuery({
        queryKey: ['flights', 'detail', params.flightNumber, params.date],
        queryFn: () =>
            fetchJson<IScheduleDetailsResponse>(
                buildBoardDetailsEndpoint(lang),
                {
                    flightNumber: params.flightNumber,
                    date: params.date,
                },
            ),
        staleTime: Infinity,
        enabled: !!params.flightNumber && !!params.date,
    });
}

export function useFlightDaysQuery(params: {
    date: string;
    param: string;
}) {
    const { i18n } = useTranslation();
    const lang = i18n.language;
    return useQuery({
        queryKey: ['flights', 'days', params.date, params.param],
        queryFn: () =>
            fetchJson<IDaysResponse>(
                buildBoardDaysEndpoint(lang, params.date, params.param),
            ),
        enabled: !!params.date && !!params.param,
    });
}

export function useScheduleQuery(params: {
    departure: string;
    arrival: string;
}) {
    const { i18n } = useTranslation();
    const lang = i18n.language;
    return useQuery({
        queryKey: ['schedule', 'search', params.departure, params.arrival],
        queryFn: () =>
            fetchJson<IScheduleResponse>(buildScheduleEndpoint(lang), {
                departure: params.departure,
                arrival: params.arrival,
            }),
        enabled: !!params.departure && !!params.arrival,
    });
}

export function useScheduleDaysQuery(params: {
    date: string;
    param: string;
}) {
    const { i18n } = useTranslation();
    const lang = i18n.language;
    return useQuery({
        queryKey: ['schedule', 'days', params.date, params.param],
        queryFn: () =>
            fetchJson<IDaysResponse>(
                buildScheduleDaysEndpoint(lang, params.date, params.param),
            ),
        enabled: !!params.date && !!params.param,
    });
}

export function useDestinationsQuery(params: { departure: string }) {
    const { i18n } = useTranslation();
    const lang = i18n.language;
    return useQuery({
        queryKey: ['map', 'destinations', params.departure],
        queryFn: () =>
            fetchJson<IDestinationsResponse>(
                buildDestinationsEndpoint(lang),
                { departure: params.departure },
            ),
        enabled: !!params.departure,
    });
}

export function usePopularRequestsQuery() {
    return useQuery({
        queryKey: ['popular-requests'],
        queryFn: () =>
            fetchJson<IPopularRequest[]>(
                buildPopularRequestsEndpoint(),
            ),
        staleTime: 60_000,
    });
}
  • Step 2: Update barrel export

Create react-app/src/shared/hooks/index.ts:

export { useFeatureFlag } from './useFeatureFlag';
export {
    useFlightsQuery,
    useFlightDetailsQuery,
    useFlightDaysQuery,
    useScheduleQuery,
    useScheduleDaysQuery,
    useDestinationsQuery,
    usePopularRequestsQuery,
} from './queries';
  • Step 3: Verify build compiles
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npm run build

Expected: Build succeeds.

  • Step 4: Commit
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/src/shared/hooks/
git commit -m "feat: add TanStack Query hooks for all API endpoints"

Task 15: Verify Full Foundation

  • Step 1: Run the dev server and verify
cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npm run dev

Verify:

  • App loads at the dev server URL

  • Root / redirects to /onlineboard

  • /onlineboard shows "Online Board -- placeholder"

  • /schedule shows "Schedule -- placeholder"

  • /flights-map redirects to /onlineboard (feature flag is false in dev)

  • No console errors

  • Step 2: Run production build

cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web/react-app
npm run build

Verify:

  • Build succeeds

  • mf-manifest.json exists in the output directory

  • No build warnings about missing exports

  • Step 3: Commit final state

cd /Users/gnezim/_projects/tims/flights_web/Aeroflot.Flights.Web
git add react-app/
git commit -m "feat: complete Phase 1 foundation -- project skeleton ready for feature implementation"

What This Plan Produces

After completing all 15 tasks, you have:

  • A working ModernJS SSR application with Module Federation 2.0
  • All TypeScript types ported from Angular
  • Environment configuration for dev/prod
  • API client with endpoint builder for all REST endpoints
  • URL parameter parsers and validators
  • Zustand stores for settings, filters, user location, search history
  • react-i18next with all 9 language files
  • Three-tier CSS token system with style isolation
  • App shell with providers (QueryClient, i18n, PrimeReact, theme)
  • Routing shell with all route paths and feature flag guard
  • SEO components (MetaTags, JSON-LD, OpenGraph)
  • TanStack Query hooks for all API endpoints
  • Placeholder feature components that MF exposes correctly

Next Plans

  • Phase 2: Online Board -- implement the main flight board feature with search, results, details, and day navigation
  • Phase 3: Schedule -- implement schedule search with date ranges and return flights
  • Phase 4: Flights Map -- implement Leaflet map with destination display
  • Phase 5: Cross-Cutting -- SignalR real-time integration, analytics, logging, web vitals, performance optimization