feat: Initial release of LOADOUT - Travel Manifest & Bag Allocation System

This commit is contained in:
airent106 committed 2026-10-15 11:01:26 -06:00
commit 805f3fe97a
26 files changed
+7618

No files matched your search

+175
View File
@@ -0,0 +1,175 @@
import React from 'react';
import { BagType, PackingItem, LuggageConfig } from '../types';
import { sound } from '../utils/audio';
import { Backpack, Briefcase, Package, UserCheck, Layers, CheckCircle2 } from 'lucide-react';
interface BagOverviewProps {
items: PackingItem[];
selectedBag: BagType | 'all';
onSelectBag: (bag: BagType | 'all') => void;
luggageConfig: LuggageConfig;
}
export const BagOverview: React.FC<BagOverviewProps> = ({
items,
selectedBag,
onSelectBag,
luggageConfig,
}) => {
const bags: {
type: BagType;
label: string;
sub: string;
icon: React.ReactNode;
tsaTip: string;
tagCode: string;
visible: boolean;
}[] = [
{
type: 'personal_item',
label: 'PERSONAL ITEM',
sub: 'Under-Seat / Daypack',
icon: <Backpack size={20} strokeWidth={2.2} />,
tsaTip: 'Cabin only: Passports, power banks, essential meds, laptop',
tagCode: 'CABIN-01',
visible: true,
},
{
type: 'carry_on',
label: 'CARRY-ON ROLLER',
sub: 'Overhead Compartment',
icon: <Briefcase size={20} strokeWidth={2.2} />,
tsaTip: 'Max 3.4oz / 100ml liquids pouch. Clothes cubes & toiletries',
tagCode: 'CABIN-02',
visible: luggageConfig !== 'backpack_only',
},
{
type: 'checked',
label: 'CHECKED LUGGAGE',
sub: 'Cargo Hold',
icon: <Package size={20} strokeWidth={2.2} />,
tsaTip: 'Liquids > 100ml, trekking poles, pocket knives, heavy layers',
tagCode: 'HOLD-03',
visible: luggageConfig === 'checked_bag',
},
{
type: 'worn',
label: 'WORN ON PERSON',
sub: 'Travel Day Outfit',
icon: <UserCheck size={20} strokeWidth={2.2} />,
tsaTip: 'Heaviest boots & airport jacket to minimize luggage bulk',
tagCode: 'PAX-WORN',
visible: true,
},
];
return (
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="font-mono text-xs uppercase font-bold text-ink tracking-wider">
LUGGAGE ALLOCATION // "WHAT GOES IN WHAT BAG"
</span>
</div>
<button
onClick={() => {
sound.playClick();
onSelectBag('all');
}}
className={`font-mono text-xs font-bold px-2.5 py-1 border border-ink transition-all flex items-center gap-1.5 ${
selectedBag === 'all'
? 'bg-ink text-ticket-100 shadow-card-hard-sm'
: 'bg-ticket-100 text-ink hover:bg-ticket-200'
}`}
>
<Layers size={13} />
<span>VIEW ALL BAGS ({items.length})</span>
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
{bags
.filter((b) => b.visible)
.map((b) => {
const bagItems = items.filter((i) => i.bag === b.type);
const bagPacked = bagItems.filter((i) => i.packed).length;
const bagTotal = bagItems.length;
const pct = bagTotal > 0 ? Math.round((bagPacked / bagTotal) * 100) : 0;
const isSelected = selectedBag === b.type;
const isFull = bagTotal > 0 && bagPacked === bagTotal;
return (
<button
key={b.type}
onClick={() => {
sound.playClick();
onSelectBag(isSelected ? 'all' : b.type);
}}
className={`relative text-left p-3.5 border-2 border-ink transition-all flex flex-col justify-between ${
isSelected
? 'bg-canary-light shadow-card-hard border-ink translate-x-0.5 translate-y-0.5'
: 'bg-ticket-50 hover:bg-ticket-100 shadow-card-hard-sm'
}`}
>
{/* Perforation holes on card edges */}
<div className="ticket-notch-left" />
<div className="ticket-notch-right" />
<div>
{/* Top Barcode Header */}
<div className="flex items-center justify-between border-b border-ink/20 pb-2 mb-2 font-mono text-[10px]">
<span className="bg-ink text-ticket-100 px-1 py-0.5 font-bold uppercase">
{b.tagCode}
</span>
<span className="text-ink-muted tracking-tight">
{b.sub}
</span>
</div>
{/* Title & Icon */}
<div className="flex items-center justify-between gap-2 mb-1">
<div className="flex items-center gap-2 text-ink">
<span className="text-ink">{b.icon}</span>
<span className="font-display text-xl tracking-wide uppercase leading-tight">
{b.label}
</span>
</div>
{isFull && (
<CheckCircle2 size={18} className="text-stamp-green shrink-0" />
)}
</div>
{/* TSA Advisory Tip */}
<p className="font-sans text-[11px] text-ink-muted leading-tight mb-3">
{b.tsaTip}
</p>
</div>
{/* Meter and Stowed Count */}
<div>
<div className="flex items-center justify-between font-mono text-xs font-bold mb-1">
<span className="text-ink">
{bagPacked}/{bagTotal} ITEMS
</span>
<span className={isFull ? 'text-stamp-green' : 'text-ink-muted'}>
{pct}%
</span>
</div>
<div className="w-full bg-ticket-200 border border-ink h-2.5 overflow-hidden">
<div
className={`h-full transition-all duration-200 ${
isFull ? 'bg-stamp-green' : 'bg-gate-orange'
}`}
style={{ width: `${pct}%` }}
/>
</div>
</div>
</button>
);
})}
</div>
</div>
);
};