244 lines
8.9 KiB
TypeScript
244 lines
8.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { BagType, PackingItem, LuggageConfig, CustomBag } from '../types';
|
|
import { sound } from '../utils/audio';
|
|
import {
|
|
Backpack,
|
|
Briefcase,
|
|
Package,
|
|
UserCheck,
|
|
Layers,
|
|
CheckCircle2,
|
|
Settings2,
|
|
Camera,
|
|
Compass,
|
|
Tag,
|
|
Check,
|
|
Luggage
|
|
} from 'lucide-react';
|
|
|
|
interface BagOverviewProps {
|
|
items: PackingItem[];
|
|
selectedBag: BagType | 'all';
|
|
onSelectBag: (bag: BagType | 'all') => void;
|
|
luggageConfig: LuggageConfig;
|
|
activeTripBags: CustomBag[];
|
|
customBags: CustomBag[];
|
|
onToggleTripBag: (bagId: BagType) => void;
|
|
onEditBags: () => void;
|
|
}
|
|
|
|
function getBagIcon(iconKey?: string) {
|
|
switch (iconKey) {
|
|
case 'backpack':
|
|
return <Backpack size={20} strokeWidth={2.2} />;
|
|
case 'briefcase':
|
|
return <Briefcase size={20} strokeWidth={2.2} />;
|
|
case 'package':
|
|
return <Package size={20} strokeWidth={2.2} />;
|
|
case 'user':
|
|
return <UserCheck size={20} strokeWidth={2.2} />;
|
|
case 'camera':
|
|
return <Camera size={20} strokeWidth={2.2} />;
|
|
case 'compass':
|
|
return <Compass size={20} strokeWidth={2.2} />;
|
|
default:
|
|
return <Tag size={20} strokeWidth={2.2} />;
|
|
}
|
|
}
|
|
|
|
export const BagOverview: React.FC<BagOverviewProps> = ({
|
|
items,
|
|
selectedBag,
|
|
onSelectBag,
|
|
activeTripBags,
|
|
customBags,
|
|
onToggleTripBag,
|
|
onEditBags,
|
|
}) => {
|
|
const [showTripBagPicker, setShowTripBagPicker] = useState(false);
|
|
const visibleBags = activeTripBags;
|
|
|
|
return (
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-2 flex-wrap gap-2">
|
|
<div className="flex items-center gap-1.5 sm:gap-2 flex-wrap">
|
|
<span className="font-mono text-xs uppercase font-bold text-ink tracking-wider">
|
|
TRIP BAGS ({visibleBags.length}/{customBags.length})
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
sound.playClick();
|
|
setShowTripBagPicker(!showTripBagPicker);
|
|
}}
|
|
className={`font-mono text-[11px] font-bold px-2 py-0.5 border border-ink transition-colors flex items-center gap-1 shadow-xs ${
|
|
showTripBagPicker
|
|
? 'bg-ink text-canary'
|
|
: 'bg-canary/80 hover:bg-canary text-ink'
|
|
}`}
|
|
title="Select which bags are going on this specific trip"
|
|
>
|
|
<Luggage size={12} />
|
|
<span>SELECT TRIP BAGS</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
sound.playClick();
|
|
onEditBags();
|
|
}}
|
|
className="font-mono text-[11px] font-bold px-2 py-0.5 border border-ink bg-white hover:bg-ticket-200 text-ink transition-colors flex items-center gap-1 shadow-xs"
|
|
title="Add, rename, or reconfigure your global bag pool"
|
|
>
|
|
<Settings2 size={12} />
|
|
<span>EDIT BAG POOL</span>
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
type="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 ({items.length})</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Inline Trip Bag Loadout Switcher */}
|
|
{showTripBagPicker && (
|
|
<div className="mb-3 p-3 bg-ticket-100 border-2 border-ink shadow-card-hard-sm animate-in fade-in duration-100">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-mono text-[11px] font-bold uppercase text-ink">
|
|
WHICH BAGS ARE GOING ON THIS TRIP? (UNCHECKED BAGS AUTOMATICALLY MOVE ITEMS TO YOUR MAIN BAG)
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{customBags.map((bag) => {
|
|
const isBringing = visibleBags.some((b) => b.id === bag.id);
|
|
return (
|
|
<button
|
|
type="button"
|
|
key={bag.id}
|
|
onClick={() => {
|
|
sound.playClick();
|
|
onToggleTripBag(bag.id);
|
|
}}
|
|
className={`px-2.5 py-1.5 border-2 font-mono text-xs font-bold flex items-center gap-2 transition-all ${
|
|
isBringing
|
|
? 'bg-canary-light border-ink text-ink shadow-card-hard-sm'
|
|
: 'bg-white/60 border-ink/40 text-ink/50 hover:bg-white hover:text-ink'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`w-4 h-4 border border-ink flex items-center justify-center ${
|
|
isBringing ? 'bg-ink text-canary' : 'bg-white'
|
|
}`}
|
|
>
|
|
{isBringing && <Check size={11} strokeWidth={3} />}
|
|
</span>
|
|
<span>{bag.label}</span>
|
|
<span className="text-[10px] bg-ink text-ticket-100 px-1 py-0.2">
|
|
{bag.tagCode}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-3">
|
|
{visibleBags.map((b) => {
|
|
const bagItems = items.filter((i) => i.bag === b.id);
|
|
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.id;
|
|
const isFull = bagTotal > 0 && bagPacked === bagTotal;
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
key={b.id}
|
|
onClick={() => {
|
|
sound.playClick();
|
|
onSelectBag(isSelected ? 'all' : b.id);
|
|
}}
|
|
className={`relative text-left p-2.5 sm: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 hidden sm:block" />
|
|
<div className="ticket-notch-right hidden sm:block" />
|
|
|
|
<div>
|
|
{/* Top Barcode Header */}
|
|
<div className="flex items-center justify-between border-b border-ink/20 pb-1.5 sm:pb-2 mb-1.5 sm:mb-2 font-mono text-[10px] gap-1">
|
|
<span className="bg-ink text-ticket-100 px-1 py-0.5 font-bold uppercase shrink-0">
|
|
{b.tagCode}
|
|
</span>
|
|
<span className="text-ink-muted tracking-tight truncate">
|
|
{b.sub}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Title & Icon */}
|
|
<div className="flex items-center justify-between gap-1.5 mb-1">
|
|
<div className="flex items-center gap-1.5 sm:gap-2 text-ink min-w-0">
|
|
<span className="text-ink shrink-0 hidden sm:inline">{getBagIcon(b.iconKey)}</span>
|
|
<span className="font-display text-base sm:text-xl tracking-wide uppercase leading-tight truncate">
|
|
{b.label}
|
|
</span>
|
|
</div>
|
|
{isFull && (
|
|
<CheckCircle2 size={16} className="text-stamp-green shrink-0" />
|
|
)}
|
|
</div>
|
|
|
|
{/* TSA / Bag Advisory Tip */}
|
|
{b.tsaTip && (
|
|
<p className="font-sans text-[11px] text-ink-muted leading-tight mb-3 line-clamp-2 hidden sm:block">
|
|
{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>
|
|
);
|
|
};
|