Files
Alem Tuzlak 79ce60c580 chore: migrate from eslint+prettier to oxlint+oxfmt
Replace eslint and prettier with oxlint and oxfmt for faster linting
and formatting across the monorepo. Remove all eslint and prettier
configs, dependencies, and related packages. Add .oxlintrc.json and
.oxfmtrc.json for the new tooling. Update CI workflows and lefthook
hooks accordingly. Reformat codebase with oxfmt.

https://claude.ai/code/session_01GMkSf29p78HuMR1mbXn8He
2026-04-02 16:39:05 +02:00

1787 lines
53 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Airline Booking</title>
<link
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<style>
/* =============================================================================
DESIGN SYSTEM - CopilotKit Palette + Glassmorphism
============================================================================= */
:root {
/* Brand Colors */
--color-lilac: #bec2ff;
--color-lilac-light: #d4d7ff;
--color-lilac-dark: #9599cc;
--color-mint: #85e0ce;
--color-mint-light: #a8e9dc;
--color-mint-dark: #1b936f;
/* Surfaces */
--color-surface: #dedee9;
--color-surface-light: #f7f7f9;
--color-container: #ffffff;
/* Text */
--color-text-primary: #010507;
--color-text-secondary: #57575b;
--color-text-tertiary: #8e8e93;
/* Borders */
--color-border: #dbdbe5;
--color-border-light: #ebebf0;
--color-border-glass: rgba(255, 255, 255, 0.3);
/* Glassmorphism */
--color-glass: rgba(255, 255, 255, 0.7);
--color-glass-subtle: rgba(255, 255, 255, 0.5);
--color-glass-dark: rgba(255, 255, 255, 0.85);
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md:
0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
--shadow-lg:
0 10px 25px -3px rgba(0, 0, 0, 0.08),
0 4px 6px -2px rgba(0, 0, 0, 0.03);
--shadow-glass: 0 4px 30px rgba(0, 0, 0, 0.1);
/* Status Colors */
--color-success: #10b981;
--color-error: #ef4444;
--color-warning: #f59e0b;
/* Spacing */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
/* Radii */
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-full: 9999px;
/* Typography */
--font-family:
"Plus Jakarta Sans", system-ui, -apple-system, sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
background: linear-gradient(
135deg,
var(--color-surface-light) 0%,
var(--color-surface) 100%
);
color: var(--color-text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* Abstract background shapes */
body::before {
content: "";
position: fixed;
top: -100px;
right: -100px;
width: 300px;
height: 300px;
background: var(--color-lilac);
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
z-index: 0;
animation: float1 20s ease-in-out infinite;
}
body::after {
content: "";
position: fixed;
bottom: -100px;
left: -100px;
width: 250px;
height: 250px;
background: var(--color-mint);
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
z-index: 0;
animation: float2 25s ease-in-out infinite;
}
@keyframes float1 {
0%,
100% {
transform: translate(0, 0);
}
50% {
transform: translate(-30px, 30px);
}
}
@keyframes float2 {
0%,
100% {
transform: translate(0, 0);
}
50% {
transform: translate(30px, -30px);
}
}
#app {
position: relative;
z-index: 1;
padding: var(--space-4);
max-width: 600px;
margin: 0 auto;
}
/* =============================================================================
GLASS COMPONENTS
============================================================================= */
.glass {
background: var(--color-glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-glass);
}
.glass-subtle {
background: var(--color-glass-subtle);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--color-border-glass);
border-radius: var(--radius-lg);
}
/* =============================================================================
WIZARD STEPS INDICATOR
============================================================================= */
.wizard-steps {
display: flex;
justify-content: center;
gap: var(--space-2);
margin-bottom: var(--space-6);
padding: var(--space-3);
}
.step-indicator {
display: flex;
align-items: center;
gap: var(--space-2);
}
.step-dot {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
background: var(--color-glass-subtle);
border: 2px solid var(--color-border);
color: var(--color-text-tertiary);
}
.step-dot.active {
background: linear-gradient(
135deg,
var(--color-lilac),
var(--color-mint)
);
border-color: transparent;
color: white;
box-shadow: 0 4px 12px rgba(190, 194, 255, 0.4);
}
.step-dot.completed {
background: var(--color-mint);
border-color: transparent;
color: white;
}
.step-line {
width: 24px;
height: 2px;
background: var(--color-border);
transition: background 0.3s ease;
}
.step-line.active {
background: linear-gradient(
90deg,
var(--color-lilac),
var(--color-mint)
);
}
/* =============================================================================
FORM ELEMENTS
============================================================================= */
.form-group {
margin-bottom: var(--space-4);
}
.form-label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: var(--space-2);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.form-input,
.form-select {
width: 100%;
padding: var(--space-3) var(--space-4);
font-family: var(--font-family);
font-size: 15px;
color: var(--color-text-primary);
background: var(--color-glass-dark);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
transition: all 0.2s ease;
outline: none;
}
.form-input:focus,
.form-select:focus {
border-color: var(--color-lilac);
box-shadow: 0 0 0 3px rgba(190, 194, 255, 0.2);
}
.form-input::placeholder {
color: var(--color-text-tertiary);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
}
/* Counter input */
.counter-input {
display: flex;
align-items: center;
gap: var(--space-3);
}
.counter-btn {
width: 36px;
height: 36px;
border-radius: var(--radius-full);
border: 1px solid var(--color-border);
background: var(--color-glass);
color: var(--color-text-primary);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.counter-btn:hover:not(:disabled) {
background: var(--color-lilac-light);
border-color: var(--color-lilac);
}
.counter-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.counter-value {
font-size: 18px;
font-weight: 600;
min-width: 40px;
text-align: center;
}
/* =============================================================================
BUTTONS
============================================================================= */
button {
cursor: pointer;
border: none;
font-family: var(--font-family);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-5);
font-size: 15px;
font-weight: 600;
border-radius: var(--radius-lg);
transition: all 0.2s ease;
}
.btn:focus-visible {
outline: 2px solid var(--color-lilac);
outline-offset: 2px;
}
.btn:active:not(:disabled) {
transform: scale(0.98);
}
.btn-primary {
background: linear-gradient(
135deg,
var(--color-lilac),
var(--color-mint)
);
color: var(--color-text-primary);
box-shadow: 0 4px 12px rgba(190, 194, 255, 0.3);
}
.btn-primary:hover:not(:disabled) {
box-shadow: 0 6px 20px rgba(190, 194, 255, 0.4);
transform: translateY(-1px);
}
.btn-secondary {
background: var(--color-glass);
border: 1px solid var(--color-border);
color: var(--color-text-primary);
}
.btn-secondary:hover:not(:disabled) {
background: var(--color-glass-dark);
border-color: var(--color-lilac);
}
.btn-block {
width: 100%;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* =============================================================================
STEP CONTAINERS
============================================================================= */
.step-container {
display: none;
}
.step-container.active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.step-title {
font-size: 24px;
font-weight: 700;
margin-bottom: var(--space-2);
background: linear-gradient(
135deg,
var(--color-text-primary),
var(--color-text-secondary)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.step-subtitle {
font-size: 14px;
color: var(--color-text-secondary);
margin-bottom: var(--space-5);
}
/* =============================================================================
FLIGHT CARDS
============================================================================= */
.flights-list {
display: flex;
flex-direction: column;
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.flight-card {
padding: var(--space-4);
cursor: pointer;
transition: all 0.2s ease;
}
.flight-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.flight-card.selected {
border-color: var(--color-lilac);
background: rgba(190, 194, 255, 0.15);
}
.flight-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-3);
}
.airline-info {
display: flex;
align-items: center;
gap: var(--space-3);
}
.airline-logo {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 14px;
}
.airline-name {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
.flight-number {
font-size: 13px;
color: var(--color-text-tertiary);
}
.flight-price {
text-align: right;
}
.price-amount {
font-size: 22px;
font-weight: 700;
color: var(--color-mint-dark);
}
.price-label {
font-size: 12px;
color: var(--color-text-tertiary);
}
.flight-details {
display: flex;
align-items: center;
gap: var(--space-4);
}
.flight-endpoint {
flex: 1;
}
.flight-time {
font-size: 20px;
font-weight: 700;
color: var(--color-text-primary);
}
.flight-city {
font-size: 13px;
color: var(--color-text-secondary);
}
.flight-duration {
text-align: center;
padding: 0 var(--space-4);
}
.duration-line {
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--color-text-tertiary);
}
.duration-line svg {
width: 20px;
height: 20px;
}
.duration-text {
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
}
.stops-text {
font-size: 12px;
color: var(--color-text-tertiary);
}
/* =============================================================================
SEAT MAP
============================================================================= */
.seat-map-container {
margin-bottom: var(--space-5);
}
.seat-map-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-4);
}
.seats-selected {
font-size: 14px;
color: var(--color-text-secondary);
}
.seats-selected strong {
color: var(--color-mint-dark);
}
.seat-legend {
display: flex;
gap: var(--space-4);
margin-bottom: var(--space-4);
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 12px;
color: var(--color-text-secondary);
}
.legend-seat {
width: 20px;
height: 20px;
border-radius: var(--radius-sm);
}
.legend-seat.available {
background: var(--color-glass-dark);
border: 1px solid var(--color-border);
}
.legend-seat.selected {
background: var(--color-lilac);
}
.legend-seat.occupied {
background: var(--color-text-tertiary);
}
.seat-map {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding: var(--space-4);
background: var(--color-glass-subtle);
border-radius: var(--radius-xl);
}
.seat-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.row-number {
width: 24px;
font-size: 12px;
font-weight: 600;
color: var(--color-text-tertiary);
text-align: center;
}
.seat {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: var(--color-glass-dark);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
color: var(--color-text-tertiary);
}
.seat:hover:not(.occupied):not(.selected) {
border-color: var(--color-lilac);
background: rgba(190, 194, 255, 0.2);
}
.seat.selected {
background: var(--color-lilac);
border-color: var(--color-lilac-dark);
color: white;
}
.seat.occupied {
background: var(--color-text-tertiary);
cursor: not-allowed;
color: transparent;
}
.aisle {
width: 24px;
}
/* =============================================================================
PASSENGER FORMS
============================================================================= */
.passengers-list {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.passenger-card {
padding: var(--space-4);
}
.passenger-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.passenger-number {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: linear-gradient(
135deg,
var(--color-lilac),
var(--color-mint)
);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
}
.passenger-label {
font-size: 16px;
font-weight: 600;
}
.passenger-seat {
font-size: 13px;
color: var(--color-text-tertiary);
margin-left: auto;
}
/* =============================================================================
CONFIRMATION
============================================================================= */
.confirmation-card {
padding: var(--space-6);
text-align: center;
}
.confirmation-icon {
width: 64px;
height: 64px;
border-radius: var(--radius-full);
background: linear-gradient(
135deg,
var(--color-mint),
var(--color-mint-dark)
);
margin: 0 auto var(--space-4);
display: flex;
align-items: center;
justify-content: center;
color: white;
animation: scaleIn 0.5s ease;
}
@keyframes scaleIn {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
.confirmation-title {
font-size: 24px;
font-weight: 700;
margin-bottom: var(--space-2);
}
.confirmation-ref {
font-size: 14px;
color: var(--color-text-secondary);
margin-bottom: var(--space-6);
}
.confirmation-ref strong {
color: var(--color-lilac-dark);
font-weight: 700;
font-size: 18px;
}
.booking-summary {
text-align: left;
padding: var(--space-4);
background: var(--color-glass-subtle);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
}
.summary-row {
display: flex;
justify-content: space-between;
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border-light);
}
.summary-row:last-child {
border-bottom: none;
}
.summary-label {
font-size: 14px;
color: var(--color-text-secondary);
}
.summary-value {
font-size: 14px;
font-weight: 600;
}
.summary-total {
font-size: 18px;
font-weight: 700;
color: var(--color-mint-dark);
}
/* =============================================================================
LOADING STATE
============================================================================= */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-8);
gap: var(--space-4);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-border);
border-top-color: var(--color-lilac);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 14px;
color: var(--color-text-secondary);
}
/* =============================================================================
ERROR STATE
============================================================================= */
.error-message {
padding: var(--space-4);
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: var(--radius-lg);
color: var(--color-error);
font-size: 14px;
margin-bottom: var(--space-4);
}
/* =============================================================================
NAVIGATION
============================================================================= */
.nav-buttons {
display: flex;
gap: var(--space-3);
margin-top: var(--space-5);
}
.nav-buttons .btn {
flex: 1;
}
</style>
</head>
<body>
<div id="app">
<!-- Wizard Step Indicator -->
<div class="wizard-steps glass">
<div class="step-indicator">
<div class="step-dot active" id="dot-1">1</div>
<div class="step-line" id="line-1"></div>
<div class="step-dot" id="dot-2">2</div>
<div class="step-line" id="line-2"></div>
<div class="step-dot" id="dot-3">3</div>
<div class="step-line" id="line-3"></div>
<div class="step-dot" id="dot-4">4</div>
<div class="step-line" id="line-4"></div>
<div class="step-dot" id="dot-5">5</div>
</div>
</div>
<!-- Loading State -->
<div id="loading" class="loading-container glass" style="display: none">
<div class="loading-spinner"></div>
<div class="loading-text">Searching for flights...</div>
</div>
<!-- Step 1: Search -->
<div id="step-1" class="step-container active">
<div class="glass" style="padding: var(--space-5)">
<h2 class="step-title">Find Your Flight</h2>
<p class="step-subtitle">
Search for the best flights to your destination
</p>
<div class="form-row">
<div class="form-group">
<label class="form-label">From</label>
<select class="form-select" id="origin">
<option value="">Select origin</option>
</select>
</div>
<div class="form-group">
<label class="form-label">To</label>
<select class="form-select" id="destination">
<option value="">Select destination</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Departure Date</label>
<input type="date" class="form-input" id="departureDate" />
</div>
<div class="form-group">
<label class="form-label">Passengers</label>
<div class="counter-input">
<button class="counter-btn" id="decPassengers" disabled>
−
</button>
<span class="counter-value" id="passengerCount">1</span>
<button class="counter-btn" id="incPassengers">+</button>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">Cabin Class</label>
<select class="form-select" id="cabinClass">
<option value="economy">Economy</option>
<option value="business">Business</option>
<option value="first">First Class</option>
</select>
</div>
<button class="btn btn-primary btn-block" id="searchBtn">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
Search Flights
</button>
</div>
</div>
<!-- Step 2: Flight Results -->
<div id="step-2" class="step-container">
<h2 class="step-title">Select Your Flight</h2>
<p class="step-subtitle" id="resultsSubtitle">5 flights found</p>
<div class="flights-list" id="flightsList"></div>
<div class="nav-buttons">
<button class="btn btn-secondary" id="backToSearch">Back</button>
<button class="btn btn-primary" id="selectFlightBtn" disabled>
Continue
</button>
</div>
</div>
<!-- Step 3: Seat Selection -->
<div id="step-3" class="step-container">
<h2 class="step-title">Choose Your Seats</h2>
<p class="step-subtitle" id="seatSubtitle">
Select seats for 2 passengers
</p>
<div class="seat-map-container glass" style="padding: var(--space-4)">
<div class="seat-map-header">
<div class="seats-selected">
Selected: <strong id="selectedSeatsText">0 of 2</strong>
</div>
</div>
<div class="seat-legend">
<div class="legend-item">
<div class="legend-seat available"></div>
<span>Available</span>
</div>
<div class="legend-item">
<div class="legend-seat selected"></div>
<span>Selected</span>
</div>
<div class="legend-item">
<div class="legend-seat occupied"></div>
<span>Occupied</span>
</div>
</div>
<div class="seat-map" id="seatMap"></div>
</div>
<div class="nav-buttons">
<button class="btn btn-secondary" id="backToFlights">Back</button>
<button class="btn btn-primary" id="confirmSeatsBtn" disabled>
Continue
</button>
</div>
</div>
<!-- Step 4: Passenger Details -->
<div id="step-4" class="step-container">
<h2 class="step-title">Passenger Details</h2>
<p class="step-subtitle">Enter information for all passengers</p>
<div class="passengers-list" id="passengersList"></div>
<div class="nav-buttons">
<button class="btn btn-secondary" id="backToSeats">Back</button>
<button class="btn btn-primary" id="confirmBookingBtn">
Complete Booking
</button>
</div>
</div>
<!-- Step 5: Confirmation -->
<div id="step-5" class="step-container">
<div class="confirmation-card glass">
<div class="confirmation-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<h2 class="confirmation-title">Booking Confirmed!</h2>
<p class="confirmation-ref">
Confirmation: <strong id="confirmationCode">ABC123</strong>
</p>
<div class="booking-summary" id="bookingSummary"></div>
<button class="btn btn-primary btn-block" id="addToCalendarBtn">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
<line x1="16" x2="16" y1="2" y2="6" />
<line x1="8" x2="8" y1="2" y2="6" />
<line x1="3" x2="21" y1="10" y2="10" />
</svg>
Add to Calendar
</button>
</div>
</div>
</div>
<script>
// =============================================================================
// MCP APP COMMUNICATION MODULE
// =============================================================================
const mcpApp = (() => {
let requestId = 1;
const pendingRequests = new Map();
const notificationHandlers = new Map();
// Handle incoming messages
window.addEventListener("message", (event) => {
const msg = event.data;
if (!msg || typeof msg !== "object") return;
// Handle responses to our requests
if (msg.id !== undefined && pendingRequests.has(msg.id)) {
const { resolve, reject } = pendingRequests.get(msg.id);
pendingRequests.delete(msg.id);
if (msg.error) {
reject(new Error(msg.error.message || "Unknown error"));
} else {
resolve(msg.result);
}
return;
}
// Handle notifications from host
if (msg.method && !msg.id) {
const handlers = notificationHandlers.get(msg.method) || [];
handlers.forEach((h) => h(msg.params));
}
});
return {
sendRequest(method, params) {
const id = requestId++;
return new Promise((resolve, reject) => {
pendingRequests.set(id, { resolve, reject });
window.parent.postMessage(
{ jsonrpc: "2.0", id, method, params },
"*",
);
});
},
sendNotification(method, params) {
window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
},
onNotification(method, handler) {
if (!notificationHandlers.has(method)) {
notificationHandlers.set(method, []);
}
notificationHandlers.get(method).push(handler);
},
};
})();
// =============================================================================
// AIRPORT DATA
// =============================================================================
const AIRPORTS = [
{ code: "JFK", city: "New York", name: "John F. Kennedy" },
{ code: "LAX", city: "Los Angeles", name: "Los Angeles Intl" },
{ code: "LHR", city: "London", name: "Heathrow" },
{ code: "CDG", city: "Paris", name: "Charles de Gaulle" },
{ code: "NRT", city: "Tokyo", name: "Narita" },
{ code: "DXB", city: "Dubai", name: "Dubai Intl" },
{ code: "SIN", city: "Singapore", name: "Changi" },
{ code: "SFO", city: "San Francisco", name: "San Francisco Intl" },
{ code: "ORD", city: "Chicago", name: "O'Hare" },
{ code: "MIA", city: "Miami", name: "Miami Intl" },
{ code: "SEA", city: "Seattle", name: "Seattle-Tacoma" },
{ code: "BOS", city: "Boston", name: "Logan" },
{ code: "FRA", city: "Frankfurt", name: "Frankfurt" },
{ code: "HKG", city: "Hong Kong", name: "Hong Kong Intl" },
{ code: "SYD", city: "Sydney", name: "Sydney Kingsford Smith" },
];
// =============================================================================
// STATE
// =============================================================================
let state = {
currentStep: 1,
passengers: 1,
searchId: null,
flights: [],
selectedFlightId: null,
selectedFlight: null,
seatMap: [],
selectedSeats: [],
passengerDetails: [],
booking: null,
};
// =============================================================================
// DOM ELEMENTS
// =============================================================================
const $ = (id) => document.getElementById(id);
// =============================================================================
// WIZARD NAVIGATION
// =============================================================================
function goToStep(step) {
state.currentStep = step;
// Update step containers
document.querySelectorAll(".step-container").forEach((el, i) => {
el.classList.toggle("active", i + 1 === step);
});
// Update step indicators
for (let i = 1; i <= 5; i++) {
const dot = $(`dot-${i}`);
const line = i < 5 ? $(`line-${i}`) : null;
dot.classList.remove("active", "completed");
if (i === step) {
dot.classList.add("active");
} else if (i < step) {
dot.classList.add("completed");
dot.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
} else {
dot.textContent = i;
}
if (line) {
line.classList.toggle("active", i < step);
}
}
reportSize();
}
function showLoading(text = "Loading...") {
$("loading").style.display = "flex";
$("loading").querySelector(".loading-text").textContent = text;
document
.querySelectorAll(".step-container")
.forEach((el) => el.classList.remove("active"));
}
function hideLoading() {
$("loading").style.display = "none";
}
// =============================================================================
// STEP 1: SEARCH
// =============================================================================
function initSearchForm() {
const originSelect = $("origin");
const destSelect = $("destination");
// Populate airport dropdowns
AIRPORTS.forEach((airport) => {
const optionText = `${airport.city} (${airport.code})`;
originSelect.add(new Option(optionText, airport.code));
destSelect.add(new Option(optionText, airport.code));
});
// Set default date to tomorrow
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
$("departureDate").value = tomorrow.toISOString().split("T")[0];
$("departureDate").min = new Date().toISOString().split("T")[0];
// Passenger counter
$("decPassengers").addEventListener("click", () => {
if (state.passengers > 1) {
state.passengers--;
updatePassengerCount();
}
});
$("incPassengers").addEventListener("click", () => {
if (state.passengers < 9) {
state.passengers++;
updatePassengerCount();
}
});
// Search button
$("searchBtn").addEventListener("click", searchFlights);
}
function updatePassengerCount() {
$("passengerCount").textContent = state.passengers;
$("decPassengers").disabled = state.passengers <= 1;
$("incPassengers").disabled = state.passengers >= 9;
}
async function searchFlights() {
const origin = $("origin").value;
const destination = $("destination").value;
const date = $("departureDate").value;
const cabinClass = $("cabinClass").value;
if (!origin || !destination || !date) {
alert("Please fill in all fields");
return;
}
if (origin === destination) {
alert("Origin and destination must be different");
return;
}
showLoading("Searching for flights...");
try {
const result = await mcpApp.sendRequest("tools/call", {
name: "search-flights",
arguments: {
origin,
destination,
departureDate: date,
passengers: state.passengers,
cabinClass,
},
});
// Use structuredContent directly (not JSON.parse on text)
const data = result.structuredContent;
if (data?.search) {
state.searchId = data.search.id;
state.flights = data.search.flights || [];
renderFlightResults();
hideLoading();
goToStep(2);
} else {
throw new Error("Invalid response from server");
}
} catch (error) {
console.error("Search failed:", error);
hideLoading();
goToStep(1);
alert("Failed to search flights. Please try again.");
}
}
// =============================================================================
// STEP 2: FLIGHT RESULTS
// =============================================================================
function renderFlightResults() {
const list = $("flightsList");
list.innerHTML = "";
$("resultsSubtitle").textContent =
`${state.flights.length} flights found`;
state.flights.forEach((flight) => {
const card = document.createElement("div");
card.className = "flight-card glass";
card.dataset.flightId = flight.id;
card.innerHTML = `
<div class="flight-header">
<div class="airline-info">
<div class="airline-logo" style="background: ${flight.airline.color}">${flight.airline.code}</div>
<div>
<div class="airline-name">${flight.airline.name}</div>
<div class="flight-number">${flight.flightNumber}</div>
</div>
</div>
<div class="flight-price">
<div class="price-amount">$${flight.price}</div>
<div class="price-label">per person</div>
</div>
</div>
<div class="flight-details">
<div class="flight-endpoint">
<div class="flight-time">${flight.departureTime}</div>
<div class="flight-city">${flight.origin.code}</div>
</div>
<div class="flight-duration">
<div class="duration-line">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"/></svg>
</div>
<div class="duration-text">${flight.duration}</div>
<div class="stops-text">${flight.stops === 0 ? "Nonstop" : flight.stops + " stop" + (flight.stops > 1 ? "s" : "")}</div>
</div>
<div class="flight-endpoint" style="text-align: right;">
<div class="flight-time">${flight.arrivalTime}</div>
<div class="flight-city">${flight.destination.code}</div>
</div>
</div>
`;
card.addEventListener("click", () => selectFlight(flight.id));
list.appendChild(card);
});
reportSize();
}
function selectFlight(flightId) {
state.selectedFlightId = flightId;
state.selectedFlight = state.flights.find((f) => f.id === flightId);
document.querySelectorAll(".flight-card").forEach((card) => {
card.classList.toggle("selected", card.dataset.flightId === flightId);
});
$("selectFlightBtn").disabled = false;
}
// =============================================================================
// STEP 3: SEAT SELECTION
// =============================================================================
async function loadSeatMap() {
showLoading("Loading seat map...");
try {
const result = await mcpApp.sendRequest("tools/call", {
name: "select-flight",
arguments: {
searchId: state.searchId,
flightId: state.selectedFlightId,
},
});
// Use structuredContent directly
const data = result.structuredContent;
if (data?.success && data?.seatMap) {
state.seatMap = data.seatMap;
state.selectedSeats = [];
renderSeatMap();
hideLoading();
goToStep(3);
} else {
throw new Error("Failed to load seat map");
}
} catch (error) {
console.error("Failed to load seat map:", error);
hideLoading();
goToStep(2);
alert("Failed to load seat map. Please try again.");
}
}
function renderSeatMap() {
const container = $("seatMap");
container.innerHTML = "";
$("seatSubtitle").textContent =
`Select seats for ${state.passengers} passenger${state.passengers > 1 ? "s" : ""}`;
updateSelectedSeatsText();
// Column headers
const headerRow = document.createElement("div");
headerRow.className = "seat-row";
headerRow.innerHTML = `
<div class="row-number"></div>
<div class="seat" style="background:none;border:none;color:var(--color-text-secondary);font-weight:600;">A</div>
<div class="seat" style="background:none;border:none;color:var(--color-text-secondary);font-weight:600;">B</div>
<div class="seat" style="background:none;border:none;color:var(--color-text-secondary);font-weight:600;">C</div>
<div class="aisle"></div>
<div class="seat" style="background:none;border:none;color:var(--color-text-secondary);font-weight:600;">D</div>
<div class="seat" style="background:none;border:none;color:var(--color-text-secondary);font-weight:600;">E</div>
<div class="seat" style="background:none;border:none;color:var(--color-text-secondary);font-weight:600;">F</div>
`;
container.appendChild(headerRow);
// Seat rows - server returns Seat[][] where each Seat has {id, row, position, status}
state.seatMap.forEach((rowSeats) => {
const rowEl = document.createElement("div");
rowEl.className = "seat-row";
// Get row number from first seat in the row
const rowNum = rowSeats[0]?.row || 1;
let html = `<div class="row-number">${rowNum}</div>`;
rowSeats.forEach((seat, i) => {
if (i === 3) html += '<div class="aisle"></div>';
const seatId = seat.id; // Already formatted as "12A"
const isSelected = state.selectedSeats.includes(seatId);
const classes = ["seat"];
if (seat.status === "occupied") classes.push("occupied");
if (isSelected) classes.push("selected");
html += `<div class="${classes.join(" ")}" data-seat="${seatId}">${seat.position}</div>`;
});
rowEl.innerHTML = html;
rowEl.querySelectorAll(".seat:not(.occupied)").forEach((seatEl) => {
seatEl.addEventListener("click", () =>
toggleSeat(seatEl.dataset.seat),
);
});
container.appendChild(rowEl);
});
reportSize();
}
function toggleSeat(seatId) {
const index = state.selectedSeats.indexOf(seatId);
if (index >= 0) {
state.selectedSeats.splice(index, 1);
} else if (state.selectedSeats.length < state.passengers) {
state.selectedSeats.push(seatId);
}
// Update UI
document.querySelectorAll(".seat").forEach((el) => {
if (el.dataset.seat) {
el.classList.toggle(
"selected",
state.selectedSeats.includes(el.dataset.seat),
);
}
});
updateSelectedSeatsText();
$("confirmSeatsBtn").disabled =
state.selectedSeats.length !== state.passengers;
}
function updateSelectedSeatsText() {
$("selectedSeatsText").textContent =
`${state.selectedSeats.length} of ${state.passengers}`;
}
async function confirmSeats() {
showLoading("Confirming seats...");
try {
const result = await mcpApp.sendRequest("tools/call", {
name: "select-seats",
arguments: {
searchId: state.searchId,
flightId: state.selectedFlightId,
seats: state.selectedSeats,
},
});
const content = result.content?.[0];
if (content?.type === "text") {
renderPassengerForms();
hideLoading();
goToStep(4);
}
} catch (error) {
console.error("Failed to confirm seats:", error);
hideLoading();
goToStep(3);
alert("Failed to confirm seats. Please try again.");
}
}
// =============================================================================
// STEP 4: PASSENGER DETAILS
// =============================================================================
function renderPassengerForms() {
const container = $("passengersList");
container.innerHTML = "";
for (let i = 0; i < state.passengers; i++) {
const card = document.createElement("div");
card.className = "passenger-card glass";
card.innerHTML = `
<div class="passenger-header">
<div class="passenger-number">${i + 1}</div>
<div class="passenger-label">Passenger ${i + 1}</div>
<div class="passenger-seat">Seat ${state.selectedSeats[i]}</div>
</div>
<div class="form-group">
<label class="form-label">Full Name</label>
<input type="text" class="form-input" id="name-${i}" placeholder="As shown on ID" required>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" id="email-${i}" placeholder="email@example.com" required>
</div>
<div class="form-group">
<label class="form-label">Phone</label>
<input type="tel" class="form-input" id="phone-${i}" placeholder="+1 (555) 000-0000">
</div>
</div>
`;
container.appendChild(card);
}
reportSize();
}
async function completeBooking() {
// Collect passenger details
const passengers = [];
for (let i = 0; i < state.passengers; i++) {
const name = $(`name-${i}`).value.trim();
const email = $(`email-${i}`).value.trim();
const phone = $(`phone-${i}`).value.trim();
if (!name || !email) {
alert(`Please fill in required fields for Passenger ${i + 1}`);
return;
}
passengers.push({ name, email, phone, seat: state.selectedSeats[i] });
}
showLoading("Completing your booking...");
try {
const result = await mcpApp.sendRequest("tools/call", {
name: "book-flight",
arguments: {
searchId: state.searchId,
flightId: state.selectedFlightId,
passengers,
},
});
// Use structuredContent directly
const data = result.structuredContent;
if (data?.success && data?.booking) {
state.booking = data.booking;
renderConfirmation();
hideLoading();
goToStep(5);
} else {
throw new Error(data?.error || "Booking failed");
}
} catch (error) {
console.error("Booking failed:", error);
hideLoading();
goToStep(4);
alert("Failed to complete booking. Please try again.");
}
}
// =============================================================================
// STEP 5: CONFIRMATION
// =============================================================================
function renderConfirmation() {
const booking = state.booking;
const flight = state.selectedFlight;
// Server uses 'confirmationNumber' not 'confirmationCode'
$("confirmationCode").textContent = booking.confirmationNumber;
const summary = $("bookingSummary");
summary.innerHTML = `
<div class="summary-row">
<span class="summary-label">Flight</span>
<span class="summary-value">${flight.airline.code} ${flight.flightNumber}</span>
</div>
<div class="summary-row">
<span class="summary-label">Route</span>
<span class="summary-value">${flight.origin.code} → ${flight.destination.code}</span>
</div>
<div class="summary-row">
<span class="summary-label">Date</span>
<span class="summary-value">${new Date($("departureDate").value).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" })}</span>
</div>
<div class="summary-row">
<span class="summary-label">Departure</span>
<span class="summary-value">${flight.departureTime}</span>
</div>
<div class="summary-row">
<span class="summary-label">Passengers</span>
<span class="summary-value">${state.passengers}</span>
</div>
<div class="summary-row">
<span class="summary-label">Seats</span>
<span class="summary-value">${state.selectedSeats.join(", ")}</span>
</div>
<div class="summary-row">
<span class="summary-label">Total</span>
<span class="summary-value summary-total">$${booking.totalPrice}</span>
</div>
`;
reportSize();
}
// =============================================================================
// INITIALIZATION
// =============================================================================
function reportSize() {
requestAnimationFrame(() => {
const rect = document.body.getBoundingClientRect();
mcpApp.sendNotification("ui/notifications/size-change", {
width: Math.ceil(rect.width),
height: Math.ceil(rect.height),
});
});
}
async function initialize() {
// Initialize UI
initSearchForm();
// Navigation buttons
$("backToSearch").addEventListener("click", () => goToStep(1));
$("selectFlightBtn").addEventListener("click", loadSeatMap);
$("backToFlights").addEventListener("click", () => goToStep(2));
$("confirmSeatsBtn").addEventListener("click", confirmSeats);
$("backToSeats").addEventListener("click", () => goToStep(3));
$("confirmBookingBtn").addEventListener("click", completeBooking);
$("addToCalendarBtn").addEventListener("click", () => {
mcpApp.sendRequest("ui/message", {
role: "user",
content: [{ type: "text", text: "Add this flight to my calendar" }],
});
});
// Initialize MCP connection
try {
await mcpApp.sendRequest("ui/initialize", {
protocolVersion: "2025-06-18",
appInfo: { name: "Airline Booking", version: "1.0.0" },
appCapabilities: {},
});
mcpApp.sendNotification("ui/notifications/initialized", {});
} catch (error) {
console.error("Failed to initialize:", error);
}
// Listen for tool input to prefill search form
mcpApp.onNotification("ui/notifications/tool-input", (params) => {
const args = params?.arguments;
if (args) {
// Prefill origin
if (args.origin) {
$("origin").value = args.origin;
}
// Prefill destination
if (args.destination) {
$("destination").value = args.destination;
}
// Prefill date
if (args.departureDate) {
$("departureDate").value = args.departureDate;
}
// Prefill passengers
if (args.passengers) {
state.passengers = args.passengers;
updatePassengerCount();
}
// Prefill cabin class
if (args.cabinClass) {
$("cabinClass").value = args.cabinClass;
}
}
});
// Listen for tool results (if pre-populated search)
mcpApp.onNotification("ui/notifications/tool-result", (params) => {
const content = params?.structuredContent;
// Server wraps data in 'search' object: { search: { id, flights, searchParams }, summary }
if (content?.search?.id && content?.search?.flights) {
state.searchId = content.search.id;
state.flights = content.search.flights;
state.passengers = content.search.searchParams?.passengers || 1;
updatePassengerCount();
renderFlightResults();
goToStep(2);
}
});
// Size observer
new ResizeObserver(reportSize).observe(document.body);
reportSize();
}
window.addEventListener("load", initialize);
</script>
</body>
</html>