import React, { useState, useEffect } from 'react'; import { Trip, PackingItem, BagType, Category, PackMemory, AppSettings, TripDebrief } from './types'; import { StorageClient } from './utils/storage'; import { sound } from './utils/audio'; import { BoardingHeader } from './components/BoardingHeader'; import { BagOverview } from './components/BagOverview'; import { ItemRow } from './components/ItemRow'; import { TripModal } from './components/TripModal'; import { DebriefModal } from './components/DebriefModal'; import { MemoryDrawer } from './components/MemoryDrawer'; import { SettingsModal } from './components/SettingsModal'; import { Plus, Search, Filter, CheckCircle2, Luggage, Sparkles, Trash2, Printer, Share2, RotateCcw, Plane } from 'lucide-react'; const CATEGORY_TABS: { id: Category | 'all'; label: string }[] = [ { id: 'all', label: 'ALL GEAR' }, { id: 'clothing', label: 'CLOTHING' }, { id: 'toiletries', label: 'TOILETRIES' }, { id: 'tech', label: 'TECH & POWER' }, { id: 'documents', label: 'DOCUMENTS' }, { id: 'gear', label: 'EXPEDITION GEAR' }, { id: 'wellness', label: 'WELLNESS / MED' }, ]; export function App() { const [trips, setTrips] = useState([]); const [activeTripId, setActiveTripId] = useState(null); const [memory, setMemory] = useState({ exclusions: [], inclusions: [], totalTripsDebriefed: 0, }); const [settings, setSettings] = useState(StorageClient.getSettings()); // UI Filter states const [selectedBag, setSelectedBag] = useState('all'); const [selectedCategory, setSelectedCategory] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [filterMode, setFilterMode] = useState<'all' | 'unpacked' | 'packed'>('all'); // Quick item entry const [newItemName, setNewItemName] = useState(''); const [newItemCategory, setNewItemCategory] = useState('clothing'); const [newItemBag, setNewItemBag] = useState('carry_on'); // Modals const [isTripModalOpen, setIsTripModalOpen] = useState(false); const [isDebriefModalOpen, setIsDebriefModalOpen] = useState(false); const [isMemoryOpen, setIsMemoryOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); // Load initial data useEffect(() => { async function loadData() { const loadedTrips = await StorageClient.getTrips(); const loadedMemory = await StorageClient.getMemory(); const loadedSettings = StorageClient.getSettings(); setTrips(loadedTrips); setMemory(loadedMemory); setSettings(loadedSettings); sound.enabled = loadedSettings.soundEnabled; const savedActiveId = StorageClient.getActiveTripId(); if (savedActiveId && loadedTrips.some((t) => t.id === savedActiveId)) { setActiveTripId(savedActiveId); } else if (loadedTrips.length > 0) { setActiveTripId(loadedTrips[0].id); } } loadData(); }, []); const activeTrip = trips.find((t) => t.id === activeTripId) || null; const handleSelectTrip = (id: string) => { setActiveTripId(id); StorageClient.setActiveTripId(id); setSelectedBag('all'); }; const handleSaveTrip = async (trip: Trip) => { await StorageClient.saveTrip(trip); const updatedTrips = await StorageClient.getTrips(); setTrips(updatedTrips); setActiveTripId(trip.id); StorageClient.setActiveTripId(trip.id); }; const handleDeleteTrip = async (tripId: string) => { if (!window.confirm('Are you sure you want to delete this trip manifest?')) return; sound.playClick(); await StorageClient.deleteTrip(tripId); const updatedTrips = await StorageClient.getTrips(); setTrips(updatedTrips); if (activeTripId === tripId) { setActiveTripId(updatedTrips[0]?.id || null); } }; // Item actions const handleTogglePacked = async (itemId: string) => { if (!activeTrip) return; const updatedItems = activeTrip.items.map((i) => i.id === itemId ? { ...i, packed: !i.packed } : i ); const updatedTrip = { ...activeTrip, items: updatedItems }; await StorageClient.saveTrip(updatedTrip); setTrips((prev) => prev.map((t) => (t.id === activeTrip.id ? updatedTrip : t))); }; const handleUpdateQuantity = async (itemId: string, delta: number) => { if (!activeTrip) return; const updatedItems = activeTrip.items.map((i) => { if (i.id === itemId) { const nextQty = Math.max(1, i.quantity + delta); return { ...i, quantity: nextQty }; } return i; }); const updatedTrip = { ...activeTrip, items: updatedItems }; await StorageClient.saveTrip(updatedTrip); setTrips((prev) => prev.map((t) => (t.id === activeTrip.id ? updatedTrip : t))); }; const handleUpdateBag = async (itemId: string, newBag: BagType) => { if (!activeTrip) return; const updatedItems = activeTrip.items.map((i) => i.id === itemId ? { ...i, bag: newBag } : i ); const updatedTrip = { ...activeTrip, items: updatedItems }; await StorageClient.saveTrip(updatedTrip); setTrips((prev) => prev.map((t) => (t.id === activeTrip.id ? updatedTrip : t))); }; const handleDeleteItem = async (itemId: string) => { if (!activeTrip) return; const updatedItems = activeTrip.items.filter((i) => i.id !== itemId); const updatedTrip = { ...activeTrip, items: updatedItems }; await StorageClient.saveTrip(updatedTrip); setTrips((prev) => prev.map((t) => (t.id === activeTrip.id ? updatedTrip : t))); }; const handleAddItem = async (e: React.FormEvent) => { e.preventDefault(); if (!activeTrip || !newItemName.trim()) return; sound.playStamp(); const newItem: PackingItem = { id: `item_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`, name: newItemName.trim(), category: newItemCategory, quantity: 1, bag: newItemBag, packed: false, source: 'user', }; const updatedTrip = { ...activeTrip, items: [newItem, ...activeTrip.items] }; await StorageClient.saveTrip(updatedTrip); setTrips((prev) => prev.map((t) => (t.id === activeTrip.id ? updatedTrip : t))); setNewItemName(''); }; const handleSaveDebrief = async (debrief: TripDebrief) => { if (!activeTrip) return; const updated = await StorageClient.recordDebrief(activeTrip.id, debrief); if (updated) { setTrips((prev) => prev.map((t) => (t.id === updated.id ? updated : t))); const refreshedMemory = await StorageClient.getMemory(); setMemory(refreshedMemory); } }; const handleClearMemoryItem = async (type: 'exclusion' | 'inclusion', itemName: string) => { const updated = { ...memory }; if (type === 'exclusion') { updated.exclusions = updated.exclusions.filter((e) => e.itemName !== itemName); } else { updated.inclusions = updated.inclusions.filter((i) => i.itemName !== itemName); } setMemory(updated); localStorage.setItem('loadout_memory_v1', JSON.stringify(updated)); }; const handleSaveSettings = (newSettings: AppSettings) => { setSettings(newSettings); StorageClient.saveSettings(newSettings); }; // Filtered items const filteredItems = (activeTrip?.items || []).filter((item) => { if (selectedBag !== 'all' && item.bag !== selectedBag) return false; if (selectedCategory !== 'all' && item.category !== selectedCategory) return false; if (filterMode === 'unpacked' && item.packed) return false; if (filterMode === 'packed' && !item.packed) return false; if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); return ( item.name.toLowerCase().includes(q) || (item.notes && item.notes.toLowerCase().includes(q)) ); } return true; }); return (
{/* Boarding Pass Telemetry Header */} setIsTripModalOpen(true)} onOpenDebrief={() => setIsDebriefModalOpen(true)} onOpenMemory={() => setIsMemoryOpen(true)} onOpenSettings={() => setIsSettingsOpen(true)} soundEnabled={settings.soundEnabled} onToggleSound={() => { const next = !settings.soundEnabled; sound.enabled = next; handleSaveSettings({ ...settings, soundEnabled: next }); }} /> {/* Main Content Area */}
{activeTrip ? ( <> {/* Bag Allocation Gauges ("What Goes in What Bag") */} {/* Quick Add Perforated Coupon Strip */}
EXPEDITION ADDITION // FAST ITEM STOWAGE
setNewItemName(e.target.value)} placeholder="Enter item name (e.g. Passport pouch, extra camera lens, dry bag)..." className="flex-1 bg-white border-2 border-ink px-3 py-2 text-xs font-grotesk font-bold text-ink shadow-xs focus:outline-none focus:ring-2 focus:ring-canary" />
{/* Filter & Search Bar */}
{/* Category Pills */}
{CATEGORY_TABS.map((cat) => ( ))}
{/* Search & State Filter */}
setSearchQuery(e.target.value)} placeholder="Search manifest..." className="pl-8 pr-3 py-1 bg-white border border-ink font-mono text-xs w-36 sm:w-48 focus:outline-none focus:ring-1 focus:ring-ink" />
{/* Manifest Item Count & Bag Filter Notice */}
DISPLAYING {filteredItems.length} ITEM(S) {selectedBag !== 'all' && ( (FILTERED TO {selectedBag.toUpperCase()}) )}
•
{/* List of Perforated Stubs */} {filteredItems.length === 0 ? (

NO ITEMS MATCHING THIS FILTER

Adjust your bag or category selection above, or add a new custom item.

) : (
{filteredItems.map((item) => ( ))}
)} ) : ( /* Empty State: Terminal Welcome */

DISPATCH TERMINAL READY

You currently have no active packing manifests. Create a new trip to generate an intelligent list, assign items to your bags, and begin your loadout.

)}
{/* Footer */}
LOADOUT // TRAVEL MANIFEST & BAG ALLOCATION SYSTEM SELF-HOSTED AT VLOSER.US • DEBRIEF-DRIVEN INTELLIGENCE
{/* Modals */} setIsTripModalOpen(false)} onSaveTrip={handleSaveTrip} memory={memory} /> {activeTrip && ( setIsDebriefModalOpen(false)} trip={activeTrip} onSaveDebrief={handleSaveDebrief} /> )} setIsMemoryOpen(false)} memory={memory} onClearMemoryItem={handleClearMemoryItem} /> setIsSettingsOpen(false)} settings={settings} onSaveSettings={handleSaveSettings} trips={trips} memory={memory} onImportData={({ trips: impTrips, memory: impMemory }) => { setTrips(impTrips); setMemory(impMemory); localStorage.setItem('loadout_trips_v1', JSON.stringify(impTrips)); localStorage.setItem('loadout_memory_v1', JSON.stringify(impMemory)); if (impTrips.length > 0) setActiveTripId(impTrips[0].id); }} />
); } export default App;