openenv / App.jsx
ishraddhapawar's picture
Upload 45 files
ad77c79 verified
Raw History Blame Contribute Delete
29.1 kB
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { Truck, Ship, Package, DollarSign, TrendingUp, AlertTriangle, Play, Factory, Activity, BarChart2, CheckCircle, Navigation, Download, Settings, RotateCcw, Zap, Globe } from 'lucide-react';
import { LineChart, Line, AreaChart, Area, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts';
import { MapContainer, TileLayer, Marker, Polyline, Popup } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import toast, { Toaster } from 'react-hot-toast';
import './App.css';
import EditorialNavigation from './components/Navigation';
import Hero from './components/Hero';
import StatsGrid from './components/StatsGrid';
import TextReveal from './components/TextReveal';
import Workflow from './components/Workflow';
import UseCaseTabs from './components/UseCaseTabs';
import ContactForm from './components/ContactForm';
import Footer from './components/Footer';
import InteractiveWavesBackground from './components/InteractiveWaves';
const API = '/api';
// ── Map Data ──────────────────────────────────────────────────────
const MAP_NODES = {
factories: [
{ id: 'china', name: 'China Factory', lat: 22.5, lng: 114, cost: 50 },
{ id: 'india', name: 'India Factory', lat: 19, lng: 72.8, cost: 45 },
{ id: 'mexico', name: 'Mexico Factory', lat: 25.6, lng: -100.3, cost: 70 },
{ id: 'vietnam', name: 'Vietnam Factory', lat: 10.8, lng: 106.6, cost: 42 },
{ id: 'brazil', name: 'Brazil Factory', lat: -23.5, lng: -46.6, cost: 65 },
{ id: 'germany', name: 'Germany Factory', lat: 48.8, lng: 9.2, cost: 90 },
],
ports: [
{ id: 'shanghai', name: 'Shanghai Port', lat: 31.23, lng: 121.47, delay: 1 },
{ id: 'rotterdam', name: 'Rotterdam Port', lat: 51.92, lng: 4.48, delay: 2 },
{ id: 'la', name: 'LA Port', lat: 33.74, lng: -118.27, delay: 1 },
{ id: 'singapore', name: 'Singapore Port', lat: 1.35, lng: 103.82, delay: 1 },
{ id: 'busan', name: 'Busan Port', lat: 35.1, lng: 129.03, delay: 1 },
{ id: 'dubai', name: 'Dubai Port', lat: 25.0, lng: 55.1, delay: 2 },
{ id: 'santos', name: 'Santos Port', lat: -23.96, lng: -46.3, delay: 2 },
],
warehouses: [
{ id: 'usa', name: 'USA Warehouse', lat: 39.83, lng: -98.58 },
{ id: 'eu', name: 'EU Warehouse', lat: 50.11, lng: 8.68 },
{ id: 'asia', name: 'Asia Warehouse', lat: 35.68, lng: 139.69 },
{ id: 'mideast', name: 'Middle East Warehouse', lat: 25.2, lng: 55.3 },
{ id: 'latam', name: 'LatAm Warehouse', lat: -23.5, lng: -46.6 },
{ id: 'oceania', name: 'Oceania Warehouse', lat: -33.87, lng: 151.21 },
],
};
const ROUTES = [
// Factory β†’ Port (gold lines)
{ from: [22.5, 114], to: [31.23, 121.47], color: '#b08d3a' },
{ from: [22.5, 114], to: [1.35, 103.82], color: '#b08d3a' },
{ from: [19, 72.8], to: [1.35, 103.82], color: '#b08d3a' },
{ from: [19, 72.8], to: [25.0, 55.1], color: '#b08d3a' },
{ from: [25.6, -100.3], to: [33.74, -118.27], color: '#b08d3a' },
{ from: [10.8, 106.6], to: [1.35, 103.82], color: '#b08d3a' },
{ from: [10.8, 106.6], to: [35.1, 129.03], color: '#b08d3a' },
{ from: [-23.5, -46.6], to: [-23.96, -46.3], color: '#b08d3a' },
{ from: [48.8, 9.2], to: [51.92, 4.48], color: '#b08d3a' },
// Port β†’ Warehouse (green lines)
{ from: [31.23, 121.47], to: [39.83, -98.58], color: '#3d7068' },
{ from: [31.23, 121.47], to: [35.68, 139.69], color: '#3d7068' },
{ from: [33.74, -118.27], to: [39.83, -98.58], color: '#3d7068' },
{ from: [51.92, 4.48], to: [50.11, 8.68], color: '#3d7068' },
{ from: [1.35, 103.82], to: [35.68, 139.69], color: '#3d7068' },
{ from: [1.35, 103.82], to: [-33.87, 151.21], color: '#3d7068' },
{ from: [35.1, 129.03], to: [35.68, 139.69], color: '#3d7068' },
{ from: [25.0, 55.1], to: [25.2, 55.3], color: '#3d7068' },
{ from: [25.0, 55.1], to: [50.11, 8.68], color: '#3d7068' },
{ from: [-23.96, -46.3], to: [-23.5, -46.6], color: '#3d7068' },
{ from: [-23.96, -46.3], to: [39.83, -98.58], color: '#3d7068' },
];
// ── Custom Marker Icons ───────────────────────────────────────────
function createIcon(type, label) {
return L.divIcon({
className: '',
html: `<div class="map-marker-label ${type}">${label}</div>`,
iconSize: [0, 0],
iconAnchor: [0, 0],
});
}
// ── Interactive Map Component ─────────────────────────────────────
function InteractiveMap({ state }) {
const inventory = state?.inventory || { usa: 0, eu: 0, asia: 0 };
const portDelays = state?.portDelays || [];
return (
<div className="map-container">
<MapContainer
center={[25, 20]}
zoom={2}
minZoom={2}
maxZoom={6}
style={{ width: '100%', height: '100%' }}
zoomControl={true}
attributionControl={true}
>
<TileLayer
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
attribution='&copy; <a href="https://carto.com/">CARTO</a>'
/>
{/* Factory Markers */}
{(state?.factoryCosts || []).map(f => {
const node = MAP_NODES.factories.find(n => n.id === f.id);
if (!node) return null;
return (
<Marker key={f.id} position={[node.lat, node.lng]} icon={createIcon('factory', `🏭 $${f.currentCost}`)}>
<Popup><b>{f.name}</b><br/>Unit Cost: ${f.currentCost}</Popup>
</Marker>
);
})}
{/* Port Markers */}
{portDelays.map(p => {
const node = MAP_NODES.ports.find(n => n.id === p.id);
if (!node) return null;
return (
<Marker key={p.id} position={[node.lat, node.lng]} icon={createIcon('port', `βš“ ${p.isClosed ? 'βœ•' : p.currentDelay + 'm'}`)}>
<Popup><b>{p.name}</b><br/>Delay: {p.currentDelay}mo<br/>Status: {p.isClosed ? 'πŸ”΄ Closed' : '🟒 Open'}</Popup>
</Marker>
);
})}
{/* Warehouse Markers */}
{MAP_NODES.warehouses.map(w => (
<Marker key={w.id} position={[w.lat, w.lng]} icon={createIcon('warehouse', `πŸ“¦ ${inventory[w.id] ?? 0}`)}>
<Popup><b>{w.name}</b><br/>Inventory: {inventory[w.id] ?? 0} units</Popup>
</Marker>
))}
{/* Route Lines */}
{ROUTES.map((r, i) => (
<Polyline key={i} positions={[r.from, r.to]} pathOptions={{ color: r.color, weight: 2, opacity: 0.6, dashArray: '8 6' }} />
))}
</MapContainer>
</div>
);
}
// ── Chart Tooltip ─────────────────────────────────────────────────
const ChartTooltip = ({ active, payload, label }) => {
if (!active || !payload?.length) return null;
return (
<div style={{
background: '#f7f6f2', border: '1px solid #e5e4de',
borderRadius: 2, padding: '10px 14px', fontSize: '0.8rem', fontFamily: "'Space Mono', monospace"
}}>
<div style={{ color: '#B4B4B4', marginBottom: 4, fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.2em' }}>{label}</div>
{payload.map((p, i) => (
<div key={i} style={{ color: p.color, fontWeight: 700, fontSize: '0.85rem' }}>{p.name}: {typeof p.value === 'number' ? p.value.toLocaleString() : p.value}</div>
))}
</div>
);
};
// ── Main App ──────────────────────────────────────────────────────
function App() {
const [state, setState] = useState(null);
const [loading, setLoading] = useState(false);
const [factory, setFactory] = useState('china');
const [port, setPort] = useState('shanghai');
const [destination, setDestination] = useState('usa');
const [quantity, setQuantity] = useState(100);
const [oracleAdvice, setOracleAdvice] = useState('');
const [autoPlaying, setAutoPlaying] = useState(false);
// ── Fetch state ───────────────────────────────────────────────
const fetchState = useCallback(async () => {
try {
const res = await fetch(`${API}/state`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
setState(data);
} catch (err) {
console.error('Fetch error:', err);
}
}, []);
useEffect(() => { fetchState(); }, [fetchState]);
// ── Submit order (matches backend OrderRequest: factory, quantity, port, dest) ──
const submitOrder = useCallback(async () => {
try {
const res = await fetch(`${API}/order`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ factory, port, dest: destination, quantity }),
});
if (!res.ok) {
const err = await res.json();
toast.error(err.detail || 'Order failed');
return;
}
toast.success(`Order: ${quantity} units via ${port}`);
await fetchState();
} catch (err) {
toast.error('Order failed');
}
}, [factory, port, destination, quantity, fetchState]);
// ── Advance turn ──────────────────────────────────────────────
const advanceTurn = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(`${API}/advance`, { method: 'POST' });
const data = await res.json();
setState(data);
toast.success(`Month ${data.turn} complete`);
} catch (err) {
toast.error('Advance failed');
} finally {
setLoading(false);
}
}, []);
// ── Reset ─────────────────────────────────────────────────────
const resetGame = useCallback(async () => {
try {
await fetch(`${API}/reset`, { method: 'POST' });
await fetchState();
toast.success('Simulation reset');
} catch (err) {
toast.error('Reset failed');
}
}, [fetchState]);
// ── Oracle ────────────────────────────────────────────────────
const fetchOracle = useCallback(async () => {
try {
const res = await fetch(`${API}/oracle`);
const data = await res.json();
setOracleAdvice(data.advice || 'No advice available');
} catch {
setOracleAdvice('Oracle unavailable');
}
}, []);
useEffect(() => { if (state) fetchOracle(); }, [state, fetchOracle]);
// ── Auto-play ─────────────────────────────────────────────────
const autoPlay = useCallback(async () => {
setAutoPlaying(true);
try {
const res = await fetch(`${API}/autoplay`, { method: 'POST' });
const data = await res.json();
setState(data);
toast.success('AI Auto-Play complete');
} catch {
toast.error('Auto-play failed');
} finally {
setAutoPlaying(false);
}
}, []);
// ── Export CSV ─────────────────────────────────────────────────
const exportCSV = useCallback(() => {
if (!state?.history?.length) return;
const headers = ['Month', 'Revenue', 'Balance', 'Profit', 'Penalties'];
const rows = state.history.map(h => [h.month, h.revenue, h.balance, h.profit, h.penalties]);
const csv = [headers, ...rows].map(r => r.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.setAttribute('hidden', '');
a.setAttribute('href', url);
a.setAttribute('download', 'supply_chain_report.csv');
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
toast.success('CSV Exported');
}, [state]);
// ── Decision preview ──────────────────────────────────────────
const preview = useMemo(() => {
if (!state) return { cost: 0, eta: 2, affordable: true, costPerUnit: 0 };
const f = (state.factoryCosts || []).find(x => x.id === factory);
const p = (state.portDelays || []).find(x => x.id === port);
const unitCost = f ? f.currentCost : 50;
const containers = Math.ceil(quantity / 100);
const cost = unitCost * quantity + containers * 3000;
const eta = p ? p.currentDelay + 1 : 2;
const affordable = cost <= (state.balance || 0);
return { cost, eta, affordable, costPerUnit: unitCost };
}, [factory, port, quantity, state]);
// ── History for charts ────────────────────────────────────────
const chartData = useMemo(() => {
if (!state?.history) return [];
return state.history.map(h => ({
name: `M${h.month}`,
revenue: h.revenue || 0,
balance: h.balance || 0,
profit: h.profit || 0,
}));
}, [state]);
// ── Derived values ────────────────────────────────────────────
const totalInventory = state ? Object.values(state.inventory || {}).reduce((a, b) => a + b, 0) : 0;
const totalDemand = state ? Object.values(state.demands || {}).reduce((a, b) => a + b, 0) : 0;
// ── Render ────────────────────────────────────────────────────
return (
<>
<Toaster position="top-right" toastOptions={{
style: { background: '#f7f6f2', color: '#1c1c1c', border: '1px solid #e5e4de', borderRadius: 2, fontSize: '0.85rem', fontFamily: "'Space Mono', monospace" }
}} />
{/* Editorial background grid */}
<div className="bg-grid" />
{/* Interactive Perlin noise waves β€” covers entire UI */}
<InteractiveWavesBackground
lineColor="rgba(61, 112, 104, 0.35)"
backgroundColor="transparent"
waveSpeedX={0.012}
waveSpeedY={0.008}
waveAmpX={40}
waveAmpY={20}
friction={0.92}
tension={0.008}
maxCursorMove={120}
xGap={14}
yGap={36}
/>
{/* Landing Page Sections */}
<EditorialNavigation />
<div className="landing-page">
<Hero />
<StatsGrid />
<TextReveal />
<Workflow />
<UseCaseTabs />
<ContactForm />
</div>
{/* Simulator Section */}
<div id="simulator" className="simulator-section">
{!state ? (
<div className="loading-screen" style={{ minHeight: '60vh' }}>
<img src="/logo.png" alt="TradeStorm" className="loading-logo" />
<div className="loading-spinner" />
<div className="loading-text">Connecting to simulation engine...</div>
</div>
) : (
<>
{/* ─── TOP BAR ─── */}
<header className="top-bar">
<div className="top-bar-right" style={{ marginLeft: 'auto' }}>
<span className="scenario-pill">
<Zap size={12} /> SCENARIO: {(state.scenario || 'normal').toUpperCase()}
</span>
<span className="month-badge">MONTH {state.turn || 1}</span>
</div>
</header>
{/* ─── MAIN CONTENT ─── */}
<div className="main-content">
{/* ─── SIDEBAR ─── */}
<aside className="sidebar">
{/* Command Center */}
<div className="panel">
<div className="panel-header">
<div className="panel-title"><Navigation size={16} /> Command Center</div>
</div>
<div className="form-group">
<label className="form-label">Source Factory</label>
<select className="form-select" value={factory} onChange={e => setFactory(e.target.value)}>
{(state.factoryCosts || []).map(f => (
<option key={f.id} value={f.id}>{f.name} (${f.currentCost}/unit)</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Shipping Port</label>
<select className="form-select" value={port} onChange={e => setPort(e.target.value)}>
{(state.portDelays || []).map(p => (
<option key={p.id} value={p.id}>{p.name} ({p.currentDelay}mo delay){p.isClosed ? ' β›” CLOSED' : ''}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Destination</label>
<select className="form-select" value={destination} onChange={e => setDestination(e.target.value)}>
{MAP_NODES.warehouses.map(w => (
<option key={w.id} value={w.id}>{w.name}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Quantity</label>
<input className="form-input" type="number" min="10" max="500" step="10" value={quantity} onChange={e => setQuantity(Number(e.target.value))} />
</div>
{/* Decision Preview */}
<div style={{ margin: '0.5rem 0' }}>
<div className="form-label" style={{ marginBottom: 6 }}>Decision Preview</div>
<div className="preview-grid">
<div className="preview-item">
<div className="label">Total Cost</div>
<div className={`value ${preview.affordable ? 'affordable' : 'expensive'}`}>${preview.cost.toLocaleString()}</div>
</div>
<div className="preview-item">
<div className="label">ETA</div>
<div className="value">{preview.eta} months</div>
</div>
<div className="preview-item">
<div className="label">Cost/Unit</div>
<div className="value">${preview.costPerUnit}</div>
</div>
<div className="preview-item">
<div className="label">Affordable?</div>
<div className={`value ${preview.affordable ? 'affordable' : 'expensive'}`}>{preview.affordable ? 'βœ“ Yes' : 'βœ• No'}</div>
</div>
</div>
</div>
<button className="btn btn-primary btn-block" onClick={submitOrder}>
<CheckCircle size={16} /> Approve Order
</button>
</div>
{/* Actions */}
<div className="panel">
<div className="panel-header">
<div className="panel-title"><Play size={16} /> Simulation</div>
</div>
<div className="flex flex-col gap-sm">
<button className="btn btn-success btn-block" onClick={advanceTurn} disabled={loading}>
{loading ? <span className="loading-spinner" style={{ width: 16, height: 16, borderWidth: 2 }} /> : <Play size={16} />}
Simulate Turn {(state.turn || 0) + 1}
</button>
<button className="btn btn-warning btn-block" onClick={autoPlay} disabled={autoPlaying}>
<Zap size={16} /> {autoPlaying ? 'AI Thinking...' : 'AI Auto-Play'}
</button>
<div className="flex gap-sm">
<button className="btn btn-outline btn-sm" style={{ flex: 1 }} onClick={resetGame}>
<RotateCcw size={14} /> Reset
</button>
<button className="btn btn-outline btn-sm" style={{ flex: 1 }} onClick={exportCSV}>
<Download size={14} /> Export
</button>
</div>
</div>
</div>
{/* Alerts */}
{(state.alerts || []).length > 0 && (
<div className="panel">
<div className="panel-header">
<div className="panel-title"><AlertTriangle size={16} /> Smart Alerts</div>
</div>
<div className="event-list">
{state.alerts.map((a, i) => (
<div key={i} className={`event-item ${a.type || 'info'}`}>{a.msg}</div>
))}
</div>
</div>
)}
{/* Events */}
<div className="panel">
<div className="panel-header">
<div className="panel-title"><Activity size={16} /> Events</div>
</div>
<div className="event-list">
{(state.events || []).length === 0 && (
<div className="event-item info">No events yet. Start simulating!</div>
)}
{(state.events || []).slice(-6).reverse().map((ev, i) => (
<div key={i} className={`event-item ${ev.type || 'info'}`}>{ev.msg || ev.message || ev}</div>
))}
</div>
</div>
{/* In Transit */}
<div className="panel">
<div className="panel-header">
<div className="panel-title"><Ship size={16} /> In Transit</div>
</div>
<div className="shipment-list">
{(state.inTransit || []).length === 0 && (
<div style={{ fontSize: '0.82rem', color: 'var(--text-muted)', textAlign: 'center', padding: '1rem 0' }}>No active shipments</div>
)}
{(state.inTransit || []).map((s, i) => (
<div key={i} className="shipment-item">
<span>{s.quantity} units β†’ {(s.dest || '').toUpperCase()}</span>
<span className={`shipment-badge ${s.delayLeft <= 0 ? 'arriving' : 'transit'}`}>
{s.delayLeft <= 0 ? 'βœ“ Arriving' : `${s.delayLeft}mo left`}
</span>
</div>
))}
</div>
</div>
</aside>
{/* Layout divider */}
<div className="layout-divider" />
{/* ─── CENTER CONTENT ─── */}
<div style={{ display: 'flex', flexDirection: 'column', overflowY: 'auto', height: '100%', minHeight: 0 }}>
{/* Oracle Banner */}
{oracleAdvice && (
<div className="oracle-banner fade-in">
<div className="oracle-title"><Zap size={14} /> AI Oracle Advisory</div>
<div className="oracle-text">{oracleAdvice}</div>
</div>
)}
{/* KPI Cards */}
<div className="kpi-grid">
<div className="kpi-card">
<div className="kpi-header">
<span className="kpi-label">Balance</span>
<div className="kpi-icon">
<DollarSign size={14} color="#3d7068" />
</div>
</div>
<div className="kpi-value">${(state.balance || 0).toLocaleString()}</div>
</div>
<div className="kpi-card">
<div className="kpi-header">
<span className="kpi-label">Benchmark</span>
<div className="kpi-icon">
<TrendingUp size={14} color="#B4B4B4" />
</div>
</div>
<div className="kpi-value" style={{ color: '#B4B4B4' }}>${(state.benchmark_balance || 0).toLocaleString()}</div>
</div>
<div className="kpi-card">
<div className="kpi-header">
<span className="kpi-label">Inventory</span>
<div className="kpi-icon">
<Package size={14} color="#b08d3a" />
</div>
</div>
<div className="kpi-value">{totalInventory}</div>
</div>
<div className="kpi-card">
<div className="kpi-header">
<span className="kpi-label">Demand</span>
<div className="kpi-icon">
<Activity size={14} color="#1c1c1c" />
</div>
</div>
<div className="kpi-value">{totalDemand}</div>
</div>
</div>
{/* Map */}
<InteractiveMap state={state} />
{/* Charts */}
<div className="charts-grid">
<div className="panel" style={{ borderRight: '1px solid #e5e4de' }}>
<div className="panel-header">
<div className="panel-title"><BarChart2 size={16} /> Balance & Revenue</div>
</div>
<div className="chart-container">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e4de" />
<XAxis dataKey="name" stroke="#B4B4B4" fontSize={10} fontFamily="Space Mono" />
<YAxis stroke="#B4B4B4" fontSize={10} fontFamily="Space Mono" />
<Tooltip content={<ChartTooltip />} />
<Area type="monotone" dataKey="balance" stroke="#3d7068" fill="rgba(61,112,104,0.08)" strokeWidth={2} name="Balance" />
<Area type="monotone" dataKey="revenue" stroke="#b08d3a" fill="rgba(176,141,58,0.08)" strokeWidth={2} name="Revenue" />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="panel">
<div className="panel-header">
<div className="panel-title"><TrendingUp size={16} /> Profit per Turn</div>
</div>
<div className="chart-container">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e4de" />
<XAxis dataKey="name" stroke="#B4B4B4" fontSize={10} fontFamily="Space Mono" />
<YAxis stroke="#B4B4B4" fontSize={10} fontFamily="Space Mono" />
<Tooltip content={<ChartTooltip />} />
<Bar dataKey="profit" fill="#3d7068" radius={[1,1,0,0]} name="Profit" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Warehouse Inventory Bars */}
<div className="panel">
<div className="panel-header">
<div className="panel-title"><Factory size={16} /> Warehouse Levels</div>
</div>
<div className="flex flex-col gap-sm">
{MAP_NODES.warehouses.map(w => {
const inv = state.inventory?.[w.id] || 0;
const demand = state.demands?.[w.id] || 0;
const pct = Math.min(100, (inv / Math.max(demand, 1)) * 100);
const color = pct > 80 ? '#3d7068' : pct > 40 ? '#b08d3a' : '#a04040';
return (
<div key={w.id}>
<div className="flex items-center justify-between" style={{ fontSize: '0.82rem' }}>
<span style={{ color: 'var(--muted)' }}>{w.name} (demand: {demand})</span>
<span className="text-mono" style={{ color, fontWeight: 600 }}>{inv} units</span>
</div>
<div className="inventory-bar-track">
<div className="inventory-bar-fill" style={{ width: `${pct}%`, background: color }} />
</div>
</div>
);
})}
</div>
</div>
{/* Leaderboard */}
{(state.leaderboard || []).length > 0 && (
<div className="panel">
<div className="panel-header">
<div className="panel-title"><BarChart2 size={16} /> Top Routes</div>
</div>
<div className="shipment-list">
{state.leaderboard.map((r, i) => (
<div key={i} className="shipment-item">
<span style={{ fontWeight: 600 }}>{i + 1}. {r.route}</span>
<span style={{ fontSize: '0.78rem', color: 'var(--text-muted)' }}>${r.cost} | {r.time}mo | {r.reliability}%</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
</>
)}
</div>
<Footer />
</>
);
}
export default App;