// src/tracking.jsx — Order-placed tracker. Distinct visual shape from cart/product
// screens: full-bleed hero with a hand-drawn map SVG, a live-ticking countdown,
// a step timeline, and a minimal order summary. No card grid.
function Tracking({ onNav, order }) {
const isMobile = useMedia("(max-width: 720px)");
// Fallback when user deep-links without an order (e.g., tweaks jump).
const safeOrder = order || {
storeId: "kalymnos",
number: "4821",
itemsCount: 3,
items: [
{ id: "deli-eggcheese", name: "Bacon-Egg-Cheese on a roll", qty: 1, unit: "6\" hero", price: 8.50 },
{ id: "drk-gatorade", name: "Gatorade Lemon-Lime", qty: 1, unit: "32oz btl", price: 2.99 },
{ id: "drk-topo", name: "Topo Chico", qty: 2, unit: "355ml btl", price: 2.50 },
],
total: 24.50,
placedAt: Date.now(),
etaSeconds: 18 * 60,
};
const store = STORES.find(s => s.id === safeOrder.storeId) || STORES[0];
const [remaining, setRemaining] = React.useState(safeOrder.etaSeconds);
React.useEffect(() => {
const id = setInterval(() => setRemaining(r => Math.max(0, r - 1)), 1000);
return () => clearInterval(id);
}, []);
const elapsed = safeOrder.etaSeconds - remaining;
const progress = Math.min(1, elapsed / safeOrder.etaSeconds);
// Four-step timeline — "current" step advances with elapsed time.
const steps = [
{ id: "placed", label: "Order placed", sub: "We texted the store", at: 0.0 },
{ id: "packed", label: "Packed", sub: `${store.name} bagged it`, at: 0.25 },
{ id: "out", label: "Out for delivery", sub: "On a bike, not a truck", at: 0.55 },
{ id: "home", label: "At your door", sub: "Leave it with the doorman", at: 1.0 },
];
const currentIdx = steps.reduce((acc, s, i) => progress >= s.at ? i : acc, 0);
const mm = String(Math.floor(remaining / 60)).padStart(2, "0");
const ss = String(remaining % 60).padStart(2, "0");
return (
{/* Hero — map + countdown, not a card stack */}
Order #{safeOrder.number} · Live
On the way.
From {store.name.split(" ")[0]} to {isMobile ? "you" : "your door"}.
{mm}:{ss}
{remaining > 0 ? "estimated to door" : "Should be knocking now."}
{/* Tiny map — inline SVG, not an image. Biker moves along a curve. */}
{!isMobile && (
)}
{/* Timeline — distinct shape: vertical ticks with an active ring */}
Progress
{steps.map((s, i) => {
const done = i < currentIdx;
const active = i === currentIdx;
return (
-
{done ?
: active ?
: null}
{s.label}
{s.sub}
);
})}
{/* Summary — minimal, not another card grid */}
);
}
function Row2({ l, r }) {
return (
{l}
{r}
);
}
// TrackingMap — minimal inline SVG. Store pin at top-right, home pin at bottom-left,
// a curved route with a moving bike marker driven by `progress` [0..1].
function TrackingMap({ progress, storeName }) {
// Control points for a gentle quadratic curve from store to home.
const from = { x: 254, y: 46 }; // store corner
const to = { x: 46, y: 254 }; // home corner
const cp = { x: 60, y: 60 }; // curve control
// Quadratic Bezier at time t
const t = progress;
const mt = 1 - t;
const bx = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
const by = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
return (
);
}
window.Tracking = Tracking;
// ─── Orders (list) ─────────────────────────────────────────────────────────
// Entry point for the Orders tab. Shows 2 active orders as compact tracker
// cards up top, then a list of recent orders below. Tapping a card opens the
// full Tracking screen for that order.
function Orders({ onNav, activeOrder, setActiveOrder }) {
const isMobile = useMedia("(max-width: 720px)");
// If there's a live order from checkout, put it at the front. Otherwise two
// mocked in-progress orders so the page has meaningful content in the demo.
// Each fallback carries a realistic item snapshot so the Tracking sidebar
// has something to render when the user taps "Track order".
const now = Date.now();
const fallbacks = [
{ number: "4821", storeId: "kalymnos", itemsCount: 3, total: 24.50,
items: [
{ id: "deli-eggcheese", name: "Bacon-Egg-Cheese on a roll", qty: 1, unit: "6\" hero", price: 8.50 },
{ id: "drk-gatorade", name: "Gatorade Lemon-Lime", qty: 1, unit: "32oz btl", price: 2.99 },
{ id: "drk-topo", name: "Topo Chico", qty: 2, unit: "355ml btl", price: 2.50 },
],
placedAt: now - 4 * 60 * 1000, etaSeconds: 14 * 60 },
{ number: "4833", storeId: "verde", itemsCount: 6, total: 38.75,
items: [
{ id: "prd-tomato", name: "Vine tomatoes", qty: 1, unit: "lb", price: 3.49 },
{ id: "prd-apple", name: "Honeycrisp apples", qty: 2, unit: "lb", price: 3.99 },
{ id: "prd-lemon", name: "Lemons", qty: 3, unit: "each", price: 0.79 },
{ id: "prd-avocado",name: "Hass avocado", qty: 2, unit: "each", price: 2.49 },
],
placedAt: now - 11 * 60 * 1000, etaSeconds: 7 * 60 },
];
const inProgress = activeOrder
? [activeOrder, fallbacks[1]]
: fallbacks;
const openTracking = (order) => {
setActiveOrder?.(order);
onNav({ screen: "tracking" });
};
const recent = [
{ number: "4802", storeId: "kalymnos", itemsCount: 3, total: 24.50,
when: "Mar 28 · 6:14 PM", status: "Delivered", items: ["Bacon-Egg-Cheese", "Gatorade Lemon-Lime", "Topo Chico"] },
{ number: "4791", storeId: "fig", itemsCount: 2, total: 47.00,
when: "Mar 25 · 8:02 PM", status: "Delivered", items: ["Meiomi Pinot Noir", "Whispering Angel"] },
{ number: "4785", storeId: "liu", itemsCount: 5, total: 18.20,
when: "Mar 22 · 11:47 PM", status: "Delivered", items: ["Advil 50ct", "Red Bull 12oz", "Liquid Death ×2", "Gum"] },
{ number: "4770", storeId: "arbor", itemsCount: 4, total: 41.85,
when: "Mar 18 · 10:15 AM", status: "Delivered", items: ["Hammer", "Picture hooks", "Drywall anchors", "Level"] },
{ number: "4758", storeId: "kalymnos", itemsCount: 7, total: 52.40,
when: "Mar 14 · 5:30 PM", status: "Delivered", items: ["Modelo 12pk", "Flamin' Hot Cheetos", "Oreo", "Topo Chico ×2", "Peanuts", "Doritos"] },
];
return (
Your orders
In progress & recent.
{/* In-progress section */}
In progress · {inProgress.length}
Live ETA from each store
{inProgress.map(o => (
openTracking(o)} />
))}
{/* Recent section */}
Recent
Last 30 days · {recent.length}
{recent.map(o => (
onNav({ screen: "store", storeId: o.storeId })} />
))}
);
}
// Compact active-order card — store thumb, live countdown, progress bar, Track.
function ActiveOrderCard({ order, onOpen }) {
const store = STORES.find(s => s.id === order.storeId) || STORES[0];
const [remaining, setRemaining] = React.useState(order.etaSeconds);
React.useEffect(() => {
const id = setInterval(() => setRemaining(r => Math.max(0, r - 1)), 1000);
return () => clearInterval(id);
}, []);
const progress = Math.min(1, (order.etaSeconds - remaining) / order.etaSeconds);
const mm = String(Math.floor(remaining / 60)).padStart(2, "0");
const ss = String(remaining % 60).padStart(2, "0");
const stage = progress < 0.25 ? "Packing" : progress < 0.55 ? "Packed" : progress < 1 ? "On the way" : "At your door";
return (
{store.name}
Live
#{order.number} · {order.itemsCount} items · {money(order.total)}
to door
~{store.distance}
}>
Track order
alert("We'll text the rider.")}>
Message rider
);
}
// Dense past-order row — store, order number, when, items count, total.
function RecentOrderRow({ order, onOpen, isMobile }) {
const store = STORES.find(s => s.id === order.storeId) || STORES[0];
return (
);
}
window.Orders = Orders;