Strony Średni
Hotel Booking — Checkout
Booking checkout page: guest details, arrival time and requests, a card payment form with live number / expiry formatting and inline validation, plus a sticky order summary and a success confirmation state.
Otwórz w Lab
MCP
html css vanilla-js
Targety: JS HTML
Kod
:root {
--navy: #1a2b4a;
--navy-d: #0f1d36;
--navy-2: #2d4570;
--cream: #f7f3eb;
--cream-2: #ece5d4;
--bone: #fbf8f2;
--gold: #c9a649;
--gold-light: #e0c378;
--gold-d: #a88a2e;
--ink: #161e2c;
--ink-2: #2e3a52;
--warm-gray: #6c7280;
--line: rgba(22, 30, 44, 0.1);
--line-strong: rgba(22, 30, 44, 0.18);
--success: #4a7752;
--danger: #b34232;
--font-display: "Cormorant Garamond", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--shadow-1: 0 1px 2px rgba(22, 30, 44, 0.06), 0 2px 8px rgba(22, 30, 44, 0.06);
--shadow-2: 0 12px 36px rgba(15, 29, 54, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
/* ── Topbar ── */
.topbar {
background: var(--navy-d);
color: var(--bone);
padding: 14px 32px;
display: flex;
align-items: center;
gap: 28px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--bone);
}
.brand-mark {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: var(--r-sm);
background: var(--gold);
color: var(--navy-d);
font-family: var(--font-display);
font-weight: 700;
font-size: 1.2rem;
}
.brand-name {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 700;
}
.brand-name em {
font-style: normal;
color: var(--gold-light);
}
.steps {
display: flex;
gap: 8px;
margin-left: auto;
list-style: none;
counter-reset: step;
}
.steps li {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.82rem;
font-weight: 600;
color: rgba(251, 248, 242, 0.5);
}
.steps li::before {
counter-increment: step;
content: counter(step);
width: 22px;
height: 22px;
border-radius: 999px;
display: grid;
place-items: center;
font-size: 0.72rem;
border: 1px solid rgba(251, 248, 242, 0.3);
}
.steps li:not(:last-child)::after {
content: "";
width: 22px;
height: 1px;
background: rgba(251, 248, 242, 0.2);
}
.steps li.done {
color: var(--gold-light);
}
.steps li.done::before {
content: "✓";
background: var(--gold);
color: var(--navy-d);
border-color: var(--gold);
}
.steps li.current {
color: var(--bone);
}
.steps li.current::before {
border-color: var(--bone);
color: var(--bone);
}
.secure {
font-size: 0.8rem;
color: rgba(251, 248, 242, 0.66);
}
/* ── Layout ── */
.checkout {
max-width: 1140px;
margin: 0 auto;
padding: 28px 32px 72px;
display: grid;
grid-template-columns: 1fr 360px;
gap: 30px;
align-items: start;
position: relative;
}
.form {
display: flex;
flex-direction: column;
gap: 18px;
}
/* ── Cards ── */
.card {
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px 24px 24px;
}
.card h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.5rem;
color: var(--navy-d);
margin-bottom: 16px;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 14px;
}
.grid-2 .field {
margin-bottom: 0;
}
.field label {
font-size: 0.78rem;
font-weight: 600;
color: var(--ink-2);
}
.field .opt {
color: var(--warm-gray);
font-weight: 400;
}
.field input,
.field select,
.field textarea {
font-family: inherit;
font-size: 0.92rem;
color: var(--navy-d);
background: var(--cream);
border: 1.5px solid var(--line);
border-radius: var(--r-sm);
padding: 11px 13px;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
width: 100%;
}
.field textarea {
resize: vertical;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
border-color: var(--gold);
box-shadow: 0 0 0 3px rgba(201, 166, 73, 0.16);
}
.field input::placeholder,
.field textarea::placeholder {
color: var(--warm-gray);
}
.err {
font-size: 0.74rem;
color: var(--danger);
font-weight: 600;
display: none;
}
.field.invalid input,
.field.invalid select {
border-color: var(--danger);
}
.field.invalid .err {
display: block;
}
.paybrands {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.pb {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 700;
color: var(--navy-2);
background: var(--cream);
border: 1px solid var(--line);
padding: 5px 10px;
border-radius: var(--r-sm);
}
.agree {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 0.84rem;
color: var(--ink-2);
margin-top: 4px;
cursor: pointer;
}
.agree input {
width: 18px;
height: 18px;
margin-top: 1px;
accent-color: var(--navy);
flex-shrink: 0;
}
.agree a {
color: var(--gold-d);
font-weight: 600;
}
.err-terms {
margin-top: 6px;
}
.err-terms.show {
display: block;
}
/* ── Summary ── */
.summary {
position: sticky;
top: 24px;
}
.summary-card {
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
padding: 20px 22px 24px;
}
.sum-room {
display: flex;
gap: 14px;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid var(--line);
}
.sum-img {
width: 64px;
height: 64px;
border-radius: var(--r-md);
flex-shrink: 0;
background: linear-gradient(135deg, #3a5180, #1a2b4a);
}
.sum-room h3 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.35rem;
color: var(--navy-d);
}
.sum-room p {
font-size: 0.82rem;
color: var(--warm-gray);
}
.sum-meta {
list-style: none;
padding: 14px 0;
border-bottom: 1px solid var(--line);
}
.sum-meta li {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 5px 0;
font-size: 0.84rem;
}
.sum-meta span {
color: var(--warm-gray);
}
.sum-meta strong {
color: var(--navy-d);
font-weight: 600;
text-align: right;
}
.sum-lines {
list-style: none;
padding: 14px 0;
}
.sum-lines li {
display: flex;
justify-content: space-between;
font-size: 0.86rem;
color: var(--ink-2);
padding: 5px 0;
}
.num {
font-family: var(--font-mono);
font-weight: 700;
color: var(--navy-d);
font-variant-numeric: tabular-nums;
}
.num.green {
color: var(--success);
font-family: inherit;
font-weight: 600;
}
.sum-total {
display: flex;
justify-content: space-between;
align-items: baseline;
padding-top: 14px;
border-top: 1px solid var(--line-strong);
}
.sum-total span {
font-size: 0.96rem;
font-weight: 600;
}
.sum-total strong {
font-family: var(--font-display);
font-size: 2.1rem;
color: var(--navy-d);
}
.pay-btn {
width: 100%;
margin-top: 18px;
background: var(--gold);
color: var(--navy-d);
border: none;
font-family: inherit;
font-weight: 700;
font-size: 0.98rem;
padding: 14px;
border-radius: var(--r-md);
cursor: pointer;
}
.pay-btn:hover:not(:disabled) {
background: var(--gold-light);
}
.pay-btn:disabled {
opacity: 0.7;
cursor: default;
}
.sum-fine {
font-size: 0.74rem;
color: var(--warm-gray);
text-align: center;
margin-top: 10px;
}
/* ── Confirmed overlay ── */
.confirmed {
position: fixed;
inset: 0;
background: rgba(15, 29, 54, 0.55);
backdrop-filter: blur(4px);
display: grid;
place-items: center;
z-index: 60;
padding: 24px;
}
.confirmed-card {
background: var(--bone);
border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
padding: 40px 38px;
text-align: center;
max-width: 440px;
width: 100%;
animation: pop 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes pop {
from {
opacity: 0;
transform: translateY(12px) scale(0.97);
}
}
.check-ic {
width: 64px;
height: 64px;
border-radius: 999px;
background: var(--success);
color: var(--bone);
font-size: 2rem;
display: grid;
place-items: center;
margin: 0 auto 18px;
}
.confirmed-card h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 2rem;
color: var(--navy-d);
}
.confirmed-card p {
font-size: 0.92rem;
color: var(--ink-2);
margin-top: 8px;
}
.conf-ref {
background: var(--cream);
border: 1px dashed var(--line-strong);
border-radius: var(--r-md);
padding: 14px;
margin: 20px 0;
}
.conf-ref span {
display: block;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--warm-gray);
font-weight: 600;
}
.conf-ref strong {
font-family: var(--font-mono);
font-size: 1.4rem;
color: var(--navy-d);
letter-spacing: 0.04em;
}
.conf-detail {
font-size: 0.86rem !important;
}
.conf-btn {
margin-top: 8px;
background: var(--navy);
color: var(--bone);
border: none;
font-family: inherit;
font-weight: 600;
font-size: 0.92rem;
padding: 12px 26px;
border-radius: var(--r-md);
cursor: pointer;
}
.conf-btn:hover {
background: var(--navy-d);
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--navy-d);
color: var(--bone);
padding: 12px 22px;
border-radius: 999px;
font-size: 0.88rem;
font-weight: 600;
box-shadow: var(--shadow-2);
z-index: 70;
}
/* ── Responsive ── */
@media (max-width: 940px) {
.checkout {
grid-template-columns: 1fr;
}
.summary {
position: static;
order: -1;
}
}
@media (max-width: 560px) {
.topbar {
padding: 12px 18px;
flex-wrap: wrap;
gap: 14px;
}
.steps {
order: 3;
width: 100%;
margin-left: 0;
justify-content: center;
}
.checkout {
padding: 22px 18px 56px;
}
.grid-2 {
grid-template-columns: 1fr;
}
}
/* ── Honor [hidden] over display (overlay/panel toggle fix) ── */
.confirmed[hidden] {
display: none;
}// ── Input formatting ──────────────────────────────────────────────────────────
const cardnum = document.getElementById("cardnum");
const expiry = document.getElementById("expiry");
const cvc = document.getElementById("cvc");
const phone = document.getElementById("phone");
cardnum.addEventListener("input", () => {
const digits = cardnum.value.replace(/\D/g, "").slice(0, 16);
cardnum.value = digits.replace(/(.{4})/g, "$1 ").trim();
clearError(cardnum);
});
expiry.addEventListener("input", () => {
let d = expiry.value.replace(/\D/g, "").slice(0, 4);
if (d.length >= 3) d = d.slice(0, 2) + "/" + d.slice(2);
expiry.value = d;
clearError(expiry);
});
cvc.addEventListener("input", () => {
cvc.value = cvc.value.replace(/\D/g, "").slice(0, 4);
clearError(cvc);
});
phone.addEventListener("input", () => {
phone.value = phone.value.replace(/[^\d+\s]/g, "");
clearError(phone);
});
// ── Validation ────────────────────────────────────────────────────────────────
function fieldOf(input) {
return input.closest(".field");
}
function clearError(input) {
const f = fieldOf(input);
if (f) f.classList.remove("invalid");
}
function setError(input) {
const f = fieldOf(input);
if (f) f.classList.add("invalid");
}
const validators = {
first: (v) => v.trim().length > 0,
last: (v) => v.trim().length > 0,
email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()),
phone: (v) => v.replace(/\D/g, "").length >= 6,
cardname: (v) => v.trim().length > 1,
cardnum: (v) => v.replace(/\s/g, "").length === 16,
expiry: (v) => /^(0[1-9]|1[0-2])\/\d{2}$/.test(v),
cvc: (v) => /^\d{3,4}$/.test(v),
};
const form = document.getElementById("form");
const termsErr = document.querySelector(".err-terms");
const terms = document.getElementById("terms");
terms.addEventListener("change", () => {
if (terms.checked) termsErr.classList.remove("show");
});
// Clear error as the guest corrects each field.
Object.keys(validators).forEach((name) => {
const el = form.elements[name];
if (el) el.addEventListener("blur", () => validateField(el));
});
function validateField(el) {
const ok = validators[el.name](el.value);
if (ok) clearError(el);
else setError(el);
return ok;
}
// ── Submit → process → confirm ────────────────────────────────────────────────
const payBtn = document.getElementById("payBtn");
const confirmed = document.getElementById("confirmed");
form.addEventListener("submit", (e) => {
e.preventDefault();
let firstInvalid = null;
for (const name of Object.keys(validators)) {
const el = form.elements[name];
if (!validateField(el) && !firstInvalid) firstInvalid = el;
}
let termsOk = terms.checked;
termsErr.classList.toggle("show", !termsOk);
if (firstInvalid || !termsOk) {
(firstInvalid || terms).focus();
showToast("Please fix the highlighted fields.");
return;
}
// Processing state.
payBtn.disabled = true;
payBtn.textContent = "Processing payment…";
setTimeout(() => {
document.getElementById("confEmail").textContent = form.elements.email.value.trim();
document.getElementById("confRef").textContent = "AUR-" + randomRef();
confirmed.hidden = false;
}, 1100);
});
function randomRef() {
// 6-char alphanumeric reference.
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let out = "";
for (let i = 0; i < 6; i++) out += chars[Math.floor(Math.random() * chars.length)];
return out;
}
document.getElementById("confDone").addEventListener("click", () => {
confirmed.hidden = true;
payBtn.disabled = false;
payBtn.textContent = "Confirm & pay €686";
showToast("Opening your booking…");
});
// ── Toast ─────────────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2400);
}<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Checkout · Aurelia Hotels</title>
</head>
<body>
<header class="topbar">
<a class="brand" href="#">
<span class="brand-mark">A</span>
<span class="brand-name">Aurelia <em>Hotels</em></span>
</a>
<ol class="steps">
<li class="done">Search</li>
<li class="done">Room</li>
<li class="current">Checkout</li>
<li>Confirmed</li>
</ol>
<span class="secure">🔒 Secure checkout</span>
</header>
<main class="checkout">
<form class="form" id="form" novalidate>
<section class="card">
<h2>Guest details</h2>
<div class="grid-2">
<div class="field">
<label for="first">First name</label>
<input id="first" name="first" type="text" required />
<span class="err">Please enter your first name.</span>
</div>
<div class="field">
<label for="last">Last name</label>
<input id="last" name="last" type="text" required />
<span class="err">Please enter your last name.</span>
</div>
<div class="field">
<label for="email">Email</label>
<input id="email" name="email" type="email" required placeholder="[email protected]" />
<span class="err">Enter a valid email address.</span>
</div>
<div class="field">
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel" required placeholder="+34 600 000 000" />
<span class="err">Please enter a phone number.</span>
</div>
</div>
</section>
<section class="card">
<h2>Arrival & requests</h2>
<div class="grid-2">
<div class="field">
<label for="arrival">Estimated arrival</label>
<select id="arrival" name="arrival">
<option>Before 15:00 (early)</option>
<option selected>15:00 – 18:00</option>
<option>18:00 – 21:00</option>
<option>After 21:00 (late)</option>
</select>
</div>
<div class="field">
<label for="country">Country / region</label>
<select id="country" name="country">
<option selected>Spain</option>
<option>Portugal</option>
<option>France</option>
<option>United Kingdom</option>
<option>Germany</option>
<option>Other</option>
</select>
</div>
</div>
<div class="field">
<label for="requests">Special requests <span class="opt">(optional)</span></label>
<textarea id="requests" name="requests" rows="3" placeholder="High floor, late check-out, allergies…"></textarea>
</div>
</section>
<section class="card">
<h2>Payment</h2>
<div class="paybrands">
<span class="pb">VISA</span><span class="pb">MC</span><span class="pb">AMEX</span>
</div>
<div class="field">
<label for="cardname">Name on card</label>
<input id="cardname" name="cardname" type="text" required />
<span class="err">Enter the name on your card.</span>
</div>
<div class="field">
<label for="cardnum">Card number</label>
<input id="cardnum" name="cardnum" inputmode="numeric" required placeholder="1234 5678 9012 3456" maxlength="19" />
<span class="err">Enter a valid 16-digit card number.</span>
</div>
<div class="grid-2">
<div class="field">
<label for="expiry">Expiry</label>
<input id="expiry" name="expiry" inputmode="numeric" required placeholder="MM/YY" maxlength="5" />
<span class="err">Use MM/YY.</span>
</div>
<div class="field">
<label for="cvc">CVC</label>
<input id="cvc" name="cvc" inputmode="numeric" required placeholder="123" maxlength="4" />
<span class="err">3–4 digits.</span>
</div>
</div>
<label class="agree">
<input type="checkbox" id="terms" name="terms" required />
<span>I agree to the <a href="#">booking conditions</a> and privacy policy.</span>
</label>
<span class="err err-terms">Please accept the booking conditions.</span>
</section>
</form>
<aside class="summary">
<div class="summary-card" id="summaryCard">
<div class="sum-room">
<div class="sum-img"></div>
<div>
<h3>Deluxe Double</h3>
<p>Aurelia · Madrid</p>
</div>
</div>
<ul class="sum-meta">
<li><span>Check-in</span><strong>Tue 9 Jun · from 15:00</strong></li>
<li><span>Check-out</span><strong>Fri 12 Jun · until 12:00</strong></li>
<li><span>Guests</span><strong>2 guests · 1 room</strong></li>
<li><span>Rate</span><strong>Bed & breakfast</strong></li>
</ul>
<ul class="sum-lines">
<li><span>€206 × 3 nights</span><span class="num">€618</span></li>
<li><span>Breakfast for 2</span><span class="num green">included</span></li>
<li><span>Taxes & fees</span><span class="num">€68</span></li>
</ul>
<div class="sum-total">
<span>Total</span>
<strong>€686</strong>
</div>
<button type="submit" form="form" class="pay-btn" id="payBtn">Confirm & pay €686</button>
<p class="sum-fine">Free cancellation until 7 Jun · no hidden fees.</p>
</div>
</aside>
<div class="confirmed" id="confirmed" hidden>
<div class="confirmed-card">
<span class="check-ic">✓</span>
<h2>Booking confirmed</h2>
<p>A confirmation email is on its way to <strong id="confEmail">your inbox</strong>.</p>
<div class="conf-ref">
<span>Booking reference</span>
<strong id="confRef">AUR-000000</strong>
</div>
<p class="conf-detail">Deluxe Double · Tue 9 Jun → Fri 12 Jun · <strong>€686</strong></p>
<button class="conf-btn" id="confDone">View my booking</button>
</div>
</div>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Booking Checkout
The final step of the booking funnel. The left column collects guest details (name, email, phone), an estimated arrival time and special requests, then card payment with live formatting — card number grouped in fours, expiry as MM/YY, numeric CVC. Inline validation flags missing or malformed fields on submit. The right column is a sticky order summary echoing the room, dates and price breakdown. Confirming runs a short processing state, then swaps in a success panel with a booking reference — all in vanilla JS.