Gym — Member Check-in Kiosk
A full-screen front-desk check-in kiosk for a performance gym, built in a bold neon-on-charcoal theme. A pulsing scan zone and a large numeric member-ID keypad drive the idle state; a simulated scan or entry flips to a kiosk-scale success card showing the member photo, name, membership status, today's booked class, and an auto-reset countdown back to idle. A live checked-in-today counter and a recent check-ins ticker keep the front desk informed.
MCP
Codice
:root {
--bg: #0d0f12;
--surface: #15181d;
--surface-2: #1d2127;
--elevated: #23282f;
--ink: #f4f6f8;
--ink-2: #c2c8d0;
--muted: #8b929c;
--neon: #c6ff3a;
--neon-d: #a6e016;
--neon-50: rgba(198, 255, 58, 0.12);
--orange: #ff6a2b;
--orange-soft: rgba(255, 106, 43, 0.14);
--line: rgba(255, 255, 255, 0.08);
--line-2: rgba(255, 255, 255, 0.16);
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(0, 0, 0, 0.4);
--sh-2: 0 10px 30px rgba(0, 0, 0, 0.45);
--sh-3: 0 24px 70px rgba(0, 0, 0, 0.55);
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
}
body {
background: var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
font-family: inherit;
}
:focus-visible {
outline: 3px solid var(--neon);
outline-offset: 3px;
border-radius: var(--r-sm);
}
/* ---------- Layout shell ---------- */
.kiosk {
position: relative;
min-height: 100vh;
min-height: 100dvh;
display: grid;
grid-template-rows: auto 1fr auto;
overflow: hidden;
padding: clamp(16px, 2.4vw, 34px);
gap: clamp(16px, 2vw, 28px);
}
.glow {
position: absolute;
border-radius: 50%;
filter: blur(90px);
pointer-events: none;
z-index: 0;
}
.glow-a {
width: 520px;
height: 520px;
background: var(--neon-50);
top: -160px;
right: -120px;
}
.glow-b {
width: 460px;
height: 460px;
background: var(--orange-soft);
bottom: -180px;
left: -140px;
}
/* ---------- Topbar ---------- */
.topbar {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.brand {
display: flex;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
}
.brand-mark {
font-size: 1.6rem;
filter: drop-shadow(0 0 10px var(--neon));
}
.brand-name {
font-weight: 900;
font-size: clamp(1.1rem, 1.6vw, 1.5rem);
letter-spacing: 0.14em;
}
.brand-sub {
color: var(--muted);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.18em;
font-weight: 600;
}
.topbar-right {
display: flex;
align-items: center;
gap: 22px;
}
.clock {
text-align: right;
line-height: 1.1;
}
.clock-time {
display: block;
font-weight: 800;
font-size: 1.4rem;
font-variant-numeric: tabular-nums;
}
.clock-date {
color: var(--muted);
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.16em;
font-weight: 600;
}
.status-dot {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 14px;
font-size: 0.78rem;
font-weight: 700;
color: var(--ink-2);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.status-dot .dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.18);
animation: blink 2.4s ease-in-out infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
/* ---------- Stage ---------- */
.stage {
position: relative;
z-index: 1;
display: grid;
place-items: stretch;
}
.panel {
width: 100%;
}
.panel[hidden] {
display: none;
}
/* ---------- Idle view ---------- */
.idle {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: clamp(20px, 3vw, 48px);
align-items: center;
height: 100%;
}
.idle-left {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.eyebrow {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.24em;
font-weight: 800;
font-size: 0.82rem;
color: var(--neon);
}
.headline {
margin: 4px 0 0;
font-weight: 900;
font-size: clamp(2.6rem, 6vw, 5rem);
line-height: 0.98;
letter-spacing: -0.02em;
}
.headline .hl {
color: var(--neon);
text-shadow: 0 0 32px rgba(198, 255, 58, 0.25);
}
.lede {
margin: 14px 0 0;
color: var(--ink-2);
font-size: clamp(1rem, 1.4vw, 1.3rem);
font-weight: 500;
max-width: 34ch;
}
.scanzone {
position: relative;
margin-top: clamp(20px, 3vw, 40px);
align-self: flex-start;
display: grid;
place-items: center;
width: clamp(220px, 24vw, 320px);
height: clamp(220px, 24vw, 320px);
border-radius: var(--r-lg);
border: 2px dashed var(--line-2);
background: radial-gradient(circle at center, var(--neon-50), transparent 70%), var(--surface);
color: var(--neon);
cursor: pointer;
transition: transform 0.18s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.scanzone:hover {
border-color: var(--neon);
transform: translateY(-2px);
box-shadow: var(--sh-2);
}
.scanzone:active {
transform: scale(0.98);
}
.scan-core {
display: grid;
place-items: center;
width: 120px;
height: 120px;
border-radius: var(--r-lg);
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--neon);
}
.scan-label {
position: absolute;
bottom: 22px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.82rem;
color: var(--ink-2);
}
.scan-ring {
position: absolute;
inset: 0;
border-radius: var(--r-lg);
border: 2px solid var(--neon);
opacity: 0;
animation: pulseRing 2.6s ease-out infinite;
}
.scan-ring-2 {
animation-delay: 1.3s;
}
@keyframes pulseRing {
0% { transform: scale(0.92); opacity: 0.6; }
100% { transform: scale(1.18); opacity: 0; }
}
/* ---------- Keypad ---------- */
.idle-right {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: clamp(18px, 2vw, 28px);
box-shadow: var(--sh-2);
display: flex;
flex-direction: column;
gap: 16px;
}
.keypad-head {
display: flex;
flex-direction: column;
gap: 8px;
}
.kp-eyebrow {
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.74rem;
font-weight: 700;
color: var(--muted);
}
.kp-display {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px;
text-align: center;
font-weight: 800;
font-size: 2rem;
letter-spacing: 0.28em;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.keypad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.key {
appearance: none;
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--ink);
font-weight: 800;
font-size: 1.5rem;
padding: 18px 0;
border-radius: var(--r-md);
cursor: pointer;
transition: transform 0.08s ease, background 0.16s ease, border-color 0.16s ease;
}
.key:hover {
background: var(--elevated);
border-color: var(--line-2);
}
.key:active {
transform: scale(0.94);
background: var(--neon-50);
border-color: var(--neon);
}
.key-util {
font-size: 1.05rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-2);
}
.key-enter {
appearance: none;
border: none;
background: var(--neon);
color: #0d0f12;
font-weight: 900;
font-size: 1.2rem;
letter-spacing: 0.04em;
padding: 18px;
border-radius: var(--r-md);
cursor: pointer;
text-transform: uppercase;
transition: transform 0.1s ease, background 0.16s ease, box-shadow 0.2s ease;
box-shadow: 0 8px 24px rgba(198, 255, 58, 0.22);
}
.key-enter:hover {
background: var(--neon-d);
transform: translateY(-2px);
}
.key-enter:active {
transform: translateY(0) scale(0.98);
}
/* ---------- Success view ---------- */
.success {
display: grid;
place-items: center;
height: 100%;
animation: rise 0.45s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes rise {
from { opacity: 0; transform: translateY(24px) scale(0.97); }
to { opacity: 1; transform: none; }
}
.success-card {
width: min(880px, 100%);
display: grid;
grid-template-columns: auto 1fr;
gap: clamp(20px, 3vw, 44px);
align-items: center;
background: linear-gradient(180deg, var(--surface), var(--surface-2));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: clamp(22px, 3vw, 44px);
box-shadow: var(--sh-3);
}
.success-photo {
position: relative;
}
.avatar {
display: grid;
place-items: center;
width: clamp(120px, 14vw, 180px);
height: clamp(120px, 14vw, 180px);
border-radius: var(--r-lg);
background: linear-gradient(140deg, var(--orange), var(--neon));
color: #0d0f12;
font-weight: 900;
font-size: clamp(2.6rem, 4vw, 4rem);
letter-spacing: 0.02em;
}
.check-burst {
position: absolute;
right: -14px;
bottom: -14px;
display: grid;
place-items: center;
width: 64px;
height: 64px;
border-radius: 50%;
background: var(--ok);
color: #07140d;
border: 4px solid var(--surface);
box-shadow: var(--sh-2);
animation: pop 0.5s 0.15s backwards cubic-bezier(0.2, 1.4, 0.4, 1);
}
@keyframes pop {
from { transform: scale(0); }
to { transform: scale(1); }
}
.success-body {
min-width: 0;
}
.success-eyebrow {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.18em;
font-weight: 800;
font-size: 0.8rem;
color: var(--neon);
}
.success-name {
margin: 6px 0 12px;
font-weight: 900;
font-size: clamp(2rem, 4vw, 3.4rem);
line-height: 1;
letter-spacing: -0.02em;
}
.success-meta {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 14px;
border-radius: 999px;
font-weight: 800;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.badge::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.badge.is-active {
background: rgba(52, 211, 153, 0.16);
color: var(--ok);
}
.badge.is-expiring {
background: rgba(251, 191, 36, 0.16);
color: var(--warn);
}
.badge.is-frozen {
background: rgba(96, 165, 250, 0.16);
color: #7cb6ff;
}
.member-id,
.streak {
color: var(--muted);
font-weight: 600;
font-size: 0.92rem;
}
.streak {
color: var(--orange);
font-weight: 700;
}
.success-class {
margin-top: 20px;
display: flex;
align-items: center;
gap: 14px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 18px;
}
.sc-icon {
font-size: 1.4rem;
}
.sc-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.sc-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--muted);
font-weight: 700;
}
.sc-value {
font-size: 1.15rem;
font-weight: 800;
}
.sc-time {
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--neon);
font-size: 1.05rem;
white-space: nowrap;
}
.success-class.is-none {
opacity: 0.7;
}
.success-class.is-none .sc-time {
color: var(--muted);
}
.success-note {
margin: 14px 0 0;
color: var(--ink-2);
font-size: 0.96rem;
min-height: 1.2em;
}
.success-actions {
margin-top: 22px;
display: flex;
align-items: center;
gap: 18px;
}
.btn-secondary {
appearance: none;
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--ink);
font-weight: 800;
font-size: 1rem;
padding: 14px 26px;
border-radius: var(--r-md);
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.06em;
transition: background 0.16s ease, transform 0.1s ease;
}
.btn-secondary:hover {
background: var(--elevated);
}
.btn-secondary:active {
transform: scale(0.97);
}
.autoreset {
color: var(--muted);
font-size: 0.88rem;
font-weight: 600;
}
.autoreset strong {
color: var(--ink);
font-variant-numeric: tabular-nums;
}
/* ---------- Dock / ticker ---------- */
.dock {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: auto 1fr;
gap: clamp(16px, 2.4vw, 36px);
align-items: center;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px clamp(18px, 2.2vw, 28px);
box-shadow: var(--sh-1);
}
.counter {
display: flex;
align-items: baseline;
gap: 10px;
padding-right: clamp(16px, 2vw, 28px);
border-right: 1px solid var(--line);
}
.counter-num {
font-weight: 900;
font-size: clamp(2rem, 3.4vw, 3rem);
color: var(--neon);
font-variant-numeric: tabular-nums;
line-height: 1;
}
.counter-num.bump {
animation: bump 0.4s cubic-bezier(0.2, 1.4, 0.4, 1);
}
@keyframes bump {
0% { transform: scale(1); }
40% { transform: scale(1.22); }
100% { transform: scale(1); }
}
.counter-label {
text-transform: uppercase;
letter-spacing: 0.14em;
font-weight: 700;
font-size: 0.78rem;
color: var(--muted);
max-width: 8ch;
}
.recent {
display: flex;
align-items: center;
gap: 16px;
min-width: 0;
}
.recent-label {
text-transform: uppercase;
letter-spacing: 0.16em;
font-weight: 800;
font-size: 0.74rem;
color: var(--muted);
white-space: nowrap;
}
.recent-list {
list-style: none;
display: flex;
gap: 12px;
margin: 0;
padding: 0;
overflow: hidden;
flex: 1;
min-width: 0;
}
.recent-item {
display: inline-flex;
align-items: center;
gap: 9px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 7px 14px 7px 7px;
white-space: nowrap;
animation: slideIn 0.4s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: none; }
}
.recent-item .ri-avatar {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
font-weight: 800;
font-size: 0.78rem;
color: #0d0f12;
background: linear-gradient(140deg, var(--neon), var(--orange));
}
.recent-item .ri-name {
font-weight: 700;
font-size: 0.92rem;
}
.recent-item .ri-time {
color: var(--muted);
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 140%);
background: var(--elevated);
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 12px 22px;
font-weight: 700;
font-size: 0.95rem;
box-shadow: var(--sh-2);
z-index: 50;
transition: transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
pointer-events: none;
}
.toast.show {
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 980px) {
.idle {
grid-template-columns: 1fr;
align-content: center;
gap: 26px;
}
.scanzone {
align-self: center;
}
.success-card {
grid-template-columns: 1fr;
text-align: center;
justify-items: center;
}
.success-meta,
.success-actions {
justify-content: center;
}
.check-burst {
right: 50%;
transform: translateX(72px);
}
}
@media (max-width: 520px) {
.kiosk {
gap: 14px;
}
.topbar-right {
gap: 12px;
}
.brand-sub {
display: none;
}
.clock-time {
font-size: 1.1rem;
}
.status-dot {
padding: 6px 10px;
font-size: 0.68rem;
}
.key {
padding: 14px 0;
font-size: 1.3rem;
}
.kp-display {
font-size: 1.5rem;
padding: 12px;
}
.dock {
grid-template-columns: 1fr;
gap: 12px;
}
.counter {
border-right: none;
border-bottom: 1px solid var(--line);
padding: 0 0 12px;
justify-content: center;
}
.counter-label {
max-width: none;
}
.recent-label {
display: none;
}
.success-actions {
flex-direction: column;
width: 100%;
}
.btn-secondary {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
/* ---------- Fictional member directory ---------- */
var MEMBERS = {
"1042": {
name: "Marcus Vue",
status: "active",
streak: 14,
class: "Olympic Lifting",
time: "6:30 PM",
},
"2087": {
name: "Priya Nakamura",
status: "active",
streak: 31,
class: "HIIT Inferno",
time: "7:00 PM",
},
"3310": {
name: "Diego Santos",
status: "expiring",
streak: 6,
class: "Mobility & Recovery",
time: "5:45 PM",
},
"4521": {
name: "Aisha Bello",
status: "active",
streak: 58,
class: null,
time: null,
},
"5099": {
name: "Tom Friedrich",
status: "frozen",
streak: 0,
class: null,
time: null,
},
"6678": {
name: "Lena Okafor",
status: "active",
streak: 9,
class: "Spin Sprint",
time: "6:00 PM",
},
};
var MEMBER_IDS = Object.keys(MEMBERS);
var STATUS_META = {
active: {
label: "Active",
cls: "is-active",
note: "Membership in good standing. Have a great session!",
},
expiring: {
label: "Expiring soon",
cls: "is-expiring",
note: "Your membership renews in 4 days — visit the front desk to keep your streak alive.",
},
frozen: {
label: "Frozen",
cls: "is-frozen",
note: "Your membership is currently frozen. Please see a team member to reactivate.",
},
};
/* ---------- Element refs ---------- */
var $ = function (id) {
return document.getElementById(id);
};
var idleView = $("idleView");
var successView = $("successView");
var scanZone = $("scanZone");
var keypad = $("keypad");
var kpDisplay = $("kpDisplay");
var enterBtn = $("enterBtn");
var doneBtn = $("doneBtn");
var toastEl = $("toast");
var recentList = $("recentList");
var todayCountEl = $("todayCount");
var entered = "";
var todayCount = 0;
var recent = [];
var resetTimer = null;
var countdownTimer = null;
var toastTimer = null;
/* ---------- Helpers ---------- */
function initials(name) {
return name
.split(" ")
.map(function (p) {
return p[0];
})
.join("")
.slice(0, 2)
.toUpperCase();
}
function nowTime() {
return new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
}
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2400);
}
/* ---------- Clock ---------- */
function tickClock() {
var d = new Date();
$("clock").textContent = d.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
$("clockDate").textContent = d.toLocaleDateString([], {
weekday: "long",
month: "short",
day: "numeric",
});
}
tickClock();
setInterval(tickClock, 1000 * 15);
/* ---------- Keypad ---------- */
function renderDisplay() {
if (!entered) {
kpDisplay.textContent = "— — — —";
return;
}
kpDisplay.textContent = entered.split("").join(" ");
}
keypad.addEventListener("click", function (e) {
var btn = e.target.closest("button");
if (!btn) return;
if (btn.dataset.act === "clear") {
entered = "";
} else if (btn.dataset.act === "del") {
entered = entered.slice(0, -1);
} else if (btn.dataset.k && entered.length < 4) {
entered += btn.dataset.k;
}
renderDisplay();
});
document.addEventListener("keydown", function (e) {
if (!successView.hidden) {
if (e.key === "Enter" || e.key === "Escape") resetToIdle();
return;
}
if (/^[0-9]$/.test(e.key) && entered.length < 4) {
entered += e.key;
renderDisplay();
} else if (e.key === "Backspace") {
entered = entered.slice(0, -1);
renderDisplay();
} else if (e.key === "Enter") {
submitId();
}
});
enterBtn.addEventListener("click", submitId);
function submitId() {
if (entered.length < 4) {
toast("Enter your 4-digit member ID");
return;
}
var member = MEMBERS[entered];
if (!member) {
toast("Member ID not found — try again or scan your QR");
entered = "";
renderDisplay();
return;
}
checkIn(entered, member);
}
/* ---------- Scan zone (simulated) ---------- */
scanZone.addEventListener("click", function () {
var id = MEMBER_IDS[Math.floor(Math.random() * MEMBER_IDS.length)];
checkIn(id, MEMBERS[id]);
});
/* ---------- Check-in flow ---------- */
function checkIn(id, member) {
var meta = STATUS_META[member.status];
$("memberAvatar").textContent = initials(member.name);
$("memberName").textContent = member.name;
$("memberIdLine").textContent = "ID " + id;
$("checkTime").textContent = "at " + nowTime();
var badge = $("statusBadge");
badge.textContent = meta.label;
badge.className = "badge " + meta.cls;
$("statusNote").textContent = meta.note;
var streakLine = $("streakLine");
if (member.streak > 0) {
streakLine.textContent = "🔥 " + member.streak + " day streak";
streakLine.style.display = "";
} else {
streakLine.style.display = "none";
}
var classBlock = $("classBlock");
if (member.class) {
classBlock.classList.remove("is-none");
$("bookedClass").textContent = member.class;
$("bookedTime").textContent = member.time;
} else {
classBlock.classList.add("is-none");
$("bookedClass").textContent = "No class booked today";
$("bookedTime").textContent = "Drop-in";
}
idleView.hidden = true;
successView.hidden = false;
recordCheckIn(member);
entered = "";
renderDisplay();
startCountdown(8);
if (member.status === "frozen") {
toast("Heads up: membership frozen");
} else {
toast("Welcome back, " + member.name.split(" ")[0] + "!");
}
}
function recordCheckIn(member) {
todayCount += 1;
todayCountEl.textContent = todayCount;
todayCountEl.classList.remove("bump");
void todayCountEl.offsetWidth; // reflow to restart animation
todayCountEl.classList.add("bump");
recent.unshift({ name: member.name, time: nowTime() });
recent = recent.slice(0, 6);
renderRecent();
}
function renderRecent() {
recentList.innerHTML = "";
recent.forEach(function (r) {
var li = document.createElement("li");
li.className = "recent-item";
li.innerHTML =
'<span class="ri-avatar">' +
initials(r.name) +
"</span>" +
'<span class="ri-name">' +
r.name +
"</span>" +
'<span class="ri-time">' +
r.time +
"</span>";
recentList.appendChild(li);
});
}
/* ---------- Auto-reset ---------- */
function startCountdown(seconds) {
clearTimers();
var remaining = seconds;
var cdEl = $("countdown");
cdEl.textContent = remaining;
countdownTimer = setInterval(function () {
remaining -= 1;
cdEl.textContent = Math.max(remaining, 0);
if (remaining <= 0) resetToIdle();
}, 1000);
resetTimer = setTimeout(resetToIdle, seconds * 1000 + 200);
}
function clearTimers() {
clearInterval(countdownTimer);
clearTimeout(resetTimer);
}
function resetToIdle() {
clearTimers();
successView.hidden = true;
idleView.hidden = false;
}
doneBtn.addEventListener("click", resetToIdle);
/* ---------- Seed some recent activity ---------- */
(function seed() {
var seedNames = ["Aisha Bello", "Lena Okafor", "Marcus Vue"];
var t = new Date(Date.now() - 9 * 60000);
seedNames.forEach(function (n) {
t = new Date(t.getTime() + 3 * 60000);
recent.push({
name: n,
time: t.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }),
});
});
recent.reverse();
todayCount = 47;
todayCountEl.textContent = todayCount;
renderRecent();
})();
renderDisplay();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>IRONPULSE — Member Check-in Kiosk</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;900&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="kiosk" id="kiosk">
<!-- Ambient -->
<div class="glow glow-a" aria-hidden="true"></div>
<div class="glow glow-b" aria-hidden="true"></div>
<!-- Top bar -->
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">⚡</span>
<span class="brand-name">IRONPULSE</span>
<span class="brand-sub">Downtown Strength Club</span>
</div>
<div class="topbar-right">
<div class="clock">
<span class="clock-time" id="clock">--:--</span>
<span class="clock-date" id="clockDate">—</span>
</div>
<div class="status-dot" title="System online">
<span class="dot" aria-hidden="true"></span> Online
</div>
</div>
</header>
<main class="stage">
<!-- ============ IDLE STATE ============ -->
<section class="panel idle" id="idleView" aria-label="Check in">
<div class="idle-left">
<p class="eyebrow">Welcome to IRONPULSE</p>
<h1 class="headline">Let's get <span class="hl">moving.</span></h1>
<p class="lede">Scan your membership QR or tap the zone to check in.</p>
<button class="scanzone" id="scanZone" type="button" aria-label="Simulate QR scan to check in">
<span class="scan-ring" aria-hidden="true"></span>
<span class="scan-ring scan-ring-2" aria-hidden="true"></span>
<span class="scan-core">
<svg viewBox="0 0 24 24" width="64" height="64" fill="none" aria-hidden="true">
<path d="M3 7V4a1 1 0 0 1 1-1h3M17 3h3a1 1 0 0 1 1 1v3M21 17v3a1 1 0 0 1-1 1h-3M7 21H4a1 1 0 0 1-1-1v-3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<rect x="7" y="7" width="4" height="4" rx="1" fill="currentColor"/>
<rect x="13" y="7" width="4" height="4" rx="1" fill="currentColor"/>
<rect x="7" y="13" width="4" height="4" rx="1" fill="currentColor"/>
<rect x="13" y="13" width="4" height="4" rx="1" fill="currentColor"/>
</svg>
</span>
<span class="scan-label">Tap to scan</span>
</button>
</div>
<div class="idle-right">
<div class="keypad-head">
<span class="kp-eyebrow">No QR? Enter your member ID</span>
<output class="kp-display" id="kpDisplay" aria-live="polite">— — — —</output>
</div>
<div class="keypad" id="keypad" role="group" aria-label="Numeric keypad">
<button class="key" data-k="1" type="button">1</button>
<button class="key" data-k="2" type="button">2</button>
<button class="key" data-k="3" type="button">3</button>
<button class="key" data-k="4" type="button">4</button>
<button class="key" data-k="5" type="button">5</button>
<button class="key" data-k="6" type="button">6</button>
<button class="key" data-k="7" type="button">7</button>
<button class="key" data-k="8" type="button">8</button>
<button class="key" data-k="9" type="button">9</button>
<button class="key key-util" data-act="clear" type="button">Clear</button>
<button class="key" data-k="0" type="button">0</button>
<button class="key key-util" data-act="del" type="button" aria-label="Delete">⌫</button>
</div>
<button class="key-enter" id="enterBtn" type="button">Check in →</button>
</div>
</section>
<!-- ============ SUCCESS STATE ============ -->
<section class="panel success" id="successView" aria-label="Check-in confirmed" hidden>
<div class="success-card">
<div class="success-photo">
<span class="avatar" id="memberAvatar" aria-hidden="true">--</span>
<span class="check-burst" aria-hidden="true">
<svg viewBox="0 0 24 24" width="40" height="40" fill="none">
<path d="M5 12.5l4.5 4.5L19 7.5" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</div>
<div class="success-body">
<p class="success-eyebrow">Checked in <span id="checkTime">just now</span></p>
<h2 class="success-name" id="memberName">Member</h2>
<div class="success-meta">
<span class="badge" id="statusBadge">Active</span>
<span class="member-id" id="memberIdLine">ID —</span>
<span class="streak" id="streakLine">🔥 — day streak</span>
</div>
<div class="success-class" id="classBlock">
<span class="sc-icon" aria-hidden="true">📋</span>
<div class="sc-text">
<span class="sc-label">Today's booked class</span>
<strong class="sc-value" id="bookedClass">—</strong>
</div>
<span class="sc-time" id="bookedTime">—</span>
</div>
<p class="success-note" id="statusNote"></p>
<div class="success-actions">
<button class="btn-secondary" id="doneBtn" type="button">Done</button>
<span class="autoreset">Auto-reset in <strong id="countdown">8</strong>s</span>
</div>
</div>
</div>
</section>
</main>
<!-- ============ FOOTER TICKER ============ -->
<footer class="dock">
<div class="counter">
<span class="counter-num" id="todayCount">0</span>
<span class="counter-label">checked in today</span>
<span class="counter-spark" id="counterSpark" aria-hidden="true"></span>
</div>
<div class="recent">
<span class="recent-label">Recent</span>
<ul class="recent-list" id="recentList" aria-live="polite"></ul>
</div>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
</div>
<script src="script.js"></script>
</body>
</html>Member Check-in Kiosk
A high-energy, full-screen kiosk for the front desk of a fictional strength club. The idle state pairs a pulsing scan zone — tap it to simulate a QR scan — with a big, tap-friendly numeric keypad for members who enter a four-digit ID instead. A live clock, an online status pill, and an ambient neon glow set the athletic, kiosk-scale tone. Type on the physical keyboard or tap the on-screen keys; the display echoes each digit and Enter submits.
On a successful scan or entry the view rises into a confirmation card with the member’s photo (initials avatar), name, a membership-status badge (Active, Expiring soon, or Frozen), their day streak, and today’s booked class with its time — or a friendly drop-in note when nothing is booked. A status-specific message handles renewals and frozen accounts, and a visible countdown auto-resets the kiosk back to idle after a few seconds (or instantly via Done, Enter, or Escape).
Every check-in bumps a live checked-in-today counter and pushes the member onto a horizontal recent check-ins ticker, so staff get an at-a-glance pulse of traffic. The layout collapses cleanly from a wide two-column kiosk down to a single column at ~360px, honors reduced-motion preferences, and ships as pure vanilla JS with no dependencies.
Illustrative UI only — uses fictional members and a simulated scanner.