- 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
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.tsfiles 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 -
/onlineboardshould show "Online Board -- placeholder" -
/scheduleshould show "Schedule -- placeholder" -
/flights-mapshould 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 -
/onlineboardshows "Online Board -- placeholder" -
/scheduleshows "Schedule -- placeholder" -
/flights-mapredirects 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.jsonexists 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