plan/react-rewrite #1

Merged
gnezim merged 138 commits from plan/react-rewrite into main 2026-04-15 12:21:16 +03:00
2 changed files with 329 additions and 0 deletions
Showing only changes of commit 44ae7f1642 - Show all commits
+209
View File
@@ -0,0 +1,209 @@
import { describe, expect, it } from "vitest";
import type { Flight, ItemList } from "schema-dts";
import { buildFlightJsonLd, buildFlightListJsonLd } from "./json-ld.js";
import type { ISimpleFlight } from "./types.js";
function makeDirectFlight(overrides: Partial<{
carrier: string;
flightNumber: string;
date: string;
depCode: string;
depCity: string;
depAirport: string;
arrCode: string;
arrCity: string;
arrAirport: string;
depTime: string;
arrTime: string;
aircraft: string;
}> = {}): ISimpleFlight {
return {
id: "test-flight-1",
routeType: "Direct",
flightId: {
carrier: overrides.carrier ?? "SU",
flightNumber: overrides.flightNumber ?? "0100",
suffix: "",
date: overrides.date ?? "20250115",
},
flyingTime: "10h 30m",
operatingBy: {},
status: "Scheduled",
leg: {
index: 0,
status: "Scheduled",
flyingTime: "10h 30m",
updated: "2025-01-15T10:00:00Z",
dayChange: 0,
equipment: { name: overrides.aircraft ?? "Boeing 777-300ER", code: "773" },
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
operatingBy: {},
departure: {
scheduled: {
airport: overrides.depAirport ?? "Sheremetyevo International Airport",
airportCode: overrides.depCode ?? "SVO",
city: overrides.depCity ?? "Moscow",
cityCode: "MOW",
countryCode: "RU",
},
checkingStatus: "closed",
times: {
scheduledDeparture: {
dayChange: { value: 0, title: "" },
local: overrides.depTime ?? "2025-01-15T10:00:00",
localTime: "10:00",
tzOffset: 3,
utc: "2025-01-15T07:00:00Z",
},
},
},
arrival: {
scheduled: {
airport: overrides.arrAirport ?? "John F. Kennedy International Airport",
airportCode: overrides.arrCode ?? "JFK",
city: overrides.arrCity ?? "New York",
cityCode: "NYC",
countryCode: "US",
},
times: {
scheduledArrival: {
dayChange: { value: 0, title: "" },
local: overrides.arrTime ?? "2025-01-15T14:30:00",
localTime: "14:30",
tzOffset: -5,
utc: "2025-01-15T19:30:00Z",
},
},
},
},
};
}
describe("buildFlightJsonLd", () => {
it("returns a Flight schema with correct @type", () => {
const flight = makeDirectFlight();
const result = buildFlightJsonLd(flight);
expect(result["@type"]).toBe("Flight");
});
it("sets flightNumber in IATA format", () => {
const flight = makeDirectFlight({ carrier: "SU", flightNumber: "0100" });
const result = buildFlightJsonLd(flight);
expect(result.flightNumber).toBe("SU0100");
});
it("maps departure airport", () => {
const flight = makeDirectFlight({ depCode: "SVO", depAirport: "Sheremetyevo" });
const result = buildFlightJsonLd(flight);
expect(result.departureAirport).toEqual({
"@type": "Airport",
iataCode: "SVO",
name: "Sheremetyevo",
});
});
it("maps arrival airport", () => {
const flight = makeDirectFlight({ arrCode: "JFK", arrAirport: "JFK Airport" });
const result = buildFlightJsonLd(flight);
expect(result.arrivalAirport).toEqual({
"@type": "Airport",
iataCode: "JFK",
name: "JFK Airport",
});
});
it("maps departure and arrival times", () => {
const flight = makeDirectFlight({
depTime: "2025-01-15T10:00:00",
arrTime: "2025-01-15T14:30:00",
});
const result = buildFlightJsonLd(flight);
expect(result.departureTime).toBe("2025-01-15T10:00:00");
expect(result.arrivalTime).toBe("2025-01-15T14:30:00");
});
it("maps estimated flight duration", () => {
const flight = makeDirectFlight();
const result = buildFlightJsonLd(flight);
expect(result.estimatedFlightDuration).toBe("10h 30m");
});
it("maps provider to Aeroflot", () => {
const flight = makeDirectFlight();
const result = buildFlightJsonLd(flight);
expect(result.provider).toEqual({
"@type": "Airline",
name: "Aeroflot",
iataCode: "SU",
});
});
it("is type-compatible with schema-dts Flight", () => {
const flight = makeDirectFlight();
const result = buildFlightJsonLd(flight);
// TypeScript compile-time check: assigning to Flight should not error
const _typed: Flight = result;
expect(_typed["@type"]).toBe("Flight");
});
});
describe("buildFlightListJsonLd", () => {
it("returns an ItemList schema with correct @type", () => {
const flights = [makeDirectFlight()];
const result = buildFlightListJsonLd(flights, "Flights from Moscow");
expect(result["@type"]).toBe("ItemList");
});
it("sets the description on the ItemList", () => {
const flights = [makeDirectFlight()];
const result = buildFlightListJsonLd(flights, "Departures from SVO");
expect(result.description).toBe("Departures from SVO");
});
it("wraps each flight as a ListItem with position", () => {
const flights = [
makeDirectFlight({ carrier: "SU", flightNumber: "0100" }),
makeDirectFlight({ carrier: "SU", flightNumber: "0200" }),
];
const result = buildFlightListJsonLd(flights, "Flights");
const items = result.itemListElement;
expect(Array.isArray(items)).toBe(true);
const itemArray = items as Array<{ "@type": string; position: number; item: Flight }>;
expect(itemArray).toHaveLength(2);
expect(itemArray[0]!["@type"]).toBe("ListItem");
expect(itemArray[0]!.position).toBe(1);
expect(itemArray[1]!.position).toBe(2);
});
it("embeds Flight objects inside ListItem.item", () => {
const flights = [makeDirectFlight({ carrier: "SU", flightNumber: "0100" })];
const result = buildFlightListJsonLd(flights, "Flights");
const items = result.itemListElement as unknown as Array<{ item: Flight }>;
expect(items[0]!.item["@type"]).toBe("Flight");
expect(items[0]!.item.flightNumber).toBe("SU0100");
});
it("handles empty flight list", () => {
const result = buildFlightListJsonLd([], "No flights");
expect(result["@type"]).toBe("ItemList");
expect(result.itemListElement).toEqual([]);
});
it("is type-compatible with schema-dts ItemList", () => {
const result = buildFlightListJsonLd([], "test");
const _typed: ItemList = result;
expect(_typed["@type"]).toBe("ItemList");
});
});
+120
View File
@@ -0,0 +1,120 @@
/**
* JSON-LD schema builders for Online Board pages.
*
* Produces schema-dts typed objects ready for <JsonLdRenderer>.
* Uses schema.org Flight and ItemList types.
*
* @module
*/
import type { Flight, ItemList, ListItem } from "schema-dts";
import type { ISimpleFlight, IFlightLeg } from "./types.js";
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/**
* Get the first leg from a flight (handles both Direct and MultiLeg).
*/
function getFirstLeg(flight: ISimpleFlight): IFlightLeg | undefined {
if (flight.routeType === "Direct") {
return flight.leg;
}
return flight.legs[0];
}
/**
* Get the last leg from a flight (handles both Direct and MultiLeg).
*/
function getLastLeg(flight: ISimpleFlight): IFlightLeg | undefined {
if (flight.routeType === "Direct") {
return flight.leg;
}
const { legs } = flight;
return legs[legs.length - 1];
}
// ---------------------------------------------------------------------------
// Public JSON-LD builders
// ---------------------------------------------------------------------------
/**
* Build a schema.org Flight JSON-LD object from a flight record.
*
* Maps the first leg's departure and last leg's arrival for multi-leg flights.
*/
export function buildFlightJsonLd(flight: ISimpleFlight): Flight {
const firstLeg = getFirstLeg(flight);
const lastLeg = getLastLeg(flight);
const { carrier, flightNumber } = flight.flightId;
const result: Flight = {
"@type": "Flight",
flightNumber: `${carrier}${flightNumber}`,
provider: {
"@type": "Airline",
name: "Aeroflot",
iataCode: carrier,
},
estimatedFlightDuration: flight.flyingTime,
};
if (firstLeg) {
result.departureAirport = {
"@type": "Airport",
iataCode: firstLeg.departure.scheduled.airportCode,
name: firstLeg.departure.scheduled.airport,
};
result.departureTime = firstLeg.departure.times.scheduledDeparture.local;
if (firstLeg.departure.terminal) {
result.departureTerminal = firstLeg.departure.terminal;
}
if (firstLeg.departure.gate) {
result.departureGate = firstLeg.departure.gate;
}
}
if (lastLeg) {
result.arrivalAirport = {
"@type": "Airport",
iataCode: lastLeg.arrival.scheduled.airportCode,
name: lastLeg.arrival.scheduled.airport,
};
result.arrivalTime = lastLeg.arrival.times.scheduledArrival.local;
if (lastLeg.arrival.terminal) {
result.arrivalTerminal = lastLeg.arrival.terminal;
}
}
if (firstLeg?.equipment.name) {
result.aircraft = firstLeg.equipment.name;
}
return result;
}
/**
* Build a schema.org ItemList of Flight JSON-LD objects from search results.
*
* Each flight becomes a ListItem with a position (1-indexed).
*/
export function buildFlightListJsonLd(
flights: ISimpleFlight[],
searchDescription: string,
): ItemList {
const items: ListItem[] = flights.map((flight, index) => ({
"@type": "ListItem" as const,
position: index + 1,
item: buildFlightJsonLd(flight),
}));
return {
"@type": "ItemList",
description: searchDescription,
itemListElement: items,
};
}