Delivery — Driver Earnings
A mobile-first driver earnings and shift summary screen for a fictional food-delivery app. A bold orange hero shows net pay with a base, tips, and bonus breakdown plus trips, hours online, and per-hour stats, and toggles between today and the full week. A seven-day bar chart animates into place and reacts to taps, a recent-trips list flags delivered, in-progress, and failed runs with pay pills, and a sticky cash-out bar fires a confirmation toast — all in vanilla JS.
MCP
Code
:root {
--brand: #ff5a2c;
--brand-d: #e0461d;
--ink: #16181d;
--ink-2: #3b3f4a;
--muted: #71757f;
--bg: #f4f5f7;
--surface: #ffffff;
--line: rgba(22, 24, 29, 0.1);
--ok: #1f9d62;
--warn: #e89422;
--danger: #d4493e;
--track: #5b8def;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-sm: 0 1px 2px rgba(22, 24, 29, 0.06), 0 1px 3px rgba(22, 24, 29, 0.05);
--shadow-md: 0 8px 24px rgba(22, 24, 29, 0.1);
--shadow-lg: 0 18px 50px rgba(22, 24, 29, 0.18);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 28px 16px;
background:
radial-gradient(1200px 600px at 50% -10%, rgba(255, 90, 44, 0.12), transparent 60%),
var(--bg);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ---------- phone shell ---------- */
.phone {
position: relative;
width: 100%;
max-width: 420px;
min-height: 760px;
display: flex;
flex-direction: column;
background: var(--bg);
border-radius: 32px;
box-shadow: var(--shadow-lg);
overflow: hidden;
border: 1px solid var(--line);
}
/* ---------- topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 18px 14px;
background: var(--surface);
border-bottom: 1px solid var(--line);
}
.driver { display: flex; align-items: center; gap: 11px; }
.avatar {
width: 42px;
height: 42px;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 700;
font-size: 14px;
color: #fff;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
letter-spacing: 0.3px;
}
.driver-name { margin: 0; font-weight: 700; font-size: 15px; }
.driver-id { margin: 0; font-size: 12px; color: var(--muted); font-weight: 500; }
.icon-btn {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink-2);
cursor: pointer;
transition: background 0.15s ease, transform 0.1s ease;
}
.icon-btn:hover { background: var(--bg); }
.icon-btn:active { transform: scale(0.94); }
.icon-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* ---------- content scroll ---------- */
.content {
flex: 1;
overflow-y: auto;
padding: 16px 16px 0;
}
/* ---------- hero ---------- */
.hero {
position: relative;
padding: 18px 18px 20px;
border-radius: var(--r-lg);
color: #fff;
background:
radial-gradient(120% 120% at 100% 0%, rgba(255, 255, 255, 0.18), transparent 55%),
linear-gradient(150deg, var(--brand), var(--brand-d));
box-shadow: var(--shadow-md);
overflow: hidden;
}
.toggle {
display: inline-flex;
padding: 3px;
gap: 3px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
margin-bottom: 16px;
}
.toggle-btn {
border: 0;
background: transparent;
color: rgba(255, 255, 255, 0.85);
font: inherit;
font-weight: 600;
font-size: 13px;
padding: 6px 16px;
border-radius: 999px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease;
}
.toggle-btn.is-active { background: #fff; color: var(--brand-d); }
.toggle-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.hero-label { margin: 0; font-size: 13px; font-weight: 500; opacity: 0.9; }
.hero-total {
margin: 2px 0 4px;
font-size: 44px;
font-weight: 800;
letter-spacing: -1.5px;
line-height: 1.05;
font-variant-numeric: tabular-nums;
}
.hero-sub { margin: 0 0 16px; font-size: 13px; font-weight: 500; opacity: 0.92; }
.breakdown {
list-style: none;
margin: 0 0 16px;
padding: 12px 14px;
display: grid;
gap: 9px;
background: rgba(255, 255, 255, 0.14);
border-radius: var(--r-md);
}
.breakdown li {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
font-size: 14px;
}
.bd-dot { width: 10px; height: 10px; border-radius: 50%; }
.dot-base { background: #fff; }
.dot-tip { background: #ffe2b0; }
.dot-bonus { background: #c7f0d9; }
.bd-k { font-weight: 500; opacity: 0.95; }
.bd-v { font-weight: 700; font-variant-numeric: tabular-nums; }
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.stat {
background: rgba(255, 255, 255, 0.14);
border-radius: var(--r-md);
padding: 11px 8px;
text-align: center;
}
.stat-v {
margin: 0;
font-size: 19px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.stat-k { margin: 2px 0 0; font-size: 11.5px; opacity: 0.88; font-weight: 500; }
/* ---------- cards ---------- */
.card {
background: var(--surface);
border-radius: var(--r-lg);
border: 1px solid var(--line);
box-shadow: var(--shadow-sm);
padding: 16px;
margin-top: 14px;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.card-head h2 { margin: 0; font-size: 15px; font-weight: 700; }
.pill {
font-size: 12px;
font-weight: 700;
padding: 3px 10px;
border-radius: 999px;
font-variant-numeric: tabular-nums;
}
.pill-ok { color: var(--ok); background: rgba(31, 157, 98, 0.12); }
.muted-link { font-size: 12.5px; color: var(--muted); font-weight: 600; }
/* ---------- chart ---------- */
.chart {
display: grid;
grid-template-columns: repeat(7, 1fr);
align-items: end;
gap: 8px;
height: 150px;
padding-top: 6px;
}
.bar-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
height: 100%;
justify-content: flex-end;
cursor: pointer;
background: none;
border: 0;
padding: 0;
font: inherit;
}
.bar-track {
width: 100%;
flex: 1;
display: flex;
align-items: flex-end;
justify-content: center;
}
.bar {
width: 70%;
max-width: 26px;
border-radius: 7px 7px 4px 4px;
background: linear-gradient(180deg, #ffd0c2, #ffb39c);
transition: height 0.5s cubic-bezier(0.16, 1, 0.3, 1), background 0.2s ease, transform 0.12s ease;
min-height: 5px;
}
.bar-col:hover .bar { background: linear-gradient(180deg, #ffae96, #ff8e6f); }
.bar-col.is-active .bar { background: linear-gradient(180deg, var(--brand), var(--brand-d)); }
.bar-col.is-today .bar { box-shadow: 0 0 0 2px rgba(255, 90, 44, 0.18); }
.bar-col:active .bar { transform: scaleY(0.96); }
.bar-col:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 8px; }
.bar-day {
font-size: 11px;
font-weight: 600;
color: var(--muted);
}
.bar-col.is-active .bar-day { color: var(--brand-d); }
.chart-hint { margin: 12px 0 0; font-size: 12px; color: var(--muted); text-align: center; }
/* ---------- trips ---------- */
.trips { list-style: none; margin: 0; padding: 0; }
.trip {
display: grid;
grid-template-columns: 38px 1fr auto;
align-items: center;
gap: 12px;
padding: 12px 0;
border-top: 1px solid var(--line);
}
.trip:first-child { border-top: 0; padding-top: 2px; }
.trip-ic {
width: 38px;
height: 38px;
border-radius: 11px;
display: grid;
place-items: center;
color: #fff;
}
.trip-ic.s-done { background: var(--ok); }
.trip-ic.s-pending { background: var(--track); }
.trip-ic.s-failed { background: var(--danger); }
.trip-main { min-width: 0; }
.trip-route {
margin: 0;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.trip-meta { margin: 2px 0 0; font-size: 12px; color: var(--muted); }
.trip-meta .tag {
display: inline-block;
padding: 1px 7px;
border-radius: 999px;
font-weight: 600;
font-size: 10.5px;
margin-right: 5px;
vertical-align: 1px;
}
.tag-done { color: var(--ok); background: rgba(31, 157, 98, 0.12); }
.tag-pending { color: var(--track); background: rgba(91, 141, 239, 0.14); }
.tag-failed { color: var(--danger); background: rgba(212, 73, 62, 0.12); }
.trip-pay {
text-align: right;
font-weight: 700;
font-size: 15px;
font-variant-numeric: tabular-nums;
}
.trip-pay.zero { color: var(--muted); }
.trip-pay small { display: block; font-size: 11px; font-weight: 500; color: var(--muted); }
.spacer { height: 18px; }
/* ---------- cashout bar ---------- */
.cashout-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px));
background: var(--surface);
border-top: 1px solid var(--line);
box-shadow: 0 -6px 20px rgba(22, 24, 29, 0.05);
}
.cashout-label { margin: 0; font-size: 12px; color: var(--muted); font-weight: 500; }
.cashout-amt {
margin: 1px 0 0;
font-size: 21px;
font-weight: 800;
letter-spacing: -0.5px;
font-variant-numeric: tabular-nums;
}
.cashout-btn {
display: inline-flex;
align-items: center;
gap: 8px;
border: 0;
cursor: pointer;
font: inherit;
font-weight: 700;
font-size: 15px;
color: #fff;
padding: 13px 22px;
border-radius: 14px;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
box-shadow: 0 6px 16px rgba(255, 90, 44, 0.35);
transition: transform 0.1s ease, box-shadow 0.18s ease, filter 0.18s ease;
}
.cashout-btn:hover { box-shadow: 0 9px 22px rgba(255, 90, 44, 0.42); }
.cashout-btn:active { transform: scale(0.97); }
.cashout-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.cashout-btn[disabled] {
filter: grayscale(0.5) opacity(0.6);
cursor: not-allowed;
box-shadow: none;
}
/* ---------- toast ---------- */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 50;
width: max-content;
max-width: 90vw;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 9px;
padding: 12px 16px;
border-radius: 12px;
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 600;
box-shadow: var(--shadow-md);
opacity: 0;
transform: translateY(14px);
animation: toast-in 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.toast.ok::before { content: ""; width: 9px; height: 9px; border-radius: 50%; background: var(--ok); }
.toast.info::before { content: ""; width: 9px; height: 9px; border-radius: 50%; background: var(--track); }
.toast.out { animation: toast-out 0.25s ease forwards; }
@keyframes toast-in { to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(10px); } }
/* ---------- responsive ---------- */
@media (max-width: 520px) {
body { padding: 0; align-items: stretch; }
.phone {
max-width: 100%;
min-height: 100vh;
border-radius: 0;
border: 0;
box-shadow: none;
}
.hero-total { font-size: 40px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}(function () {
"use strict";
// ---------- fictional data ----------
var WEEK = [
{ day: "Mon", base: 84.5, tips: 31.0, bonus: 8.0 },
{ day: "Tue", base: 72.25, tips: 24.5, bonus: 0 },
{ day: "Wed", base: 96.0, tips: 41.75, bonus: 12.0 },
{ day: "Thu", base: 61.5, tips: 18.0, bonus: 0 },
{ day: "Fri", base: 118.75, tips: 58.25, bonus: 20.0 },
{ day: "Sat", base: 134.0, tips: 72.5, bonus: 25.0 },
{ day: "Sun", base: 88.4, tips: 36.6, bonus: 6.0 } // today
];
var TODAY_INDEX = 6;
var TODAY = {
trips: 14,
hours: 6.5,
base: WEEK[TODAY_INDEX].base,
tips: WEEK[TODAY_INDEX].tips,
bonus: WEEK[TODAY_INDEX].bonus
};
var TRIPS = [
{ route: "Ria's Kitchen → Maple Ct", time: "8:42 PM", status: "pending", pay: 11.4 },
{ route: "Pho Saigon → 14 Birch Ln", time: "8:09 PM", status: "done", pay: 9.75 },
{ route: "Green Bowl → Harbor Apts", time: "7:31 PM", status: "done", pay: 14.2 },
{ route: "Nonna's → 88 Cedar St", time: "6:58 PM", status: "failed", pay: 0 },
{ route: "Sushi Hana → Lakeview 3B", time: "6:22 PM", status: "done", pay: 8.6 },
{ route: "Taco Local → 5 Pine Rd", time: "5:47 PM", status: "done", pay: 12.05 }
];
// ---------- helpers ----------
function $(sel) { return document.querySelector(sel); }
function money(n) {
return "$" + n.toFixed(2);
}
function moneyShort(n) {
return "$" + (Math.round(n) === n ? n.toFixed(0) : n.toFixed(2));
}
function dayTotal(d) { return d.base + d.tips + d.bonus; }
var toastWrap = $("#toastWrap");
function toast(msg, kind) {
var el = document.createElement("div");
el.className = "toast " + (kind || "info");
el.textContent = msg;
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("out");
el.addEventListener("animationend", function () { el.remove(); });
}, 2600);
}
// ---------- render hero ----------
function renderHero(period, picked) {
var base, tips, bonus, trips, hours, sub;
if (period === "day") {
var src = picked || TODAY;
base = src.base; tips = src.tips; bonus = src.bonus;
trips = picked ? Math.round(8 + Math.random() * 0) : TODAY.trips;
// for a picked weekday use a derived trip count
if (picked) trips = Math.max(6, Math.round((base + tips) / 7));
hours = picked ? Math.round(((base + tips) / 18) * 10) / 10 : TODAY.hours;
sub = picked ? picked.day + " · estimated shift" : "Today · " + TODAY.trips + " trips";
} else {
base = WEEK.reduce(function (s, d) { return s + d.base; }, 0);
tips = WEEK.reduce(function (s, d) { return s + d.tips; }, 0);
bonus = WEEK.reduce(function (s, d) { return s + d.bonus; }, 0);
trips = 86;
hours = 38.5;
sub = "Mon – Sun · 7-day total";
}
var total = base + tips + bonus;
$("#heroTotal").textContent = money(total);
$("#heroSub").textContent = sub;
$("#bdBase").textContent = money(base);
$("#bdTips").textContent = money(tips);
$("#bdBonus").textContent = money(bonus);
$("#statTrips").textContent = String(trips);
$("#statHours").textContent = hours + "h";
$("#statRate").textContent = money(hours ? total / hours : 0).replace(".00", "");
return total;
}
// ---------- render chart ----------
var chart = $("#chart");
var max = Math.max.apply(null, WEEK.map(dayTotal));
function buildChart() {
chart.innerHTML = "";
WEEK.forEach(function (d, i) {
var col = document.createElement("button");
col.type = "button";
col.className = "bar-col" + (i === TODAY_INDEX ? " is-today" : "");
col.setAttribute("data-i", i);
col.setAttribute("aria-label", d.day + " " + money(dayTotal(d)));
var track = document.createElement("span");
track.className = "bar-track";
var bar = document.createElement("span");
bar.className = "bar";
bar.style.height = "0%";
track.appendChild(bar);
var label = document.createElement("span");
label.className = "bar-day";
label.textContent = d.day;
col.appendChild(track);
col.appendChild(label);
chart.appendChild(col);
requestAnimationFrame(function () {
bar.style.height = Math.max(6, (dayTotal(d) / max) * 100) + "%";
});
col.addEventListener("click", function () { selectBar(i); });
});
}
function selectBar(i) {
var cols = chart.querySelectorAll(".bar-col");
cols.forEach(function (c, idx) { c.classList.toggle("is-active", idx === i); });
var d = WEEK[i];
$("#chartHint").textContent =
d.day + ": " + money(dayTotal(d)) + " — base " + moneyShort(d.base) +
", tips " + moneyShort(d.tips) + ", bonus " + moneyShort(d.bonus);
// switch hero into day mode for the picked day
setPeriod("day", d);
}
// ---------- render trips ----------
var ICONS = {
done: '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M9.55 17.6 4.4 12.45l1.42-1.42 3.73 3.73 8.23-8.24L19.2 7.94z"/></svg>',
pending: '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 10.6 3.5 2-.9 1.7-4.6-2.7V6h2z"/></svg>',
failed: '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="m13.4 12 4.3-4.3-1.4-1.4L12 10.6 7.7 6.3 6.3 7.7 10.6 12l-4.3 4.3 1.4 1.4 4.3-4.3 4.3 4.3 1.4-1.4z"/></svg>'
};
var STATUS_LABEL = { done: "Delivered", pending: "In progress", failed: "Failed" };
function renderTrips() {
var list = $("#trips");
list.innerHTML = "";
TRIPS.forEach(function (t) {
var li = document.createElement("li");
li.className = "trip";
li.innerHTML =
'<span class="trip-ic s-' + t.status + '">' + ICONS[t.status] + "</span>" +
'<div class="trip-main">' +
'<p class="trip-route">' + t.route + "</p>" +
'<p class="trip-meta"><span class="tag tag-' + t.status + '">' +
STATUS_LABEL[t.status] + "</span>" + t.time + "</p>" +
"</div>" +
'<div class="trip-pay' + (t.pay === 0 ? " zero" : "") + '">' +
(t.pay === 0 ? "$0.00<small>no pay</small>" : money(t.pay) + "<small>earned</small>") +
"</div>";
list.appendChild(li);
});
$("#tripCount").textContent = TRIPS.length + " trips";
}
// ---------- cashout ----------
var available = dayTotal(WEEK[TODAY_INDEX]);
var cashedOut = false;
function renderCashout() {
$("#cashoutAmt").textContent = money(available);
var btn = $("#cashoutBtn");
if (cashedOut) {
btn.disabled = true;
btn.innerHTML = "Cashed out";
}
}
$("#cashoutBtn").addEventListener("click", function () {
if (cashedOut) return;
cashedOut = true;
toast("Sent " + money(available) + " to your debit card · arrives ~30 min", "ok");
available = 0;
renderCashout();
});
$("#settingsBtn").addEventListener("click", function () {
toast("Earnings settings — demo only", "info");
});
// ---------- period toggle ----------
var toggleBtns = document.querySelectorAll(".toggle-btn");
function setPeriod(period, picked) {
toggleBtns.forEach(function (b) {
var on = b.getAttribute("data-period") === period && !picked;
b.classList.toggle("is-active", on);
b.setAttribute("aria-selected", on ? "true" : "false");
});
var total = renderHero(period, picked);
$("#weekTotalPill").textContent =
moneyShort(WEEK.reduce(function (s, d) { return s + dayTotal(d); }, 0));
return total;
}
toggleBtns.forEach(function (b) {
b.addEventListener("click", function () {
var p = b.getAttribute("data-period");
// clear bar selection when using the toggle
chart.querySelectorAll(".bar-col").forEach(function (c) { c.classList.remove("is-active"); });
$("#chartHint").textContent = "Tap a bar to see that day.";
setPeriod(p);
});
});
// ---------- init ----------
buildChart();
renderTrips();
setPeriod("day");
renderCashout();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Foodhy Driver — Earnings</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="phone" role="application" aria-label="Driver earnings summary">
<header class="topbar">
<div class="driver">
<span class="avatar" aria-hidden="true">MR</span>
<div class="driver-meta">
<p class="driver-name">Marcus Reyes</p>
<p class="driver-id">Courier · #FH-4821</p>
</div>
</div>
<button class="icon-btn" type="button" aria-label="Driver settings" id="settingsBtn">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4Zm8.94-2a7.9 7.9 0 0 0-.12-1.36l1.86-1.45-1.92-3.32-2.2.88a7.6 7.6 0 0 0-2.35-1.36L13.8 2h-3.6l-.41 2.39a7.6 7.6 0 0 0-2.35 1.36l-2.2-.88L1.32 8.19l1.86 1.45a7.6 7.6 0 0 0 0 2.72L1.32 13.8l1.92 3.32 2.2-.88a7.6 7.6 0 0 0 2.35 1.36L10.2 22h3.6l.41-2.39a7.6 7.6 0 0 0 2.35-1.36l2.2.88 1.92-3.32-1.86-1.45c.07-.45.12-.9.12-1.36Z"/></svg>
</button>
</header>
<main class="content">
<section class="hero" aria-labelledby="heroHeading">
<div class="toggle" role="tablist" aria-label="Earnings period">
<button class="toggle-btn is-active" role="tab" aria-selected="true" data-period="day" type="button">Today</button>
<button class="toggle-btn" role="tab" aria-selected="false" data-period="week" type="button">This week</button>
</div>
<p class="hero-label" id="heroHeading">Net earnings</p>
<p class="hero-total" id="heroTotal">$0.00</p>
<p class="hero-sub" id="heroSub">—</p>
<ul class="breakdown" aria-label="Earnings breakdown">
<li><span class="bd-dot dot-base"></span><span class="bd-k">Base pay</span><span class="bd-v" id="bdBase">$0</span></li>
<li><span class="bd-dot dot-tip"></span><span class="bd-k">Tips</span><span class="bd-v" id="bdTips">$0</span></li>
<li><span class="bd-dot dot-bonus"></span><span class="bd-k">Bonuses</span><span class="bd-v" id="bdBonus">$0</span></li>
</ul>
<div class="stats">
<div class="stat"><p class="stat-v" id="statTrips">0</p><p class="stat-k">Trips</p></div>
<div class="stat"><p class="stat-v" id="statHours">0h</p><p class="stat-k">Online</p></div>
<div class="stat"><p class="stat-v" id="statRate">$0</p><p class="stat-k">Per hr</p></div>
</div>
</section>
<section class="card chart-card" aria-labelledby="chartHeading">
<div class="card-head">
<h2 id="chartHeading">Weekly earnings</h2>
<span class="pill pill-ok" id="weekTotalPill">$0</span>
</div>
<div class="chart" id="chart" role="img" aria-label="Bar chart of earnings per day this week">
<!-- bars injected by JS -->
</div>
<p class="chart-hint" id="chartHint">Tap a bar to see that day.</p>
</section>
<section class="card trips-card" aria-labelledby="tripsHeading">
<div class="card-head">
<h2 id="tripsHeading">Recent trips</h2>
<span class="muted-link" id="tripCount">0 trips</span>
</div>
<ul class="trips" id="trips"></ul>
</section>
<div class="spacer" aria-hidden="true"></div>
</main>
<footer class="cashout-bar">
<div class="cashout-meta">
<p class="cashout-label">Available to cash out</p>
<p class="cashout-amt" id="cashoutAmt">$0.00</p>
</div>
<button class="cashout-btn" type="button" id="cashoutBtn">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M21 7H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1Zm-9 7a2 2 0 1 1 0-4 2 2 0 0 1 0 4Z"/></svg>
Cash out
</button>
</footer>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Driver Earnings
A phone-shaped earnings dashboard for a fictional delivery courier. A vivid orange hero card leads with the big net-earnings figure and a small period toggle: switch between Today and This week to swap the headline number, the base / tips / bonus breakdown, and the trips, hours-online, and dollars-per-hour stat tiles. Everything is driven by a small vanilla-JS data model — no frameworks, no build step.
Below the hero, a seven-day bar chart eases into place on load and highlights the current day. Tap any bar to read that day’s split in the hint line and pull the hero into a single-day view, or hit the toggle to clear the selection and return to the weekly total. A recent-trips list rounds out the screen with delivered, in-progress, and failed runs, each tagged with a coloured status pill and its own pay amount, including a zero-pay failed delivery.
A sticky footer shows the amount available to cash out and a Cash out button that fires a
confirmation toast, drains the balance to zero, and locks itself afterward. Hover, active, and
keyboard-focus states are wired throughout, and all motion respects prefers-reduced-motion.
Illustrative UI only — fictional brand, not a real delivery service.