Seiten Medium
Hotel Guest Portal
Logged-in guest dashboard with three tabs — My Stay, Folio, and Requests. Guests can view their room and check-out countdown, browse current charges, and submit live service requests that appear in a status-tracked list.
In Lab öffnen
MCP
html css vanilla-js
Targets: JS HTML
Code
/* ── Design tokens ── */
: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;
--warning: #d99020;
--info: #4a6da0;
--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;
}
html,
body {
height: 100%;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
-webkit-font-smoothing: antialiased;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ── Topbar ── */
.topbar {
position: sticky;
top: 0;
z-index: 50;
background: var(--navy-d);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
height: 56px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
flex-shrink: 0;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--bone);
}
.brand-mark {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--gold);
color: var(--navy-d);
font-family: var(--font-display);
font-size: 1.1rem;
font-weight: 700;
display: grid;
place-items: center;
flex-shrink: 0;
}
.brand-name {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.brand-name em {
font-style: normal;
color: var(--gold-light);
}
.topbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.guest-chip {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 999px;
padding: 5px 12px 5px 5px;
}
.gc-avatar {
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--gold);
color: var(--navy-d);
font-size: 0.7rem;
font-weight: 700;
display: grid;
place-items: center;
}
.gc-name {
font-size: 0.82rem;
font-weight: 500;
color: rgba(251, 248, 242, 0.85);
}
.btn-signout {
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
padding: 6px 14px;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 999px;
color: rgba(251, 248, 242, 0.65);
cursor: pointer;
}
.btn-signout:hover {
background: rgba(255, 255, 255, 0.07);
color: var(--bone);
}
/* ── Shell layout ── */
.shell {
flex: 1;
display: grid;
grid-template-columns: 220px 1fr;
min-height: 0;
}
/* ── Sidebar ── */
.sidebar {
background: var(--navy);
display: flex;
flex-direction: column;
border-right: 1px solid rgba(255, 255, 255, 0.06);
min-height: calc(100vh - 56px);
}
.sb-prop {
padding: 20px 18px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.sb-prop-name {
font-family: var(--font-display);
font-size: 1.05rem;
font-weight: 700;
color: var(--bone);
letter-spacing: 0.01em;
}
.sb-prop-sub {
font-size: 0.75rem;
color: rgba(251, 248, 242, 0.55);
margin-top: 2px;
}
.sb-nav {
padding: 12px 10px;
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.sb-link {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: var(--r-md);
font-family: inherit;
font-size: 0.9rem;
font-weight: 500;
color: rgba(251, 248, 242, 0.65);
background: none;
border: none;
cursor: pointer;
text-align: left;
width: 100%;
position: relative;
}
.sb-link:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--bone);
}
.sb-link.is-active {
background: rgba(201, 166, 73, 0.15);
color: var(--gold-light);
font-weight: 600;
}
.sb-icon {
width: 18px;
text-align: center;
font-size: 0.9rem;
flex-shrink: 0;
}
.sb-badge {
margin-left: auto;
background: var(--gold);
color: var(--navy-d);
font-size: 0.66rem;
font-weight: 700;
border-radius: 999px;
padding: 2px 7px;
min-width: 20px;
text-align: center;
}
.sb-footer {
padding: 16px 18px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.sb-foot-label {
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: rgba(251, 248, 242, 0.45);
font-weight: 700;
margin-bottom: 4px;
}
.sb-countdown {
font-family: var(--font-mono);
font-size: 0.9rem;
font-weight: 700;
color: var(--gold-light);
font-variant-numeric: tabular-nums;
}
/* ── Main area ── */
.main-area {
background: var(--cream);
overflow-y: auto;
min-height: 0;
}
/* ── Tab panels ── */
.tab-panel {
padding: 28px 28px 40px;
max-width: 800px;
}
.panel-head {
margin-bottom: 24px;
}
.panel-head h1 {
font-family: var(--font-display);
font-size: 2rem;
font-weight: 700;
color: var(--navy-d);
letter-spacing: -0.01em;
}
.panel-sub {
font-size: 0.88rem;
color: var(--warm-gray);
margin-top: 4px;
}
/* ── Room hero ── */
.room-hero {
border-radius: var(--r-lg);
overflow: hidden;
background: var(--bone);
border: 1px solid var(--line-strong);
margin-bottom: 20px;
box-shadow: var(--shadow-1);
}
.room-hero-img {
height: 160px;
background: linear-gradient(135deg, var(--navy-2) 0%, var(--navy-d) 100%);
position: relative;
}
.room-hero-img::after {
content: "Room 302 · Deluxe Double";
position: absolute;
bottom: 12px;
left: 16px;
font-family: var(--font-mono);
font-size: 0.72rem;
color: rgba(251, 248, 242, 0.55);
font-weight: 600;
}
.room-hero-info {
padding: 16px 20px 18px;
}
.rh-eyebrow {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--gold-d);
font-weight: 700;
margin-bottom: 4px;
}
.rh-name {
font-family: var(--font-display);
font-size: 1.45rem;
font-weight: 700;
color: var(--navy-d);
}
.rh-specs {
font-size: 0.82rem;
color: var(--warm-gray);
margin-top: 4px;
}
/* ── Facts grid ── */
.facts-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.fact-card {
background: var(--bone);
border: 1px solid var(--line-strong);
border-radius: var(--r-md);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 3px;
box-shadow: var(--shadow-1);
}
.fact-card--countdown {
border-color: var(--gold);
background: linear-gradient(135deg, var(--bone), #fffbf0);
}
.fc-label {
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--warm-gray);
font-weight: 700;
}
.fc-val {
font-size: 1rem;
font-weight: 700;
color: var(--navy-d);
line-height: 1.2;
}
.fc-val.mono {
font-family: var(--font-mono);
font-size: 0.9rem;
font-variant-numeric: tabular-nums;
}
.fc-sub {
font-size: 0.74rem;
color: var(--warm-gray);
}
.fact-card--countdown .fc-val {
color: var(--gold-d);
}
/* ── Digital key ── */
.dkey-card {
display: flex;
align-items: center;
gap: 16px;
background: linear-gradient(135deg, var(--navy) 0%, var(--navy-d) 100%);
border-radius: var(--r-lg);
padding: 18px 20px;
margin-bottom: 20px;
box-shadow: var(--shadow-2);
}
.dkey-icon {
font-size: 2rem;
color: var(--gold);
flex-shrink: 0;
line-height: 1;
}
.dkey-body {
flex: 1;
}
.dkey-body strong {
font-size: 0.94rem;
font-weight: 700;
color: var(--bone);
display: block;
margin-bottom: 3px;
}
.dkey-body p {
font-size: 0.8rem;
color: rgba(251, 248, 242, 0.65);
}
.btn-dkey {
font-family: inherit;
font-size: 0.84rem;
font-weight: 700;
padding: 10px 20px;
background: var(--gold);
color: var(--navy-d);
border: none;
border-radius: 999px;
cursor: pointer;
flex-shrink: 0;
}
.btn-dkey:hover {
background: var(--gold-light);
}
.btn-dkey.is-active {
background: var(--success);
color: #fff;
}
/* ── Info rows ── */
.info-row-block {
background: var(--bone);
border: 1px solid var(--line-strong);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--shadow-1);
}
.info-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 11px 18px;
border-bottom: 1px solid var(--line);
gap: 12px;
font-size: 0.88rem;
}
.info-row:last-child {
border-bottom: none;
}
.ir-label {
color: var(--warm-gray);
font-weight: 500;
}
.ir-val {
font-weight: 500;
color: var(--ink);
text-align: right;
}
.ir-val.mono {
font-family: var(--font-mono);
font-size: 0.84rem;
font-variant-numeric: tabular-nums;
}
.ir-val.strong {
font-weight: 700;
font-size: 1rem;
color: var(--navy-d);
}
/* ── Folio table ── */
.folio-wrap {
background: var(--bone);
border: 1px solid var(--line-strong);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--shadow-1);
margin-bottom: 16px;
overflow-x: auto;
}
.folio-table {
width: 100%;
border-collapse: collapse;
font-size: 0.86rem;
}
.folio-table thead {
background: var(--cream);
border-bottom: 1px solid var(--line-strong);
}
.folio-table th {
padding: 9px 14px;
text-align: left;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--warm-gray);
font-weight: 700;
}
.col-date {
width: 88px;
}
.col-amt {
width: 90px;
text-align: right;
}
.folio-table td {
padding: 9px 14px;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.folio-table tbody tr:last-child td {
border-bottom: none;
}
.td-date {
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--warm-gray);
}
.td-amt {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-weight: 600;
text-align: right;
}
.folio-totals {
background: var(--bone);
border: 1px solid var(--line-strong);
border-radius: var(--r-md);
padding: 12px 18px;
max-width: 280px;
margin-left: auto;
display: flex;
flex-direction: column;
gap: 6px;
box-shadow: var(--shadow-1);
}
.ft-row {
display: flex;
justify-content: space-between;
font-size: 0.86rem;
color: var(--ink-2);
}
.ft-row .mono {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.ft-row--bold {
font-weight: 700;
color: var(--ink);
border-top: 1px solid var(--line-strong);
padding-top: 8px;
margin-top: 4px;
}
.ft-row--balance {
font-size: 1rem;
font-weight: 700;
color: var(--navy-d);
border-top: 2px solid var(--line-strong);
padding-top: 8px;
margin-top: 4px;
}
/* ── Request form ── */
.req-form {
background: var(--bone);
border: 1px solid var(--line-strong);
border-radius: var(--r-lg);
padding: 20px;
margin-bottom: 24px;
box-shadow: var(--shadow-1);
display: flex;
flex-direction: column;
gap: 12px;
}
.req-form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.field span {
font-size: 0.74rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--warm-gray);
}
.field select,
.field textarea {
font-family: inherit;
font-size: 0.9rem;
background: var(--cream);
border: 1px solid var(--line-strong);
border-radius: var(--r-sm);
padding: 9px 12px;
color: var(--ink);
outline: none;
resize: vertical;
}
.field select:focus,
.field textarea:focus {
border-color: var(--gold);
}
.req-form-foot {
display: flex;
justify-content: flex-end;
}
.btn-submit {
font-family: inherit;
font-size: 0.88rem;
font-weight: 700;
padding: 11px 24px;
background: var(--navy);
color: var(--bone);
border: none;
border-radius: 999px;
cursor: pointer;
}
.btn-submit:hover {
background: var(--navy-2);
}
/* ── Request list ── */
.req-list-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--warm-gray);
}
.req-count {
font-weight: 600;
}
.req-empty {
font-size: 0.88rem;
color: var(--warm-gray);
padding: 20px 0;
text-align: center;
}
.req-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.req-item {
background: var(--bone);
border: 1px solid var(--line-strong);
border-radius: var(--r-md);
padding: 14px 16px;
display: flex;
align-items: flex-start;
gap: 12px;
box-shadow: var(--shadow-1);
}
.ri-body {
flex: 1;
min-width: 0;
}
.ri-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--ink);
margin-bottom: 3px;
}
.ri-meta {
font-size: 0.76rem;
color: var(--warm-gray);
}
.ri-notes {
font-size: 0.8rem;
color: var(--ink-2);
margin-top: 4px;
font-style: italic;
}
.status-pill {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 4px 10px;
border-radius: 999px;
cursor: pointer;
border: none;
font-family: inherit;
white-space: nowrap;
flex-shrink: 0;
margin-top: 1px;
}
.pill--pending {
background: rgba(217, 144, 32, 0.14);
color: var(--warning);
}
.pill--progress {
background: rgba(74, 109, 160, 0.14);
color: var(--info);
}
.pill--done {
background: rgba(74, 119, 82, 0.14);
color: var(--success);
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--navy-d);
color: var(--bone);
padding: 11px 22px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: 0 10px 30px rgba(22, 30, 44, 0.25);
z-index: 200;
white-space: nowrap;
}
/* ── Responsive ── */
@media (max-width: 960px) {
.shell {
grid-template-columns: 1fr;
}
.sidebar {
min-height: auto;
flex-direction: row;
align-items: center;
overflow-x: auto;
padding: 0 12px;
gap: 8px;
border-right: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.sb-prop {
display: none;
}
.sb-nav {
flex-direction: row;
padding: 0;
flex: none;
}
.sb-footer {
display: none;
}
.facts-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 560px) {
.topbar {
padding: 0 12px;
}
.gc-name {
display: none;
}
.tab-panel {
padding: 18px 14px 32px;
}
.facts-grid {
grid-template-columns: 1fr 1fr;
}
.req-form-row {
grid-template-columns: 1fr;
}
.folio-totals {
max-width: 100%;
}
}// ── Toast helper ──
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2200);
}
const $ = (id) => document.getElementById(id);
const fmt = (n) =>
"€" + n.toLocaleString("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
// ── Tab switching ──
document.querySelectorAll(".sb-link").forEach((btn) => {
btn.addEventListener("click", () => {
const target = btn.dataset.tab;
// Deactivate all
document.querySelectorAll(".sb-link").forEach((b) => b.classList.remove("is-active"));
document.querySelectorAll(".tab-panel").forEach((p) => {
p.hidden = true;
p.classList.remove("is-active");
});
// Activate target
btn.classList.add("is-active");
const panel = $(`tab-${target}`);
panel.hidden = false;
panel.classList.add("is-active");
});
});
// ── Check-out countdown ──
// Target: Thu 12 Jun 2026 12:00:00 local
const checkoutDate = new Date("2026-06-12T12:00:00");
function updateCountdown() {
const now = new Date();
const diff = checkoutDate - now;
if (diff <= 0) {
$("mainCountdown").textContent = "Check-out now";
$("sideCountdown").textContent = "Now";
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const secs = Math.floor((diff % (1000 * 60)) / 1000);
const main =
days > 0
? `${days}d ${String(hours).padStart(2, "0")}h ${String(mins).padStart(2, "0")}m`
: `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
const side =
days > 0
? `${days}d ${String(hours).padStart(2, "0")}h`
: `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
$("mainCountdown").textContent = main;
$("sideCountdown").textContent = side;
}
updateCountdown();
setInterval(updateCountdown, 1000);
// ── Digital key ──
$("btnDkey").addEventListener("click", () => {
const btn = $("btnDkey");
if (btn.classList.contains("is-active")) {
btn.classList.remove("is-active");
btn.textContent = "Activate key";
showToast("Digital key deactivated.");
} else {
btn.classList.add("is-active");
btn.textContent = "Key active ✓";
showToast("Digital key activated for Room 302.");
}
});
// ── Sign out (mock) ──
document.querySelector(".btn-signout").addEventListener("click", () => {
showToast("Signing out…");
});
// ── Folio data ──
const folioCharges = [
{ date: "09 Jun", desc: "Room 302 — Standard Double", amount: 184.0 },
{ date: "09 Jun", desc: "City tourist tax", amount: 1.65 },
{ date: "09 Jun", desc: "Restaurant — Dinner", amount: 62.4 },
{ date: "10 Jun", desc: "Room 302 — Standard Double", amount: 184.0 },
{ date: "10 Jun", desc: "City tourist tax", amount: 1.65 },
{ date: "10 Jun", desc: "Minibar restock", amount: 18.0 },
{ date: "10 Jun", desc: "Spa — Massage 60 min", amount: 85.0 },
{ date: "11 Jun", desc: "Room 302 — Standard Double", amount: 184.0 },
{ date: "11 Jun", desc: "City tourist tax", amount: 1.65 },
{ date: "11 Jun", desc: "Restaurant — Breakfast", amount: 28.5 },
];
const paymentsApplied = 400.0;
function renderFolio() {
$("folioBody").innerHTML = folioCharges
.map(
(c) => `
<tr>
<td class="td-date">${c.date}</td>
<td>${c.desc}</td>
<td class="td-amt">${fmt(c.amount)}</td>
</tr>
`
)
.join("");
const subtotal = folioCharges.reduce((s, c) => s + c.amount, 0);
const vat = parseFloat((subtotal * 0.1).toFixed(2));
const total = parseFloat((subtotal + vat).toFixed(2));
const balance = parseFloat((total - paymentsApplied).toFixed(2));
$("fSubtotal").textContent = fmt(subtotal);
$("fVat").textContent = fmt(vat);
$("fTotal").textContent = fmt(total);
$("fBalance").textContent = fmt(Math.max(0, balance));
}
renderFolio();
// ── Requests ──
let requests = [];
let nextReqId = 1;
const reqTypeLabels = {
housekeeping: "Housekeeping turndown",
towels: "Extra towels",
"late-checkout": "Late check-out request",
pillows: "Extra pillows",
iron: "Iron & ironing board",
cot: "Baby cot",
taxi: "Taxi arrangement",
other: "Special request",
};
const timeLabels = {
asap: "As soon as possible",
"30min": "Within 30 minutes",
"1h": "Within 1 hour",
evening: "This evening",
tomorrow: "Tomorrow morning",
};
const statusCycle = ["pending", "progress", "done"];
const statusLabel = { pending: "Pending", progress: "In progress", done: "Done" };
const statusClass = { pending: "pill--pending", progress: "pill--progress", done: "pill--done" };
function renderRequests() {
const list = $("reqList");
const empty = $("reqEmpty");
const badge = $("reqBadge");
const count = $("reqCount");
if (requests.length === 0) {
empty.hidden = false;
badge.hidden = true;
count.textContent = "0 requests";
return;
}
empty.hidden = true;
badge.textContent = requests.length;
badge.hidden = false;
count.textContent = `${requests.length} request${requests.length !== 1 ? "s" : ""}`;
// Re-render only the items list portion (preserve empty paragraph)
const existingItems = list.querySelectorAll(".req-item");
existingItems.forEach((el) => el.remove());
requests.forEach((req) => {
const el = document.createElement("div");
el.className = "req-item";
el.dataset.reqId = req.id;
el.innerHTML = `
<div class="ri-body">
<div class="ri-title">${req.title}</div>
<div class="ri-meta">${req.time} · Submitted ${req.submitted}</div>
${req.notes ? `<div class="ri-notes">"${req.notes}"</div>` : ""}
</div>
<button
class="status-pill ${statusClass[req.status]}"
data-req-id="${req.id}"
type="button"
title="Click to update status"
>${statusLabel[req.status]}</button>
`;
list.appendChild(el);
});
}
// ── Advance status on pill click ──
$("reqList").addEventListener("click", (e) => {
const pill = e.target.closest(".status-pill");
if (!pill) return;
const id = parseInt(pill.dataset.reqId, 10);
const req = requests.find((r) => r.id === id);
if (!req) return;
const idx = statusCycle.indexOf(req.status);
req.status = statusCycle[(idx + 1) % statusCycle.length];
renderRequests();
showToast(`Request updated: ${statusLabel[req.status]}`);
});
// ── Submit request ──
$("reqForm").addEventListener("submit", (e) => {
e.preventDefault();
const type = $("reqType").value;
const time = $("reqTime").value;
const notes = $("reqNotes").value.trim();
if (!type) {
showToast("Please select a request type.");
$("reqType").focus();
return;
}
// Build time string for "now"
const now = new Date();
const hhmm = now.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
requests.unshift({
id: nextReqId++,
title: reqTypeLabels[type] || "Special request",
time: timeLabels[time] || time,
submitted: hhmm,
notes,
status: "pending",
});
// Reset form
$("reqType").value = "";
$("reqTime").value = "asap";
$("reqNotes").value = "";
renderRequests();
showToast(`Request submitted: ${reqTypeLabels[type]}`);
});
// ── Late check-out shortcut: switch to Requests tab and pre-select ──
// (wired via a late-checkout option already in the form; no extra wiring needed)
// ── Init ──
renderRequests();<!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>My Stay · Aurelia Hotels</title>
</head>
<body>
<!-- ── Top nav ── -->
<header class="topbar">
<a class="brand" href="#">
<span class="brand-mark">A</span>
<span class="brand-name">Aurelia <em>Hotels</em></span>
</a>
<div class="topbar-right">
<span class="guest-chip">
<span class="gc-avatar">ML</span>
<span class="gc-name">Margaux Lefèvre</span>
</span>
<button class="btn-signout" type="button">Sign out</button>
</div>
</header>
<!-- ── Layout shell ── -->
<div class="shell">
<!-- ── Sidebar ── -->
<aside class="sidebar">
<div class="sb-prop">
<p class="sb-prop-name">Aurelia Madrid</p>
<p class="sb-prop-sub">Gran Vía · Room 302</p>
</div>
<nav class="sb-nav" aria-label="Guest portal">
<button class="sb-link is-active" data-tab="stay" type="button">
<span class="sb-icon">⊞</span> My stay
</button>
<button class="sb-link" data-tab="folio" type="button">
<span class="sb-icon">◈</span> Folio
</button>
<button class="sb-link" data-tab="requests" type="button">
<span class="sb-icon">✉</span> Requests
<span class="sb-badge" id="reqBadge" hidden>0</span>
</button>
</nav>
<div class="sb-footer">
<p class="sb-foot-label">Check-out in</p>
<p class="sb-countdown" id="sideCountdown">—</p>
</div>
</aside>
<!-- ── Main ── -->
<main class="main-area">
<!-- ══ TAB: My Stay ══ -->
<section class="tab-panel is-active" id="tab-stay" aria-label="My stay">
<div class="panel-head">
<h1>My stay</h1>
<p class="panel-sub">Everything about your current reservation.</p>
</div>
<!-- Room hero -->
<div class="room-hero">
<div class="room-hero-img" aria-hidden="true"></div>
<div class="room-hero-info">
<p class="rh-eyebrow">Deluxe collection</p>
<h2 class="rh-name">Deluxe Double — City View</h2>
<p class="rh-specs">Room 302 · 28m² · King bed · Sleeps 2 · 3rd floor</p>
</div>
</div>
<!-- Stay facts grid -->
<div class="facts-grid">
<div class="fact-card">
<span class="fc-label">Check-in</span>
<span class="fc-val mono">09 Jun 2026</span>
<span class="fc-sub">Arrived 15:22</span>
</div>
<div class="fact-card">
<span class="fc-label">Check-out</span>
<span class="fc-val mono">12 Jun 2026</span>
<span class="fc-sub">By 12:00 noon</span>
</div>
<div class="fact-card">
<span class="fc-label">Duration</span>
<span class="fc-val">3 nights</span>
<span class="fc-sub">2 adults</span>
</div>
<div class="fact-card fact-card--countdown">
<span class="fc-label">Check-out in</span>
<span class="fc-val mono" id="mainCountdown">—</span>
<span class="fc-sub">Thu 12 Jun · 12:00</span>
</div>
</div>
<!-- Digital key teaser -->
<div class="dkey-card">
<div class="dkey-icon">⬡</div>
<div class="dkey-body">
<strong>Digital room key</strong>
<p>Tap to activate your phone as a contactless key for Room 302.</p>
</div>
<button class="btn-dkey" id="btnDkey" type="button">Activate key</button>
</div>
<!-- Rate plan -->
<div class="info-row-block">
<div class="info-row">
<span class="ir-label">Rate plan</span>
<span class="ir-val">Best Available Rate · Non-refundable</span>
</div>
<div class="info-row">
<span class="ir-label">Reservation</span>
<span class="ir-val mono">AH-2026-88341</span>
</div>
<div class="info-row">
<span class="ir-label">Total</span>
<span class="ir-val mono strong">€552.00</span>
</div>
</div>
</section>
<!-- ══ TAB: Folio ══ -->
<section class="tab-panel" id="tab-folio" aria-label="Folio" hidden>
<div class="panel-head">
<h1>Folio</h1>
<p class="panel-sub">Your current charges and balance.</p>
</div>
<div class="folio-wrap">
<table class="folio-table" aria-label="Posted charges">
<thead>
<tr>
<th class="col-date">Date</th>
<th class="col-desc">Description</th>
<th class="col-amt">Amount</th>
</tr>
</thead>
<tbody id="folioBody">
<!-- populated by JS -->
</tbody>
</table>
</div>
<div class="folio-totals">
<div class="ft-row"><span>Subtotal</span><span class="mono" id="fSubtotal">€0.00</span></div>
<div class="ft-row"><span>VAT 10%</span><span class="mono" id="fVat">€0.00</span></div>
<div class="ft-row ft-row--bold"><span>Total incl. VAT</span><span class="mono" id="fTotal">€0.00</span></div>
<div class="ft-row ft-row--balance"><span>Balance due</span><span class="mono" id="fBalance">€0.00</span></div>
</div>
</section>
<!-- ══ TAB: Requests ══ -->
<section class="tab-panel" id="tab-requests" aria-label="Requests" hidden>
<div class="panel-head">
<h1>Service requests</h1>
<p class="panel-sub">Submit a request and track its progress.</p>
</div>
<!-- Request form -->
<form class="req-form" id="reqForm" novalidate>
<div class="req-form-row">
<label class="field">
<span>Request type</span>
<select id="reqType" required aria-required="true">
<option value="">Select…</option>
<option value="housekeeping">Housekeeping turndown</option>
<option value="towels">Extra towels</option>
<option value="late-checkout">Late check-out request</option>
<option value="pillows">Extra pillows</option>
<option value="iron">Iron & ironing board</option>
<option value="cot">Baby cot</option>
<option value="taxi">Taxi arrangement</option>
<option value="other">Other (describe below)</option>
</select>
</label>
<label class="field">
<span>Time preference</span>
<select id="reqTime">
<option value="asap">As soon as possible</option>
<option value="30min">Within 30 minutes</option>
<option value="1h">Within 1 hour</option>
<option value="evening">This evening</option>
<option value="tomorrow">Tomorrow morning</option>
</select>
</label>
</div>
<label class="field">
<span>Notes (optional)</span>
<textarea id="reqNotes" rows="2" placeholder="Any specific details…"></textarea>
</label>
<div class="req-form-foot">
<button class="btn-submit" type="submit">Send request</button>
</div>
</form>
<!-- Request list -->
<div class="req-list-head">
<span>Your requests</span>
<span class="req-count" id="reqCount">0 requests</span>
</div>
<div class="req-list" id="reqList">
<p class="req-empty" id="reqEmpty">No requests yet. Use the form above to get in touch with our team.</p>
</div>
</section>
</main>
</div>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Guest Portal — My Stay Dashboard
A full-page authenticated guest portal with a top navigation bar and a tabbed main area. The “My Stay” tab displays room details, stay dates, a digital-key teaser, and a live countdown to check-out. The “Folio” tab lists all posted charges with a running balance. The “Requests” tab contains a service-request form (housekeeping, extra towels, late checkout, and others) that appends new items to a live request list; each item shows a status pill that can be cycled through pending → in progress → done.