224 lines
8.5 KiB
TypeScript
224 lines
8.5 KiB
TypeScript
import React from 'react';
|
|
import { Trip } from '../types';
|
|
import { sound } from '../utils/audio';
|
|
import {
|
|
Plane,
|
|
Luggage,
|
|
Volume2,
|
|
VolumeX,
|
|
BrainCircuit,
|
|
Settings as SettingsIcon,
|
|
Plus,
|
|
RotateCcw,
|
|
Sparkles
|
|
} from 'lucide-react';
|
|
|
|
interface BoardingHeaderProps {
|
|
activeTrip: Trip | null;
|
|
trips: Trip[];
|
|
onSelectTrip: (tripId: string) => void;
|
|
onNewTrip: () => void;
|
|
onOpenDebrief: () => void;
|
|
onOpenMemory: () => void;
|
|
onOpenSettings: () => void;
|
|
soundEnabled: boolean;
|
|
onToggleSound: () => void;
|
|
}
|
|
|
|
export const BoardingHeader: React.FC<BoardingHeaderProps> = ({
|
|
activeTrip,
|
|
trips,
|
|
onSelectTrip,
|
|
onNewTrip,
|
|
onOpenDebrief,
|
|
onOpenMemory,
|
|
onOpenSettings,
|
|
soundEnabled,
|
|
onToggleSound,
|
|
}) => {
|
|
const totalItems = activeTrip ? activeTrip.items.length : 0;
|
|
const packedItems = activeTrip ? activeTrip.items.filter((i) => i.packed).length : 0;
|
|
const percentPacked = totalItems > 0 ? Math.round((packedItems / totalItems) * 100) : 0;
|
|
|
|
return (
|
|
<header className="border-b-2 border-ink bg-ticket-100 shadow-card-hard">
|
|
{/* Top Telemetry Flight Ticker */}
|
|
<div className="bg-ink text-ticket-100 px-3 py-1.5 flex items-center justify-between text-xs font-mono tracking-wider border-b border-ink-light">
|
|
<div className="flex items-center gap-2">
|
|
<span className="inline-block w-2 h-2 rounded-full bg-canary animate-pulse" />
|
|
<span className="font-bold uppercase tracking-widest text-canary">LOADOUT // MANIFEST</span>
|
|
<span className="hidden sm:inline text-ink-faint">|</span>
|
|
<span className="hidden sm:inline text-ticket-200">
|
|
{activeTrip ? `REF: ${activeTrip.routeCode}` : 'DISPATCH: READY'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => {
|
|
onToggleSound();
|
|
sound.playClick();
|
|
}}
|
|
title={soundEnabled ? 'Mute sound effects' : 'Enable tactile sounds'}
|
|
className="flex items-center gap-1 hover:text-canary transition-colors"
|
|
>
|
|
{soundEnabled ? <Volume2 size={13} /> : <VolumeX size={13} />}
|
|
<span className="hidden md:inline">{soundEnabled ? 'AUDIO: ON' : 'AUDIO: OFF'}</span>
|
|
</button>
|
|
<span className="text-ink-faint">|</span>
|
|
<button
|
|
onClick={() => {
|
|
sound.playClick();
|
|
onOpenMemory();
|
|
}}
|
|
className="flex items-center gap-1 text-canary hover:underline"
|
|
title="View accumulated packing wisdom & debrief history"
|
|
>
|
|
<BrainCircuit size={13} />
|
|
<span className="font-bold">FLIGHT LOG & MEMORY</span>
|
|
</button>
|
|
<span className="text-ink-faint">|</span>
|
|
<button
|
|
onClick={() => {
|
|
sound.playClick();
|
|
onOpenSettings();
|
|
}}
|
|
className="hover:text-canary transition-colors"
|
|
title="App settings & AI dispatch"
|
|
>
|
|
<SettingsIcon size={13} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Boarding Pass Header Card */}
|
|
<div className="px-3 sm:px-6 py-4 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
{/* Left: Voyage & Route Code */}
|
|
<div className="flex items-start sm:items-center gap-3">
|
|
<div className="w-12 h-12 bg-canary border-2 border-ink flex items-center justify-center shadow-card-hard-sm shrink-0">
|
|
<Plane className="text-ink transform -rotate-45" size={26} strokeWidth={2.5} />
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<h1 className="font-display text-3xl sm:text-4xl tracking-wide uppercase leading-none text-ink">
|
|
{activeTrip ? activeTrip.title : 'NO VOYAGE SELECTED'}
|
|
</h1>
|
|
{activeTrip?.debrief && (
|
|
<span className="stamp-badge stamp-badge-packed text-[11px] py-0.5">
|
|
DEBRIEFED
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 mt-1 font-mono text-xs text-ink-muted flex-wrap">
|
|
{activeTrip ? (
|
|
<>
|
|
<span className="bg-ticket-200 border border-ticket-border px-1.5 py-0.5 font-bold text-ink">
|
|
{activeTrip.routeCode}
|
|
</span>
|
|
<span>•</span>
|
|
<span>{activeTrip.destination}</span>
|
|
<span>•</span>
|
|
<span>
|
|
{activeTrip.durationDays}D / {activeTrip.durationNights}N
|
|
</span>
|
|
<span>•</span>
|
|
<span className="uppercase text-gate-orange font-bold">
|
|
{activeTrip.tripType.replace('_', ' ')}
|
|
</span>
|
|
{activeTrip.hasLaundry && (
|
|
<span className="bg-stamp-green-light text-stamp-green border border-stamp-green/30 px-1 py-0.5 text-[10px] uppercase font-bold">
|
|
LAUNDRY ACCESS
|
|
</span>
|
|
)}
|
|
</>
|
|
) : (
|
|
<span>CREATE A NEW TRAVEL MANIFEST TO BEGIN PACKING</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Center / Right: Voyage switcher and Actions */}
|
|
<div className="flex items-center gap-2 sm:gap-3 flex-wrap">
|
|
{trips.length > 0 && (
|
|
<select
|
|
value={activeTrip?.id || ''}
|
|
onChange={(e) => {
|
|
sound.playClick();
|
|
onSelectTrip(e.target.value);
|
|
}}
|
|
className="bg-white border-2 border-ink font-mono text-xs px-2.5 py-2 shadow-card-hard-sm focus:outline-none focus:ring-2 focus:ring-canary"
|
|
>
|
|
{trips.map((t) => (
|
|
<option key={t.id} value={t.id}>
|
|
{t.routeCode} — {t.title} ({t.durationDays}D)
|
|
</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => {
|
|
sound.playClick();
|
|
onNewTrip();
|
|
}}
|
|
className="flex items-center gap-1.5 bg-canary hover:bg-canary-hover text-ink font-grotesk font-bold text-xs uppercase px-3.5 py-2 border-2 border-ink shadow-card-hard-sm active:translate-x-0.5 active:translate-y-0.5 transition-all"
|
|
>
|
|
<Plus size={15} strokeWidth={3} />
|
|
<span>NEW VOYAGE</span>
|
|
</button>
|
|
|
|
{activeTrip && (
|
|
<button
|
|
onClick={() => {
|
|
sound.playStamp();
|
|
onOpenDebrief();
|
|
}}
|
|
className="flex items-center gap-1.5 bg-gate-orange hover:bg-gate-hover text-white font-grotesk font-bold text-xs uppercase px-3 py-2 border-2 border-ink shadow-card-hard-sm active:translate-x-0.5 active:translate-y-0.5 transition-all"
|
|
title="Conduct post-trip debrief to log what to exclude/include next time"
|
|
>
|
|
<RotateCcw size={14} strokeWidth={2.5} />
|
|
<span>{activeTrip.debrief ? 'EDIT DEBRIEF' : 'TRIP DEBRIEF'}</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress & Stowing Tally Bar */}
|
|
{activeTrip && (
|
|
<div className="bg-ticket-200 border-t border-ticket-border px-3 sm:px-6 py-2 flex flex-col sm:flex-row sm:items-center justify-between gap-2 text-xs font-mono">
|
|
<div className="flex items-center gap-3">
|
|
<span className="font-bold uppercase text-ink">
|
|
TOTAL MANIFEST LOAD:
|
|
</span>
|
|
<span className="bg-ink text-ticket-100 px-2 py-0.5 font-bold">
|
|
{packedItems} / {totalItems} STOWED
|
|
</span>
|
|
<span className="font-bold text-ink">
|
|
({percentPacked}%)
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 w-full sm:w-64">
|
|
<div className="w-full bg-ticket-50 border border-ink h-3.5 relative overflow-hidden">
|
|
<div
|
|
className={`h-full transition-all duration-300 ${
|
|
percentPacked === 100 ? 'bg-stamp-green' : 'bg-canary'
|
|
}`}
|
|
style={{ width: `${percentPacked}%` }}
|
|
/>
|
|
</div>
|
|
{percentPacked === 100 && (
|
|
<span className="stamp-badge stamp-badge-packed text-[10px] py-0 px-1 font-bold whitespace-nowrap">
|
|
ALL STOWED
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</header>
|
|
);
|
|
};
|