Gym — Trainer Dashboard
A high-energy daily console for personal trainers built in vanilla JS. KPI cards summarise sessions, active clients, attendance and weekly revenue; a back-to-back session timeline expands to reveal focus, location, goal and notes with mark-complete and message actions; a searchable client roster shows avatars, next session and colour-coded adherence bars; and a follow-up action list toggles programs and check-ins done with live due counts and toast feedback.
MCP
程式碼
: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 8px 24px rgba(0, 0, 0, 0.45);
--sh-3: 0 18px 48px rgba(0, 0, 0, 0.55);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
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,
input {
font-family: inherit;
}
:focus-visible {
outline: 2px solid var(--neon);
outline-offset: 2px;
border-radius: var(--r-sm);
}
.app {
max-width: 1280px;
margin: 0 auto;
padding: 18px 22px 48px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 0 20px;
border-bottom: 1px solid var(--line);
margin-bottom: 24px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand-mark {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--neon), var(--neon-d));
color: #0b0d10;
font-weight: 900;
letter-spacing: -0.5px;
font-size: 17px;
box-shadow: 0 6px 18px var(--neon-50);
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.1;
}
.brand-name {
font-weight: 800;
font-size: 18px;
letter-spacing: -0.3px;
}
.brand-sub {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--muted);
font-weight: 600;
}
.topbar-right {
display: flex;
align-items: center;
gap: 14px;
}
.date-chip {
display: flex;
flex-direction: column;
align-items: center;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 6px 14px;
line-height: 1.1;
}
.date-day {
font-size: 10px;
letter-spacing: 1.5px;
color: var(--neon);
font-weight: 800;
}
.date-num {
font-size: 14px;
font-weight: 700;
}
.coach {
display: flex;
align-items: center;
gap: 10px;
}
.coach-av {
width: 40px;
height: 40px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--orange-soft);
color: var(--orange);
font-weight: 800;
font-size: 14px;
border: 1px solid rgba(255, 106, 43, 0.3);
}
.coach-meta {
display: flex;
flex-direction: column;
line-height: 1.15;
}
.coach-name {
font-weight: 700;
font-size: 14px;
}
.coach-role {
font-size: 11px;
color: var(--muted);
}
/* ---------- KPIs ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 22px;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 6px;
box-shadow: var(--sh-1);
position: relative;
overflow: hidden;
}
.kpi.accent {
background: linear-gradient(160deg, rgba(198, 255, 58, 0.08), var(--surface) 60%);
border-color: rgba(198, 255, 58, 0.25);
}
.kpi-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--muted);
font-weight: 700;
}
.kpi-value {
font-size: 30px;
font-weight: 900;
letter-spacing: -1px;
}
.kpi-value small {
font-size: 16px;
font-weight: 700;
color: var(--ink-2);
}
.kpi-delta {
font-size: 12px;
font-weight: 600;
color: var(--muted);
}
.kpi-delta.up {
color: var(--ok);
}
/* ---------- Layout grid ---------- */
.grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 18px;
align-items: start;
}
.side {
display: flex;
flex-direction: column;
gap: 18px;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: var(--sh-1);
}
.panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.eyebrow {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--neon);
font-weight: 800;
}
.panel-title {
margin: 2px 0 0;
font-size: 19px;
font-weight: 800;
letter-spacing: -0.4px;
}
.pill {
font-size: 11px;
font-weight: 700;
padding: 5px 10px;
border-radius: 999px;
background: var(--neon-50);
color: var(--neon);
white-space: nowrap;
}
.pill.warn {
background: rgba(251, 191, 36, 0.14);
color: var(--warn);
}
/* ---------- Segmented control ---------- */
.seg {
display: inline-flex;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px;
gap: 2px;
}
.seg-btn {
border: 0;
background: transparent;
color: var(--ink-2);
font-size: 12px;
font-weight: 700;
padding: 6px 12px;
border-radius: 999px;
cursor: pointer;
transition: background 0.16s, color 0.16s;
}
.seg-btn:hover {
color: var(--ink);
}
.seg-btn.is-active {
background: var(--neon);
color: #0b0d10;
}
/* ---------- Timeline ---------- */
.timeline {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.t-item {
display: grid;
grid-template-columns: 64px 1fr;
gap: 14px;
padding: 14px 0;
border-top: 1px solid var(--line);
}
.t-item:first-child {
border-top: 0;
}
.t-item.is-hidden {
display: none;
}
.t-time {
display: flex;
flex-direction: column;
align-items: flex-end;
padding-top: 2px;
}
.t-start {
font-weight: 800;
font-size: 14px;
}
.t-end {
font-size: 11px;
color: var(--muted);
}
.t-card {
background: var(--surface-2);
border: 1px solid var(--line);
border-left: 3px solid var(--type-color, var(--line-2));
border-radius: var(--r-md);
overflow: hidden;
transition: border-color 0.16s, transform 0.12s;
}
.t-card.type-pt {
--type-color: var(--neon);
}
.t-card.type-group {
--type-color: var(--orange);
}
.t-card.type-assessment {
--type-color: #60a5fa;
}
.t-card.is-done {
opacity: 0.62;
}
.t-trigger {
width: 100%;
text-align: left;
background: transparent;
border: 0;
color: inherit;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
}
.t-trigger:hover {
background: rgba(255, 255, 255, 0.025);
}
.t-main {
flex: 1;
min-width: 0;
}
.t-row1 {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.t-title {
font-weight: 700;
font-size: 14.5px;
}
.t-badge {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.6px;
padding: 3px 8px;
border-radius: 999px;
background: var(--type-soft, rgba(255, 255, 255, 0.06));
color: var(--type-color);
border: 1px solid currentColor;
}
.t-sub {
font-size: 12.5px;
color: var(--muted);
margin-top: 2px;
}
.t-chev {
color: var(--muted);
transition: transform 0.18s;
flex-shrink: 0;
}
.t-card.is-open .t-chev {
transform: rotate(180deg);
}
.t-detail {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.22s ease;
}
.t-card.is-open .t-detail {
grid-template-rows: 1fr;
}
.t-detail-inner {
overflow: hidden;
padding: 0 14px;
}
.t-card.is-open .t-detail-inner {
padding: 0 14px 14px;
}
.t-meta-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
border-top: 1px solid var(--line);
padding-top: 12px;
}
.t-meta {
display: flex;
flex-direction: column;
gap: 2px;
}
.t-meta dt {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--muted);
font-weight: 700;
margin: 0;
}
.t-meta dd {
margin: 0;
font-size: 13px;
font-weight: 600;
}
.t-notes {
font-size: 13px;
color: var(--ink-2);
margin: 12px 0 0;
}
.t-actions {
display: flex;
gap: 8px;
margin-top: 14px;
flex-wrap: wrap;
}
/* ---------- Buttons ---------- */
.btn {
border: 1px solid var(--line-2);
background: var(--elevated);
color: var(--ink);
font-size: 13px;
font-weight: 700;
padding: 9px 14px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.14s, transform 0.08s, border-color 0.14s;
}
.btn:hover {
border-color: var(--line-2);
background: #2a3038;
}
.btn:active {
transform: translateY(1px);
}
.btn-primary {
background: var(--neon);
border-color: var(--neon);
color: #0b0d10;
}
.btn-primary:hover {
background: var(--neon-d);
border-color: var(--neon-d);
}
.btn-done {
background: var(--ok);
border-color: var(--ok);
color: #062318;
}
/* ---------- Clients ---------- */
.search {
margin-bottom: 12px;
}
.search input {
width: 100%;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 9px 12px;
color: var(--ink);
font-size: 13px;
}
.search input::placeholder {
color: var(--muted);
}
.clients {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
max-height: 360px;
overflow-y: auto;
}
.c-item {
display: flex;
align-items: center;
gap: 11px;
padding: 11px 8px;
border-top: 1px solid var(--line);
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.14s;
}
.c-item:first-child {
border-top: 0;
}
.c-item:hover {
background: var(--surface-2);
}
.c-item.is-hidden {
display: none;
}
.c-av {
width: 38px;
height: 38px;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 800;
font-size: 13px;
flex-shrink: 0;
color: #0b0d10;
}
.c-main {
flex: 1;
min-width: 0;
}
.c-name {
font-weight: 700;
font-size: 13.5px;
}
.c-next {
font-size: 11.5px;
color: var(--muted);
}
.c-adh {
text-align: right;
flex-shrink: 0;
}
.c-adh-val {
font-weight: 800;
font-size: 14px;
}
.c-adh-bar {
width: 54px;
height: 5px;
border-radius: 999px;
background: var(--surface-2);
overflow: hidden;
margin-top: 4px;
}
.c-adh-fill {
height: 100%;
border-radius: 999px;
}
.adh-hi .c-adh-fill {
background: var(--ok);
}
.adh-hi .c-adh-val {
color: var(--ok);
}
.adh-mid .c-adh-fill {
background: var(--warn);
}
.adh-mid .c-adh-val {
color: var(--warn);
}
.adh-lo .c-adh-fill {
background: var(--danger);
}
.adh-lo .c-adh-val {
color: var(--danger);
}
.empty {
text-align: center;
color: var(--muted);
font-size: 13px;
padding: 22px 0;
}
/* ---------- Tasks ---------- */
.tasks {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.task {
display: flex;
align-items: center;
gap: 11px;
padding: 11px 12px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
transition: opacity 0.16s, background 0.16s;
}
.task.is-done {
opacity: 0.5;
}
.task-check {
width: 22px;
height: 22px;
border-radius: 7px;
border: 2px solid var(--line-2);
background: transparent;
cursor: pointer;
flex-shrink: 0;
display: grid;
place-items: center;
transition: background 0.14s, border-color 0.14s;
color: #062318;
}
.task-check:hover {
border-color: var(--neon);
}
.task.is-done .task-check {
background: var(--ok);
border-color: var(--ok);
}
.task-check svg {
opacity: 0;
transition: opacity 0.12s;
}
.task.is-done .task-check svg {
opacity: 1;
}
.task-main {
flex: 1;
min-width: 0;
}
.task-title {
font-weight: 700;
font-size: 13.5px;
}
.task.is-done .task-title {
text-decoration: line-through;
}
.task-sub {
font-size: 11.5px;
color: var(--muted);
}
.task-tag {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.6px;
padding: 3px 8px;
border-radius: 999px;
flex-shrink: 0;
}
.tag-program {
background: var(--neon-50);
color: var(--neon);
}
.tag-checkin {
background: var(--orange-soft);
color: var(--orange);
}
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 50;
width: max-content;
max-width: 92vw;
}
.toast {
background: var(--elevated);
border: 1px solid var(--line-2);
border-left: 3px solid var(--neon);
color: var(--ink);
padding: 12px 16px;
border-radius: var(--r-md);
font-size: 13px;
font-weight: 600;
box-shadow: var(--sh-3);
animation: toast-in 0.25s ease both;
}
.toast.out {
animation: toast-out 0.25s ease forwards;
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(12px);
}
}
@keyframes toast-out {
to {
opacity: 0;
transform: translateY(12px);
}
}
/* ---------- Responsive ---------- */
@media (max-width: 920px) {
.grid {
grid-template-columns: 1fr;
}
.kpis {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 520px) {
.app {
padding: 14px 14px 40px;
}
.topbar {
flex-wrap: wrap;
gap: 12px;
}
.coach-meta {
display: none;
}
.kpis {
gap: 10px;
}
.kpi {
padding: 13px 14px;
}
.kpi-value {
font-size: 24px;
}
.panel {
padding: 14px;
}
.panel-head {
flex-direction: column;
gap: 10px;
}
.seg {
width: 100%;
justify-content: space-between;
}
.seg-btn {
flex: 1;
text-align: center;
}
.t-item {
grid-template-columns: 52px 1fr;
gap: 10px;
}
.t-meta-grid {
grid-template-columns: 1fr 1fr;
}
.clients {
max-height: none;
}
}(function () {
"use strict";
/* ---------- Data ---------- */
var AVATARS = ["#c6ff3a", "#ff6a2b", "#60a5fa", "#34d399", "#fbbf24", "#f472b6", "#a78bfa"];
function initials(name) {
return name
.split(" ")
.map(function (p) { return p[0]; })
.join("")
.slice(0, 2)
.toUpperCase();
}
function avColor(name) {
var sum = 0;
for (var i = 0; i < name.length; i++) sum += name.charCodeAt(i);
return AVATARS[sum % AVATARS.length];
}
var sessions = [
{
id: "s1", start: "06:00", end: "07:00", type: "pt", title: "Strength PT — Daniel Cho",
sub: "1-on-1 · Lower body power", focus: "Back squat, RDL, sled push",
location: "Rack 2", goal: "PR test on squat", notes: "Cue bracing on the descent; tape last set.",
done: false
},
{
id: "s2", start: "07:00", end: "07:45", type: "assessment", title: "Movement Screen — Priya Nair",
sub: "New member · FMS baseline", focus: "Overhead squat, hip mobility",
location: "Assess Bay", goal: "Establish baseline + flags", notes: "Suspected left ankle restriction — note for plan.",
done: false
},
{
id: "s3", start: "08:00", end: "08:50", type: "group", title: "Power Hour — HIIT",
sub: "Group · 12 booked", focus: "Conditioning circuit",
location: "Studio A", goal: "Avg HR zone 4", notes: "Two scaling options on the board for beginners.",
done: false
},
{
id: "s4", start: "09:00", end: "10:00", type: "pt", title: "Hypertrophy PT — Marcus Lin",
sub: "1-on-1 · Push day", focus: "Bench, incline DB, dips",
location: "Rack 1", goal: "Volume +5% vs last week", notes: "He's deloading next week — keep RPE under 8.",
done: false
},
{
id: "s5", start: "10:30", end: "11:15", type: "assessment", title: "Progress Check — Aisha Bello",
sub: "Re-assessment · Week 8", focus: "Body comp + lifts review",
location: "Assess Bay", goal: "Compare to month-1 numbers", notes: "Bring InBody printout; update macros.",
done: false
},
{
id: "s6", start: "12:00", end: "12:50", type: "group", title: "Lunch Express — Mobility",
sub: "Group · 8 booked", focus: "Mobility + core",
location: "Studio B", goal: "Active recovery", notes: "Low-impact only; mats out beforehand.",
done: false
},
{
id: "s7", start: "16:00", end: "17:00", type: "pt", title: "Athletic PT — Sofia Mendes",
sub: "1-on-1 · Speed & agility", focus: "Sprints, plyo, change of direction",
location: "Turf", goal: "10m split improvement", notes: "Long warm-up — she had a tight hamstring Friday.",
done: false
},
{
id: "s8", start: "17:30", end: "18:20", type: "group", title: "Evening Burn — Strength Circuit",
sub: "Group · 15 booked", focus: "Full body strength",
location: "Studio A", goal: "Hit all stations twice", notes: "Cap class at 16 — waitlist has 3.",
done: false
}
];
var clients = [
{ name: "Daniel Cho", next: "Today · 6:00 AM", adh: 96 },
{ name: "Sofia Mendes", next: "Today · 4:00 PM", adh: 91 },
{ name: "Marcus Lin", next: "Today · 9:00 AM", adh: 88 },
{ name: "Aisha Bello", next: "Today · 10:30 AM", adh: 82 },
{ name: "Priya Nair", next: "Today · 7:00 AM", adh: 100 },
{ name: "Tomás Rivera", next: "Wed · 6:30 AM", adh: 74 },
{ name: "Hannah Webb", next: "Thu · 5:30 PM", adh: 67 },
{ name: "Leo Karlsson", next: "Fri · 8:00 AM", adh: 58 },
{ name: "Naomi Park", next: "Sat · 9:00 AM", adh: 93 },
{ name: "Owen Fletcher", next: "No session booked", adh: 41 }
];
var tasks = [
{ id: "t1", title: "Send 4-week strength block", sub: "Daniel Cho", tag: "program", done: false },
{ id: "t2", title: "Weekly check-in message", sub: "Hannah Webb · 5 days overdue", tag: "checkin", done: false },
{ id: "t3", title: "Build mobility program", sub: "Aisha Bello", tag: "program", done: false },
{ id: "t4", title: "Re-engage lapsed member", sub: "Owen Fletcher · 41% adherence", tag: "checkin", done: false },
{ id: "t5", title: "Share nutrition guide", sub: "Priya Nair (new member)", tag: "program", done: false }
];
/* ---------- Toast ---------- */
var toastWrap = document.getElementById("toastWrap");
function toast(msg) {
var el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("out");
el.addEventListener("animationend", function () { el.remove(); });
}, 2600);
}
/* ---------- Timeline ---------- */
var timelineEl = document.getElementById("timeline");
var TYPE_LABEL = { pt: "PT", group: "Group", assessment: "Assess" };
var CHEV = '<svg class="t-chev" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
function renderTimeline() {
timelineEl.innerHTML = "";
sessions.forEach(function (s) {
var li = document.createElement("li");
li.className = "t-item";
li.dataset.type = s.type;
li.innerHTML =
'<div class="t-time"><span class="t-start">' + s.start + '</span><span class="t-end">' + s.end + '</span></div>' +
'<div class="t-card type-' + s.type + (s.done ? " is-done" : "") + '" data-id="' + s.id + '">' +
'<button class="t-trigger" aria-expanded="false">' +
'<div class="t-main">' +
'<div class="t-row1"><span class="t-title">' + s.title + '</span>' +
'<span class="t-badge">' + TYPE_LABEL[s.type] + '</span></div>' +
'<div class="t-sub">' + s.sub + '</div>' +
'</div>' + CHEV +
'</button>' +
'<div class="t-detail"><div class="t-detail-inner">' +
'<dl class="t-meta-grid">' +
'<div class="t-meta"><dt>Focus</dt><dd>' + s.focus + '</dd></div>' +
'<div class="t-meta"><dt>Location</dt><dd>' + s.location + '</dd></div>' +
'<div class="t-meta"><dt>Goal</dt><dd>' + s.goal + '</dd></div>' +
'</dl>' +
'<p class="t-notes">' + s.notes + '</p>' +
'<div class="t-actions">' +
'<button class="btn btn-primary act-done">' + (s.done ? "Mark not done" : "Mark complete") + '</button>' +
'<button class="btn act-msg">Message client</button>' +
'</div>' +
'</div></div>' +
'</div>';
timelineEl.appendChild(li);
});
}
timelineEl.addEventListener("click", function (e) {
var card = e.target.closest(".t-card");
if (!card) return;
var id = card.dataset.id;
var data = sessions.find(function (x) { return x.id === id; });
if (e.target.closest(".act-done")) {
data.done = !data.done;
card.classList.toggle("is-done", data.done);
var btn = card.querySelector(".act-done");
btn.textContent = data.done ? "Mark not done" : "Mark complete";
toast(data.done ? "Session marked complete ✓" : "Session reopened");
return;
}
if (e.target.closest(".act-msg")) {
toast("Message drafted to " + data.title.split("—")[1].trim());
return;
}
if (e.target.closest(".t-trigger")) {
var open = card.classList.toggle("is-open");
card.querySelector(".t-trigger").setAttribute("aria-expanded", open ? "true" : "false");
}
});
/* ---------- Segmented filter ---------- */
var segBtns = document.querySelectorAll(".seg-btn");
segBtns.forEach(function (b) {
b.addEventListener("click", function () {
segBtns.forEach(function (x) {
x.classList.remove("is-active");
x.setAttribute("aria-selected", "false");
});
b.classList.add("is-active");
b.setAttribute("aria-selected", "true");
var f = b.dataset.filter;
var shown = 0;
document.querySelectorAll(".t-item").forEach(function (item) {
var match = f === "all" || item.dataset.type === f;
item.classList.toggle("is-hidden", !match);
if (match) shown++;
});
document.getElementById("kpiSessions").textContent = f === "all" ? sessions.length : shown;
});
});
/* ---------- Clients ---------- */
var clientsEl = document.getElementById("clients");
function adhClass(v) {
return v >= 85 ? "adh-hi" : v >= 65 ? "adh-mid" : "adh-lo";
}
function renderClients() {
clientsEl.innerHTML = "";
clients.forEach(function (c) {
var li = document.createElement("li");
li.className = "c-item " + adhClass(c.adh);
li.dataset.name = c.name.toLowerCase();
li.tabIndex = 0;
li.innerHTML =
'<div class="c-av" style="background:' + avColor(c.name) + '">' + initials(c.name) + '</div>' +
'<div class="c-main"><div class="c-name">' + c.name + '</div>' +
'<div class="c-next">' + c.next + '</div></div>' +
'<div class="c-adh"><span class="c-adh-val">' + c.adh + '%</span>' +
'<div class="c-adh-bar"><div class="c-adh-fill" style="width:' + c.adh + '%"></div></div></div>';
var open = function () { toast(c.name + " · " + c.adh + "% adherence · " + c.next); };
li.addEventListener("click", open);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); }
});
clientsEl.appendChild(li);
});
}
var searchEl = document.getElementById("clientSearch");
searchEl.addEventListener("input", function () {
var q = searchEl.value.trim().toLowerCase();
var shown = 0;
document.querySelectorAll(".c-item").forEach(function (item) {
var match = item.dataset.name.indexOf(q) !== -1;
item.classList.toggle("is-hidden", !match);
if (match) shown++;
});
var existing = clientsEl.querySelector(".empty");
if (shown === 0 && !existing) {
var p = document.createElement("li");
p.className = "empty";
p.textContent = "No clients match “" + searchEl.value + "”";
clientsEl.appendChild(p);
} else if (shown > 0 && existing) {
existing.remove();
}
});
/* ---------- Tasks ---------- */
var tasksEl = document.getElementById("tasks");
var taskCountEl = document.getElementById("taskCount");
var CHECK = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
var TAG_LABEL = { program: "Program", checkin: "Check-in" };
function updateTaskCount() {
var due = tasks.filter(function (t) { return !t.done; }).length;
taskCountEl.textContent = due + " due";
taskCountEl.style.display = due === 0 ? "none" : "";
}
function renderTasks() {
tasksEl.innerHTML = "";
tasks.forEach(function (t) {
var li = document.createElement("li");
li.className = "task" + (t.done ? " is-done" : "");
li.dataset.id = t.id;
li.innerHTML =
'<button class="task-check" aria-pressed="' + t.done + '" aria-label="Mark done">' + CHECK + '</button>' +
'<div class="task-main"><div class="task-title">' + t.title + '</div>' +
'<div class="task-sub">' + t.sub + '</div></div>' +
'<span class="task-tag tag-' + t.tag + '">' + TAG_LABEL[t.tag] + '</span>';
tasksEl.appendChild(li);
});
updateTaskCount();
}
tasksEl.addEventListener("click", function (e) {
var btn = e.target.closest(".task-check");
if (!btn) return;
var li = btn.closest(".task");
var t = tasks.find(function (x) { return x.id === li.dataset.id; });
t.done = !t.done;
li.classList.toggle("is-done", t.done);
btn.setAttribute("aria-pressed", String(t.done));
if (t.done) toast("Done: " + t.title);
updateTaskCount();
});
/* ---------- Boot ---------- */
renderTimeline();
renderClients();
renderTasks();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Trainer Dashboard — Iron Pulse</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="app">
<!-- Topbar -->
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true">IP</div>
<div class="brand-text">
<span class="brand-name">Iron Pulse</span>
<span class="brand-sub">Trainer Console</span>
</div>
</div>
<div class="topbar-right">
<div class="date-chip">
<span class="date-day">MON</span>
<span class="date-num">Jun 8</span>
</div>
<div class="coach">
<div class="coach-av" aria-hidden="true">MR</div>
<div class="coach-meta">
<span class="coach-name">Mara Reyes</span>
<span class="coach-role">Head Coach</span>
</div>
</div>
</div>
</header>
<main class="main">
<!-- KPI row -->
<section class="kpis" aria-label="Today at a glance">
<article class="kpi">
<span class="kpi-label">Sessions today</span>
<strong class="kpi-value" id="kpiSessions">8</strong>
<span class="kpi-delta up">2 back-to-back blocks</span>
</article>
<article class="kpi">
<span class="kpi-label">Active clients</span>
<strong class="kpi-value">27</strong>
<span class="kpi-delta up">+3 this month</span>
</article>
<article class="kpi">
<span class="kpi-label">Attendance rate</span>
<strong class="kpi-value">92<small>%</small></strong>
<span class="kpi-delta up">+4 vs last week</span>
</article>
<article class="kpi accent">
<span class="kpi-label">Revenue this week</span>
<strong class="kpi-value">$4,180</strong>
<span class="kpi-delta up">68% of goal</span>
</article>
</section>
<div class="grid">
<!-- Timeline -->
<section class="panel timeline-panel" aria-label="Today's schedule">
<div class="panel-head">
<div>
<span class="eyebrow">Today</span>
<h2 class="panel-title">Session Timeline</h2>
</div>
<div class="seg" role="tablist" aria-label="Filter sessions">
<button class="seg-btn is-active" data-filter="all" role="tab" aria-selected="true">All</button>
<button class="seg-btn" data-filter="pt" role="tab" aria-selected="false">PT</button>
<button class="seg-btn" data-filter="group" role="tab" aria-selected="false">Group</button>
<button class="seg-btn" data-filter="assessment" role="tab" aria-selected="false">Assess</button>
</div>
</div>
<ol class="timeline" id="timeline"><!-- injected --></ol>
</section>
<!-- Side column -->
<div class="side">
<!-- Clients -->
<section class="panel clients-panel" aria-label="Your clients">
<div class="panel-head">
<div>
<span class="eyebrow">Roster</span>
<h2 class="panel-title">Clients</h2>
</div>
<span class="pill" id="clientCount">27 active</span>
</div>
<div class="search">
<input type="search" id="clientSearch" placeholder="Search clients…" aria-label="Search clients" />
</div>
<ul class="clients" id="clients"><!-- injected --></ul>
</section>
<!-- Action list -->
<section class="panel tasks-panel" aria-label="Action list">
<div class="panel-head">
<div>
<span class="eyebrow">Follow-ups</span>
<h2 class="panel-title">Action List</h2>
</div>
<span class="pill warn" id="taskCount">0 due</span>
</div>
<ul class="tasks" id="tasks"><!-- injected --></ul>
</section>
</div>
</div>
</main>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Trainer Dashboard
A bold, athletic command center for a trainer’s day. Four KPI cards lead the view — sessions today, active clients, attendance rate and revenue this week — with a neon-accented revenue card showing goal progress. Below, a two-column layout pairs a session timeline with a side stack of clients and follow-ups, all in a dark, neon-and-orange performance-gym theme.
The Session Timeline runs back-to-back blocks of PT, group and assessment sessions, each colour-keyed by type. Tap a session to expand its details — focus, location, goal and coaching notes — then mark it complete or draft a message to the client. A segmented filter narrows the list to one session type and live-updates the sessions-today count.
The Clients roster shows avatar initials, next-session text and a colour-coded adherence bar (green / amber / red), with instant search filtering. The Action List tracks programs to send and check-ins due; ticking a task strikes it through, fades the row and decrements the due pill. Every interaction surfaces a small toast for feedback.
Illustrative UI only — fictional trainers, clients and data.