trains: pokedex pages (builders, operators, countries, detail, random) and registrations
All checks were successful
Deploy production / deploy (push) Successful in 1m9s

This commit is contained in:
2026-10-25 00:11:50 +00:00
parent 8c0fc74283
commit 5f940e8196
28 changed files with 2630 additions and 0 deletions

243
src/lib/trains/shared.ts Normal file
View File

@@ -0,0 +1,243 @@
// Shared (client + server) labels and helpers for the train pokedex (/trackers/trains).
export {
dexLabel,
monogram,
brandHue,
fmtCompact,
fmtNumber,
thumbAt,
thumbSrcset,
commonsPage
} from '#lib/cars/shared.js';
export type { CarPhoto as TrainPhoto } from '#lib/cars/shared.js';
export const CATEGORIES = [
'steam',
'diesel',
'electric',
'high-speed',
'multiple unit',
'metro',
'tram',
'carriage',
'other'
] as const;
export const TRACTIONS = [
'steam',
'diesel',
'electric',
'bi-mode',
'battery',
'hydrogen',
'gas turbine',
'none'
] as const;
export const ERAS = {
pioneer: { label: 'pioneers (–1869)', from: null, to: 1869 },
steam: { label: 'age of steam (1870–1919)', from: 1870, to: 1919 },
interwar: { label: 'interwar (1920–1945)', from: 1920, to: 1945 },
modern: { label: 'modernisation (1946–1979)', from: 1946, to: 1979 },
late: { label: 'late 20th century (1980–1999)', from: 1980, to: 1999 },
new: { label: '21st century (2000–)', from: 2000, to: null }
} as const;
export type TrainEra = keyof typeof ERAS;
export const SORTS = {
built: 'most built',
dex: 'dex #',
name: 'name',
newest: 'newest',
oldest: 'oldest',
fastest: 'fastest',
power: 'most powerful'
} as const;
export type TrainSort = keyof typeof SORTS;
export const categoryLabel: Record<string, string> = {
steam: 'steam locomotive',
diesel: 'diesel locomotive',
electric: 'electric locomotive',
'high-speed': 'high-speed train',
'multiple unit': 'multiple unit',
metro: 'metro',
tram: 'tram / light rail',
carriage: 'carriage / wagon',
other: 'other'
};
export const categoryEmoji: Record<string, string> = {
steam: '🚂',
diesel: '🛤️',
electric: '⚡',
'high-speed': '🚄',
'multiple unit': '🚆',
metro: '🚇',
tram: '🚋',
carriage: '🚃',
other: '🚉'
};
/** One beginner-friendly line per category. */
export const categoryHint: Record<string, string> = {
steam: 'Burns coal, wood or oil to boil water; the steam drives pistons that turn the wheels.',
diesel:
'A diesel engine on board drives the wheels, usually through a generator (diesel-electric).',
electric: 'Draws power from overhead wires or a third rail and pulls separate carriages.',
'high-speed': 'Built for 200–350 km/h on dedicated lines, like the TGV, ICE or Shinkansen.',
'multiple unit':
'A self-propelled train: motors are spread under the passenger cars, so no separate locomotive.',
metro: 'Urban rapid transit: frequent, high-capacity trains, often underground.',
tram: 'Street-running or light-rail vehicles that share or skirt the road.',
carriage: 'Unpowered passenger coaches or freight wagons, pulled by a locomotive.',
other: 'Railcars, inspection vehicles and other rolling stock.'
};
export const tractionLabel: Record<string, string> = {
steam: 'steam',
diesel: 'diesel',
electric: 'electric',
'bi-mode': 'bi-mode',
battery: 'battery',
hydrogen: 'hydrogen',
'gas turbine': 'gas turbine',
none: 'unpowered'
};
export const tractionHint: Record<string, string> = {
steam: 'Steam engine: fire, boiler and pistons.',
diesel: 'Diesel engine on board, mechanical, hydraulic or electric transmission.',
electric: 'Electric motors fed from overhead wires or a third rail.',
'bi-mode': 'Runs electric under wires and on its own diesel engine elsewhere.',
battery: 'Electric motors fed from on-board batteries (charged under wires or at stations).',
hydrogen: 'Hydrogen fuel cells make electricity on board.',
'gas turbine': 'A gas turbine (jet-style engine) generates the power.',
none: 'No motor of its own: hauled by a locomotive.'
};
const FLAGS: Record<string, string> = {
'United States': '🇺🇸',
'United Kingdom': '🇬🇧',
France: '🇫🇷',
Germany: '🇩🇪',
'East Germany': '🇩🇪',
'Soviet Union': '🇷🇺',
Russia: '🇷🇺',
Italy: '🇮🇹',
Japan: '🇯🇵',
Canada: '🇨🇦',
Brazil: '🇧🇷',
China: '🇨🇳',
Netherlands: '🇳🇱',
Belgium: '🇧🇪',
Luxembourg: '🇱🇺',
Sweden: '🇸🇪',
Spain: '🇪🇸',
Portugal: '🇵🇹',
Czechoslovakia: '🇨🇿',
'Czech Republic': '🇨🇿',
Slovakia: '🇸🇰',
Poland: '🇵🇱',
Switzerland: '🇨🇭',
Austria: '🇦🇹',
'Austria-Hungary': '🇦🇹',
Australia: '🇦🇺',
Ukraine: '🇺🇦',
India: '🇮🇳',
Pakistan: '🇵🇰',
Bangladesh: '🇧🇩',
'Sri Lanka': '🇱🇰',
Israel: '🇮🇱',
Romania: '🇷🇴',
Bulgaria: '🇧🇬',
Greece: '🇬🇷',
Indonesia: '🇮🇩',
Malaysia: '🇲🇾',
Singapore: '🇸🇬',
Thailand: '🇹🇭',
Vietnam: '🇻🇳',
Philippines: '🇵🇭',
Argentina: '🇦🇷',
Chile: '🇨🇱',
Mexico: '🇲🇽',
'South Korea': '🇰🇷',
'North Korea': '🇰🇵',
Turkey: '🇹🇷',
'New Zealand': '🇳🇿',
Finland: '🇫🇮',
Norway: '🇳🇴',
Denmark: '🇩🇰',
Ireland: '🇮🇪',
Slovenia: '🇸🇮',
Croatia: '🇭🇷',
Serbia: '🇷🇸',
Yugoslavia: '🇷🇸',
Hungary: '🇭🇺',
'South Africa': '🇿🇦',
Egypt: '🇪🇬',
Kenya: '🇰🇪',
Nigeria: '🇳🇬',
Iran: '🇮🇷',
'Saudi Arabia': '🇸🇦',
Taiwan: '🇹🇼',
'Hong Kong': '🇭🇰',
Kazakhstan: '🇰🇿',
Belarus: '🇧🇾',
Latvia: '🇱🇻',
Lithuania: '🇱🇹',
Estonia: '🇪🇪',
Morocco: '🇲🇦',
Algeria: '🇩🇿',
Tunisia: '🇹🇳',
Peru: '🇵🇪',
Colombia: '🇨🇴',
Uruguay: '🇺🇾',
Cuba: '🇨🇺'
};
export const flag = (country: string | null) => (country ? (FLAGS[country] ?? '') : '');
export const countrySlug = (c: string) =>
c
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
export const fmtKw = (kw: number) =>
kw >= 1000
? `${Math.round(kw / 100) / 10} MW`
: `${new Intl.NumberFormat('en-GB').format(kw)} kW`;
export const fmtM = (m: number) => `${m >= 100 ? Math.round(m) : Math.round(m * 10) / 10} m`;
export const fmtT = (t: number) => `${t >= 100 ? Math.round(t) : Math.round(t * 10) / 10} t`;
export type TrainCard = {
dex: number;
slug: string;
name: string;
fullName: string;
makerSlug: string;
makerName: string;
makerLogo: string | null;
category: string;
traction: string | null;
country: string | null;
firstBuilt: number | null;
inService: boolean | null;
status: string | null;
numberBuilt: number | null;
maxSpeedKmh: number | null;
powerKw: number | null;
photo: string | null;
};
export type MakerTileData = {
slug: string;
name: string;
types: number;
built?: number;
logo: string | null;
href?: string;
};
export const statusLabel: Record<string, string> = {
'in service': 'in service',
preserved: 'preserved',
retired: 'retired'
};
export const LOGO_NOTE =
'Logos come from Wikimedia Commons. Most are public domain as simple text or shapes, but they remain trademarks of their owners and are shown only to identify the maker or operator.';