Auto — Test Drive Booking
A polished test drive booking flow for a dealership, built with plain HTML, CSS and vanilla JavaScript. It pairs a selected vehicle card with stock, VIN and odometer details against a horizontal date picker and a grid of time slots that mark already-booked times as unavailable. Customers enter contact details, confirm a valid license, and submit to a confirmation modal that generates a downloadable calendar invite for Bay 7.
MCP
Code
:root {
--garage: #141518;
--garage-2: #1f2127;
--steel: #5b6470;
--steel-l: #8a929d;
--orange: #ff6a13;
--orange-d: #e2540a;
--orange-50: #fff0e6;
--ink: #16181c;
--ink-2: #3b4049;
--muted: #737a85;
--bg: #f3f4f6;
--surface: #ffffff;
--line: rgba(20, 21, 24, 0.1);
--line-2: rgba(20, 21, 24, 0.18);
--ok: #2f9e6f;
--warn: #e0962a;
--danger: #d4493e;
--waiting: #e0962a;
--inprogress: #2b7fff;
--done: #2f9e6f;
--hold: #d4493e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 18px;
--sh-1: 0 1px 2px rgba(20, 21, 24, 0.06), 0 1px 3px rgba(20, 21, 24, 0.08);
--sh-2: 0 6px 18px rgba(20, 21, 24, 0.1);
--sh-3: 0 18px 50px rgba(20, 21, 24, 0.28);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
[data-tabular] { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
.shell { max-width: 1120px; margin: 0 auto; padding: 22px 20px 56px; }
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 18px;
background: var(--garage);
color: #fff;
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
width: 42px; height: 42px;
display: grid; place-items: center;
border-radius: 11px;
background: linear-gradient(145deg, var(--orange), var(--orange-d));
font-weight: 800; font-size: 16px; letter-spacing: 0.5px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
}
.brand-txt { display: flex; flex-direction: column; line-height: 1.25; }
.brand-txt strong { font-size: 15px; font-weight: 700; }
.brand-txt span { font-size: 12px; color: var(--steel-l); }
.topbar-meta {
display: inline-flex; align-items: center; gap: 8px;
font-size: 12.5px; color: var(--steel-l); font-weight: 500;
}
.topbar-meta .dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--ok); box-shadow: 0 0 0 4px rgba(47, 158, 111, 0.22);
}
/* Layout */
.layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 22px;
margin-top: 22px;
align-items: start;
}
.page-title { margin: 0 0 4px; font-size: 26px; font-weight: 800; letter-spacing: -0.02em; }
.page-sub { margin: 0 0 18px; color: var(--muted); font-size: 14px; max-width: 60ch; }
/* Vehicle card */
.vehicle-card {
display: grid;
grid-template-columns: 220px 1fr;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-1);
margin-bottom: 22px;
}
.vehicle-photo {
position: relative;
background:
radial-gradient(120% 90% at 30% 20%, #2c2f37 0%, transparent 60%),
linear-gradient(160deg, var(--garage-2), var(--garage) 75%);
display: grid; place-items: center;
min-height: 168px;
}
.vehicle-photo::after {
content: "";
position: absolute; left: 12%; right: 12%; bottom: 22px; height: 10px;
background: radial-gradient(closest-side, rgba(0,0,0,0.5), transparent);
filter: blur(2px);
}
.vehicle-silhouette {
color: var(--orange);
font-size: 30px;
letter-spacing: 6px;
opacity: 0.85;
text-shadow: 0 6px 16px rgba(255, 106, 19, 0.4);
}
.vehicle-tag {
position: absolute; top: 12px; left: 12px;
font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
background: rgba(255, 106, 19, 0.16); color: #ffb27d;
padding: 4px 8px; border-radius: 999px;
border: 1px solid rgba(255, 106, 19, 0.4);
}
.vehicle-body { padding: 18px 20px; }
.vehicle-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.vehicle-head h2 { margin: 0; font-size: 18px; font-weight: 700; }
.vehicle-trim { margin: 2px 0 0; font-size: 13px; color: var(--muted); }
.vehicle-price { text-align: right; }
.price-num { display: block; font-size: 18px; font-weight: 800; color: var(--ink); }
.price-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.spec-grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
margin: 16px 0 10px; padding: 0;
}
.spec-grid div {
background: var(--bg); border: 1px solid var(--line);
border-radius: var(--r-sm); padding: 8px 10px;
}
.spec-grid dt { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.spec-grid dd { margin: 2px 0 0; font-size: 13px; font-weight: 600; color: var(--ink-2); }
.link-btn {
background: none; border: none; padding: 4px 0; cursor: pointer;
color: var(--orange-d); font: inherit; font-size: 13px; font-weight: 600;
}
.link-btn:hover { text-decoration: underline; }
.link-btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 3px; border-radius: 4px; }
/* Form blocks */
.block {
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: var(--surface);
padding: 18px 20px 20px;
margin: 0 0 18px;
box-shadow: var(--sh-1);
}
.block legend {
font-size: 13px; font-weight: 700; color: var(--ink);
padding: 0 8px; margin-left: -4px;
text-transform: uppercase; letter-spacing: 0.04em;
}
.block legend::first-letter { color: var(--orange-d); }
/* Date row */
.date-row { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 4px; }
.date-chip {
flex: 0 0 auto;
min-width: 72px;
border: 1px solid var(--line-2);
background: var(--surface);
border-radius: var(--r-md);
padding: 10px 8px;
text-align: center;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, transform 0.1s;
}
.date-chip:hover { border-color: var(--steel); transform: translateY(-1px); }
.date-chip:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.date-chip .dow { font-size: 11px; color: var(--muted); text-transform: uppercase; font-weight: 600; }
.date-chip .day { font-size: 19px; font-weight: 800; line-height: 1.1; }
.date-chip .mon { font-size: 11px; color: var(--muted); font-weight: 600; }
.date-chip[aria-checked="true"] {
border-color: var(--orange);
background: var(--orange-50);
box-shadow: inset 0 0 0 1px var(--orange);
}
.date-chip[aria-checked="true"] .day { color: var(--orange-d); }
/* Slots */
.slot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(98px, 1fr));
gap: 10px;
}
.slot {
border: 1px solid var(--line-2);
background: var(--surface);
border-radius: var(--r-sm);
padding: 11px 8px;
font-size: 14px; font-weight: 600;
font-variant-numeric: tabular-nums;
cursor: pointer;
color: var(--ink-2);
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.1s;
}
.slot:hover:not(:disabled) { border-color: var(--orange); color: var(--orange-d); transform: translateY(-1px); }
.slot:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.slot[aria-checked="true"] {
background: var(--orange); border-color: var(--orange); color: #fff;
box-shadow: 0 6px 14px rgba(255, 106, 19, 0.35);
}
.slot:disabled { opacity: 0.45; cursor: not-allowed; text-decoration: line-through; }
.hint { margin: 12px 0 0; font-size: 12px; color: var(--muted); }
.err { margin: 8px 0 0; font-size: 12.5px; color: var(--danger); font-weight: 600; }
/* Fields */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field > span { font-size: 12.5px; font-weight: 600; color: var(--ink-2); }
.field input {
font: inherit; font-size: 14px;
padding: 10px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--ink);
transition: border-color 0.15s, box-shadow 0.15s;
}
.field input::placeholder { color: var(--steel-l); }
.field input:focus { outline: none; border-color: var(--orange); box-shadow: 0 0 0 3px rgba(255, 106, 19, 0.18); }
.field input[aria-invalid="true"] { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(212, 73, 62, 0.16); }
.field-err { color: var(--danger); font-size: 12px; font-weight: 600; }
.check {
display: flex; gap: 10px; align-items: flex-start;
margin-top: 16px; font-size: 13.5px; color: var(--ink-2);
cursor: pointer;
}
.check input { width: 18px; height: 18px; margin-top: 1px; accent-color: var(--orange); flex: 0 0 auto; }
.check strong { color: var(--ink); }
/* Actions */
.actions { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.actions-note { font-size: 12.5px; color: var(--muted); }
.btn-primary {
font: inherit; font-weight: 700; font-size: 14.5px;
color: #fff; cursor: pointer;
background: linear-gradient(160deg, var(--orange), var(--orange-d));
border: none; border-radius: var(--r-md);
padding: 12px 22px;
box-shadow: 0 8px 18px rgba(226, 84, 10, 0.32);
transition: transform 0.1s, box-shadow 0.15s, filter 0.15s;
}
.btn-primary:hover { filter: brightness(1.04); box-shadow: 0 10px 22px rgba(226, 84, 10, 0.4); }
.btn-primary:active { transform: translateY(1px); }
.btn-primary:focus-visible { outline: 2px solid var(--garage); outline-offset: 3px; }
.btn-ghost {
font: inherit; font-weight: 600; font-size: 14px;
color: var(--ink-2); cursor: pointer;
background: var(--surface); border: 1px solid var(--line-2);
border-radius: var(--r-md); padding: 12px 20px;
transition: background 0.15s, border-color 0.15s;
}
.btn-ghost:hover { background: var(--bg); border-color: var(--steel); }
.btn-ghost:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
/* Sidebar */
.col-side { position: sticky; top: 18px; }
.summary {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: var(--sh-1);
}
.summary h3 { margin: 0 0 12px; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
.sum-list { list-style: none; margin: 0 0 14px; padding: 0; }
.sum-list li {
display: flex; justify-content: space-between; gap: 10px;
padding: 8px 0; border-bottom: 1px dashed var(--line);
font-size: 13.5px;
}
.sum-list li:last-child { border-bottom: none; }
.sum-list span { color: var(--muted); }
.sum-list strong { color: var(--ink); font-weight: 600; text-align: right; }
.status-panel {
display: flex; gap: 11px; align-items: center;
padding: 12px 14px; border-radius: var(--r-md);
background: var(--garage); color: #fff;
margin-bottom: 16px;
}
.status-panel small { display: block; color: var(--steel-l); font-size: 11.5px; margin-top: 2px; }
.status-panel strong { font-size: 13.5px; }
.status-led {
width: 12px; height: 12px; border-radius: 50%; flex: 0 0 auto;
background: var(--waiting); box-shadow: 0 0 0 4px rgba(224, 150, 42, 0.25);
}
.status-panel[data-state="ready"] .status-led { background: var(--inprogress); box-shadow: 0 0 0 4px rgba(43, 127, 255, 0.25); }
.status-panel[data-state="done"] .status-led { background: var(--done); box-shadow: 0 0 0 4px rgba(47, 158, 111, 0.25); }
.side-tips p { margin: 0 0 6px; font-size: 12.5px; }
.side-tips strong { color: var(--ink); }
.side-tips ul { margin: 0; padding-left: 18px; color: var(--muted); font-size: 12.5px; }
.side-tips li { margin: 2px 0; }
/* Modal */
.modal-wrap {
position: fixed; inset: 0; z-index: 50;
display: grid; place-items: center;
background: rgba(20, 21, 24, 0.55);
backdrop-filter: blur(3px);
padding: 20px;
animation: fade 0.18s ease;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.modal {
width: min(440px, 100%);
background: var(--surface);
border-radius: var(--r-lg);
padding: 26px 26px 22px;
box-shadow: var(--sh-3);
text-align: center;
animation: pop 0.22s cubic-bezier(0.2, 0.8, 0.3, 1);
}
@keyframes pop { from { transform: translateY(10px) scale(0.97); opacity: 0; } to { transform: none; opacity: 1; } }
.modal-icon {
width: 56px; height: 56px; margin: 0 auto 12px;
display: grid; place-items: center; border-radius: 50%;
background: rgba(47, 158, 111, 0.14); color: var(--ok);
font-size: 28px; font-weight: 800;
}
.modal h2 { margin: 0 0 6px; font-size: 21px; font-weight: 800; }
.modal-sub { margin: 0 0 18px; color: var(--muted); font-size: 13.5px; }
.confirm-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
text-align: left; margin: 0 0 20px;
}
.confirm-grid div { background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 9px 11px; }
.confirm-grid dt { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.confirm-grid dd { margin: 3px 0 0; font-size: 13.5px; font-weight: 700; color: var(--ink); }
.modal-actions { display: flex; gap: 10px; }
.modal-actions .btn-primary, .modal-actions .btn-ghost { flex: 1; }
/* Toast */
.toast-host {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
z-index: 60; display: flex; flex-direction: column; gap: 8px; align-items: center;
pointer-events: none;
}
.toast {
background: var(--garage); color: #fff;
padding: 11px 18px; border-radius: 999px;
font-size: 13px; font-weight: 600;
box-shadow: var(--sh-2);
display: inline-flex; align-items: center; gap: 8px;
animation: toastin 0.25s ease;
}
.toast::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--orange); }
@keyframes toastin { from { transform: translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } }
/* Responsive */
@media (max-width: 880px) {
.layout { grid-template-columns: 1fr; }
.col-side { position: static; }
}
@media (max-width: 520px) {
.shell { padding: 16px 14px 44px; }
.topbar { flex-direction: column; align-items: flex-start; gap: 8px; }
.vehicle-card { grid-template-columns: 1fr; }
.vehicle-photo { min-height: 140px; }
.spec-grid { grid-template-columns: 1fr 1fr; }
.grid-2 { grid-template-columns: 1fr; }
.page-title { font-size: 22px; }
.confirm-grid { grid-template-columns: 1fr; }
.modal-actions { flex-direction: column; }
}(function () {
"use strict";
var DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var ALL_SLOTS = ["9:00", "9:45", "10:30", "11:15", "1:00", "1:45", "2:30", "3:15", "4:00", "4:45", "5:30"];
// Deterministic "booked" pattern per date so it feels real but stable.
function bookedFor(date) {
var seed = date.getDate() + date.getMonth() * 3;
var taken = {};
ALL_SLOTS.forEach(function (s, i) {
if ((seed + i * 5) % 7 === 0 || (seed + i * 3) % 11 === 0) taken[s] = true;
});
return taken;
}
var state = { date: null, slot: null };
var dateRow = document.getElementById("dateRow");
var slotGrid = document.getElementById("slotGrid");
var slotErr = document.getElementById("slotErr");
var sumDate = document.getElementById("sumDate");
var sumTime = document.getElementById("sumTime");
var statusPanel = document.getElementById("statusPanel");
var statusLabel = document.getElementById("statusLabel");
var statusSub = document.getElementById("statusSub");
var form = document.getElementById("bookingForm");
function toast(msg) {
var host = document.getElementById("toastHost");
var el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
host.appendChild(el);
setTimeout(function () {
el.style.transition = "opacity .3s, transform .3s";
el.style.opacity = "0";
el.style.transform = "translateY(8px)";
setTimeout(function () { el.remove(); }, 320);
}, 2400);
}
function fmtLong(d) {
return DOW[d.getDay()] + ", " + MON[d.getMonth()] + " " + d.getDate();
}
function meridiem(slot) {
var h = parseInt(slot.split(":")[0], 10);
return h >= 9 && h <= 11 ? slot + " AM" : slot + " PM";
}
// Build 7 upcoming dates (skip same-day, dealership demos start tomorrow)
var dates = [];
(function buildDates() {
var base = new Date();
base.setHours(0, 0, 0, 0);
var added = 0, i = 1;
while (added < 7) {
var d = new Date(base);
d.setDate(base.getDate() + i);
if (d.getDay() !== 0) { dates.push(d); added++; } // closed Sundays
i++;
}
})();
dates.forEach(function (d, idx) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "date-chip";
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", "false");
btn.innerHTML =
'<span class="dow">' + DOW[d.getDay()] + "</span>" +
'<span class="day">' + d.getDate() + "</span>" +
'<span class="mon">' + MON[d.getMonth()] + "</span>";
btn.addEventListener("click", function () { selectDate(idx, btn); });
dateRow.appendChild(btn);
});
function selectDate(idx, btn) {
state.date = dates[idx];
state.slot = null;
Array.prototype.forEach.call(dateRow.children, function (c) {
c.setAttribute("aria-checked", "false");
});
btn.setAttribute("aria-checked", "true");
renderSlots();
sumDate.textContent = fmtLong(state.date);
sumTime.textContent = "—";
updateStatus();
}
function renderSlots() {
slotGrid.innerHTML = "";
var taken = bookedFor(state.date);
ALL_SLOTS.forEach(function (slot) {
var b = document.createElement("button");
b.type = "button";
b.className = "slot";
b.setAttribute("role", "radio");
b.textContent = meridiem(slot);
if (taken[slot]) {
b.disabled = true;
b.setAttribute("aria-disabled", "true");
b.title = "Already booked";
} else {
b.setAttribute("aria-checked", "false");
b.addEventListener("click", function () { selectSlot(slot, b); });
}
slotGrid.appendChild(b);
});
}
function selectSlot(slot, btn) {
state.slot = slot;
slotErr.hidden = true;
Array.prototype.forEach.call(slotGrid.children, function (c) {
if (!c.disabled) c.setAttribute("aria-checked", "false");
});
btn.setAttribute("aria-checked", "true");
sumTime.textContent = meridiem(slot);
updateStatus();
}
function updateStatus() {
if (state.date && state.slot) {
statusPanel.dataset.state = "ready";
statusLabel.textContent = "Ready to confirm";
statusSub.textContent = fmtLong(state.date) + " · " + meridiem(state.slot);
} else {
statusPanel.dataset.state = "waiting";
statusLabel.textContent = "Awaiting confirmation";
statusSub.textContent = state.date
? "Pick an available time slot."
: "Select a date and slot to continue.";
}
}
// Field validation
function validateField(input) {
var v = (input.value || "").trim();
var ok = true;
if (input.id === "email") ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
else if (input.id === "phone") ok = v.replace(/\D/g, "").length >= 7;
else if (input.id === "license") ok = v.length >= 5;
else ok = v.length >= 2;
setFieldError(input, ok);
return ok;
}
function setFieldError(input, ok) {
input.setAttribute("aria-invalid", ok ? "false" : "true");
var err = document.querySelector('.field-err[data-for="' + input.id + '"]');
if (err) err.hidden = ok;
}
["name", "phone", "email", "license"].forEach(function (id) {
var el = document.getElementById(id);
el.addEventListener("blur", function () { validateField(el); });
el.addEventListener("input", function () {
if (el.getAttribute("aria-invalid") === "true") validateField(el);
});
});
document.getElementById("changeVehicle").addEventListener("click", function () {
toast("Only the demo Aurora GT-Line is available today");
});
var lastBooking = null;
form.addEventListener("submit", function (e) {
e.preventDefault();
var valid = true;
if (!state.date || !state.slot) {
slotErr.hidden = false;
valid = false;
}
["name", "phone", "email", "license"].forEach(function (id) {
if (!validateField(document.getElementById(id))) valid = false;
});
var confirm = document.getElementById("licenseConfirm");
var confirmErr = document.querySelector('.field-err[data-for="licenseConfirm"]');
if (!confirm.checked) {
confirmErr.hidden = false;
valid = false;
} else {
confirmErr.hidden = true;
}
if (!valid) {
toast("Please fix the highlighted fields");
var firstBad = form.querySelector('[aria-invalid="true"]') || (!confirm.checked ? confirm : null);
if (firstBad) firstBad.focus();
return;
}
var code = "TD-" + Math.random().toString(36).slice(2, 7).toUpperCase();
lastBooking = {
code: code,
date: state.date,
slot: state.slot,
driver: document.getElementById("name").value.trim()
};
document.getElementById("confCode").textContent = code;
document.getElementById("confWhen").textContent = fmtLong(state.date) + " · " + meridiem(state.slot);
document.getElementById("confDriver").textContent = lastBooking.driver;
statusPanel.dataset.state = "done";
statusLabel.textContent = "Test drive confirmed";
statusSub.textContent = "Confirmation " + code;
openModal();
});
// Modal
var modal = document.getElementById("successModal");
function openModal() {
modal.hidden = false;
document.body.style.overflow = "hidden";
document.getElementById("closeModal").focus();
document.addEventListener("keydown", onKey);
}
function closeModal() {
modal.hidden = true;
document.body.style.overflow = "";
document.removeEventListener("keydown", onKey);
}
function onKey(e) { if (e.key === "Escape") closeModal(); }
document.getElementById("closeModal").addEventListener("click", function () {
closeModal();
toast("Booking saved — see you at Bay 7");
});
modal.addEventListener("click", function (e) { if (e.target === modal) closeModal(); });
// Add to calendar -> generate .ics download
document.getElementById("addCalendar").addEventListener("click", function () {
if (!lastBooking) return;
var d = lastBooking.date;
var hm = lastBooking.slot.split(":");
var hour = parseInt(hm[0], 10);
if (hour < 9) hour += 12; // PM afternoon slots
else if (hour <= 5) hour += 12; // 1:00–5:30 are PM
var min = parseInt(hm[1], 10);
var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), hour, min);
var end = new Date(start.getTime() + 30 * 60000);
function z(n) { return (n < 10 ? "0" : "") + n; }
function ics(dt) {
return dt.getFullYear() + z(dt.getMonth() + 1) + z(dt.getDate()) +
"T" + z(dt.getHours()) + z(dt.getMinutes()) + "00";
}
var body = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Velocity Motors//Test Drive//EN",
"BEGIN:VEVENT",
"UID:" + lastBooking.code + "@velocitymotors.demo",
"DTSTART:" + ics(start),
"DTEND:" + ics(end),
"SUMMARY:Test Drive — Aurora GT-Line (" + lastBooking.code + ")",
"LOCATION:Velocity Motors Lakeside, Bay 7",
"DESCRIPTION:Bring a valid driver's license. Arrive 5 minutes early.",
"END:VEVENT",
"END:VCALENDAR"
].join("\r\n");
var blob = new Blob([body], { type: "text/calendar" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "test-drive-" + lastBooking.code + ".ics";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
toast("Calendar invite downloaded");
});
// Preselect first date for a friendly start
selectDate(0, dateRow.children[0]);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Velocity Motors — Test Drive Booking</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="shell">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">VM</span>
<div class="brand-txt">
<strong>Velocity Motors</strong>
<span>Lakeside Dealership · Bay 7 Test Drive Center</span>
</div>
</div>
<div class="topbar-meta">
<span class="dot" aria-hidden="true"></span>
Booking desk open · closes 7:00 PM
</div>
</header>
<main class="layout">
<!-- Vehicle + form -->
<section class="col-main" aria-label="Test drive booking">
<h1 class="page-title">Book a Test Drive</h1>
<p class="page-sub">Reserve a no-obligation drive. Bring a valid driver’s license. Sessions run ~30 minutes with a product specialist.</p>
<!-- Selected vehicle -->
<article class="vehicle-card" aria-label="Selected vehicle">
<div class="vehicle-photo" role="img" aria-label="2024 Aurora GT-Line in Midnight Steel">
<span class="vehicle-tag">Demo unit</span>
<span class="vehicle-silhouette" aria-hidden="true">●—●</span>
</div>
<div class="vehicle-body">
<div class="vehicle-head">
<div>
<h2>2024 Aurora GT-Line</h2>
<p class="vehicle-trim">2.0T AWD · Midnight Steel · 8-spd</p>
</div>
<div class="vehicle-price">
<span class="price-num" data-tabular>$38,450</span>
<span class="price-label">MSRP</span>
</div>
</div>
<dl class="spec-grid">
<div><dt>Stock</dt><dd data-tabular>#GT-2271</dd></div>
<div><dt>VIN</dt><dd data-tabular>1AUGT···7K2271</dd></div>
<div><dt>Odometer</dt><dd data-tabular>142 mi</dd></div>
<div><dt>Plate</dt><dd data-tabular>DLR·0427</dd></div>
</dl>
<button class="link-btn" type="button" id="changeVehicle">Change vehicle</button>
</div>
</article>
<form id="bookingForm" novalidate>
<!-- Date -->
<fieldset class="block">
<legend>1 · Pick a date</legend>
<div class="date-row" id="dateRow" role="radiogroup" aria-label="Available dates"></div>
</fieldset>
<!-- Time slots -->
<fieldset class="block">
<legend>2 · Choose a time slot</legend>
<div class="slot-grid" id="slotGrid" role="radiogroup" aria-label="Available time slots"></div>
<p class="hint" id="slotHint">Slots update for the date you select. Greyed slots are already booked.</p>
<p class="err" id="slotErr" role="alert" hidden>Please select an available time slot.</p>
</fieldset>
<!-- Contact -->
<fieldset class="block">
<legend>3 · Your details</legend>
<div class="grid-2">
<label class="field">
<span>Full name</span>
<input type="text" id="name" name="name" autocomplete="name" placeholder="Jordan Avery" required />
<small class="field-err" data-for="name" hidden>Enter your full name.</small>
</label>
<label class="field">
<span>Phone</span>
<input type="tel" id="phone" name="phone" autocomplete="tel" placeholder="(555) 018-4420" required />
<small class="field-err" data-for="phone" hidden>Enter a reachable phone number.</small>
</label>
<label class="field">
<span>Email</span>
<input type="email" id="email" name="email" autocomplete="email" placeholder="you@email.com" required />
<small class="field-err" data-for="email" hidden>Enter a valid email address.</small>
</label>
<label class="field">
<span>Driver’s license #</span>
<input type="text" id="license" name="license" inputmode="latin" placeholder="A-2204-8831-09" required />
<small class="field-err" data-for="license" hidden>License number is required to drive.</small>
</label>
</div>
<label class="check">
<input type="checkbox" id="licenseConfirm" required />
<span>I confirm I hold a <strong>valid driver’s license</strong> and will present it on arrival.</span>
</label>
<small class="field-err" data-for="licenseConfirm" hidden>You must confirm a valid license to book.</small>
</fieldset>
<div class="actions">
<button type="submit" class="btn-primary" id="confirmBtn">Confirm test drive</button>
<span class="actions-note">No payment required · Free to cancel</span>
</div>
</form>
</section>
<!-- Summary sidebar -->
<aside class="col-side" aria-label="Booking summary">
<div class="summary">
<h3>Booking summary</h3>
<ul class="sum-list">
<li><span>Vehicle</span><strong>Aurora GT-Line</strong></li>
<li><span>Location</span><strong>Lakeside · Bay 7</strong></li>
<li><span>Date</span><strong id="sumDate">—</strong></li>
<li><span>Time</span><strong id="sumTime" data-tabular>—</strong></li>
<li><span>Duration</span><strong>~30 min</strong></li>
</ul>
<div class="status-panel" id="statusPanel" data-state="waiting">
<span class="status-led" aria-hidden="true"></span>
<div>
<strong id="statusLabel">Awaiting confirmation</strong>
<small id="statusSub">Select a date and slot to continue.</small>
</div>
</div>
<div class="side-tips">
<p><strong>What to bring</strong></p>
<ul>
<li>Valid driver’s license</li>
<li>Proof of insurance (optional)</li>
<li>Arrive 5 min early at Bay 7</li>
</ul>
</div>
</div>
</aside>
</main>
</div>
<!-- Success modal -->
<div class="modal-wrap" id="successModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="successTitle">
<div class="modal-icon" aria-hidden="true">✓</div>
<h2 id="successTitle">Test drive confirmed</h2>
<p class="modal-sub">We’ve reserved the Aurora GT-Line for your slot. A specialist will text you a reminder.</p>
<dl class="confirm-grid">
<div><dt>Confirmation</dt><dd data-tabular id="confCode">—</dd></div>
<div><dt>When</dt><dd id="confWhen">—</dd></div>
<div><dt>Where</dt><dd>Lakeside · Bay 7</dd></div>
<div><dt>Driver</dt><dd id="confDriver">—</dd></div>
</dl>
<div class="modal-actions">
<button type="button" class="btn-primary" id="addCalendar">Add to calendar</button>
<button type="button" class="btn-ghost" id="closeModal">Done</button>
</div>
</div>
</div>
<div class="toast-host" id="toastHost" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Test Drive Booking
An industrial, status-forward booking screen for a fictional dealership. A selected vehicle card leads with a gradient photo placeholder, MSRP in tabular figures, and a spec grid of stock number, VIN, odometer and dealer plate. The booking form steps the customer through a scrollable date picker, a responsive grid of time slots, and the contact details needed to take a car out — name, phone, email and driver’s license number, plus an explicit valid-license confirmation.
Interactions are entirely vanilla JS. Selecting a date repaints the slot grid with a deterministic set of already-booked (disabled, struck-through) times, and picking a slot lights up the sticky summary panel and its status LED as it moves from “Awaiting confirmation” to “Ready to confirm”. Submitting runs inline field validation with accessible error messages; on success a confirmation modal shows a generated booking code and the appointment, and an “Add to calendar” button builds and downloads a real .ics invite for Bay 7. Toasts give lightweight feedback throughout.
The layout is mobile-first responsive down to ~360px, uses the safety-orange-on-garage-black automotive palette, and keeps buttons and inputs keyboard-usable with visible focus rings and WCAG AA contrast.
Illustrative UI only — fictional shop/dealership, not a real service system.