feat: Add custom bag and archetype editors and compact line view mode
This commit is contained in:
1 parent
805f3fe97a
commit
d275808711
8 files changed
+1087
-350
No files matched your search
+127
-119
@@ -1,13 +1,45 @@
|
||||
import React from 'react';
|
||||
import { BagType, PackingItem, LuggageConfig } from '../types';
|
||||
import { BagType, PackingItem, LuggageConfig, CustomBag } from '../types';
|
||||
import { sound } from '../utils/audio';
|
||||
import { Backpack, Briefcase, Package, UserCheck, Layers, CheckCircle2 } from 'lucide-react';
|
||||
import {
|
||||
Backpack,
|
||||
Briefcase,
|
||||
Package,
|
||||
UserCheck,
|
||||
Layers,
|
||||
CheckCircle2,
|
||||
Settings2,
|
||||
Camera,
|
||||
Compass,
|
||||
Tag
|
||||
} from 'lucide-react';
|
||||
|
||||
interface BagOverviewProps {
|
||||
items: PackingItem[];
|
||||
selectedBag: BagType | 'all';
|
||||
onSelectBag: (bag: BagType | 'all') => void;
|
||||
luggageConfig: LuggageConfig;
|
||||
customBags: CustomBag[];
|
||||
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> = ({
|
||||
@@ -15,64 +47,39 @@ export const BagOverview: React.FC<BagOverviewProps> = ({
|
||||
selectedBag,
|
||||
onSelectBag,
|
||||
luggageConfig,
|
||||
customBags,
|
||||
onEditBags,
|
||||
}) => {
|
||||
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,
|
||||
},
|
||||
];
|
||||
// Filter bags based on luggage mode if it's a default bag
|
||||
const visibleBags = customBags.filter((b) => {
|
||||
if (b.id === 'carry_on' && luggageConfig === 'backpack_only') return false;
|
||||
if (b.id === 'checked' && luggageConfig !== 'checked_bag') return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center justify-between mb-2 flex-wrap gap-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>
|
||||
<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-canary text-ink transition-colors flex items-center gap-1 shadow-xs"
|
||||
title="Add, rename, or reconfigure bags"
|
||||
>
|
||||
<Settings2 size={12} />
|
||||
<span>EDIT BAGS</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
sound.playClick();
|
||||
onSelectBag('all');
|
||||
@@ -89,86 +96,87 @@ export const BagOverview: React.FC<BagOverviewProps> = ({
|
||||
</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;
|
||||
{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
|
||||
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" />
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={b.id}
|
||||
onClick={() => {
|
||||
sound.playClick();
|
||||
onSelectBag(isSelected ? 'all' : b.id);
|
||||
}}
|
||||
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}
|
||||
<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 truncate max-w-[130px]">
|
||||
{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 min-w-0">
|
||||
<span className="text-ink shrink-0">{getBagIcon(b.iconKey)}</span>
|
||||
<span className="font-display text-xl tracking-wide uppercase leading-tight truncate">
|
||||
{b.label}
|
||||
</span>
|
||||
</div>
|
||||
{isFull && (
|
||||
<CheckCircle2 size={18} className="text-stamp-green shrink-0" />
|
||||
)}
|
||||
</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">
|
||||
{/* TSA / Bag Advisory Tip */}
|
||||
{b.tsaTip && (
|
||||
<p className="font-sans text-[11px] text-ink-muted leading-tight mb-3 line-clamp-2">
|
||||
{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>
|
||||
|
||||
{/* 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 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>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user