anycoder-82086c20 / index.html
cemo's picture
Upload folder using huggingface_hub
bc24ada verified
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>GymFlow — Home</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="color-scheme" content="light dark">
<style>
/* ---------- Design tokens ---------- */
:root {
--brand: #6d5efc;
--brand-2: #22c55e;
--bg: #f7f7fb;
--surface: #ffffff;
--surface-2: #f2f2f7;
--text: #0f172a;
--muted: #64748b;
--border: #e5e7eb;
--ring: rgba(109, 94, 252, 0.35);
--shadow: 0 10px 25px rgba(17, 24, 39, 0.08);
--radius: 14px;
--radius-sm: 10px;
--success: #22c55e;
--danger: #ef4444;
--warning: #f59e0b;
--focus: 2px solid var(--ring);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0b1020;
--surface: #0f152b;
--surface-2: #0c1226;
--text: #e6e8ee;
--muted: #93a0b8;
--border: #1f2940;
--ring: rgba(109, 94, 252, 0.45);
--shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
}
}
/* ---------- Reset ---------- */
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, "Apple Color Emoji", "Segoe UI Emoji";
background: radial-gradient(1200px 600px at 85% -200px, rgba(109, 94, 252, 0.12), transparent 60%), var(--bg);
color: var(--text);
line-height: 1.35;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img,
svg {
display: block;
}
button {
font: inherit;
color: inherit;
}
a {
color: inherit;
text-decoration: none;
}
/* ---------- App layout ---------- */
.app {
max-width: 1100px;
margin: 0 auto;
padding: clamp(14px, 2.4vw, 24px);
display: grid;
gap: 20px;
}
header.appbar {
position: sticky;
top: 0;
z-index: 40;
padding: clamp(8px, 1.8vw, 14px);
border-radius: 16px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0));
backdrop-filter: saturate(1.4) blur(10px);
border: 1px solid var(--border);
box-shadow: var(--shadow);
display: grid;
grid-template-columns: 1fr auto auto;
align-items: center;
gap: 16px;
}
@media (prefers-color-scheme: dark) {
header.appbar {
background: linear-gradient(180deg, rgba(17, 25, 40, 0.65), rgba(17, 25, 40, 0.1));
}
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand-badge {
width: 38px;
height: 38px;
border-radius: 12px;
background: conic-gradient(from 200deg at 50% 50%, #6d5efc, #22c55e 55%, #06b6d4 85%, #6d5efc);
position: relative;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.35), 0 6px 16px rgba(109, 94, 252, 0.4);
display: grid;
place-items: center;
color: white;
font-weight: 700;
letter-spacing: 0.5px;
font-size: 13px;
}
.brand h1 {
font-size: 18px;
margin: 0;
}
.brand small {
color: var(--muted);
display: block;
font-size: 12px;
margin-top: 2px;
}
.brand a.credit {
margin-left: 8px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
color: var(--brand);
border: 1px dashed color-mix(in oklab, var(--brand), transparent 65%);
background: color-mix(in oklab, var(--brand), transparent 92%);
transition: 0.2s ease;
}
.brand a.credit:hover {
transform: translateY(-1px);
}
.search {
display: none;
}
@media (min-width: 760px) {
.search {
display: block;
position: relative;
}
.search input {
width: 300px;
max-width: 46vw;
padding: 10px 14px 10px 36px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--surface);
outline: none;
transition: 0.2s border, 0.2s box-shadow;
}
.search input:focus {
border-color: var(--brand);
box-shadow: 0 0 0 6px var(--ring);
}
.search svg {
position: absolute;
top: 50%;
left: 12px;
transform: translateY(-50%);
width: 16px;
height: 16px;
opacity: 0.7;
}
}
.user {
display: flex;
align-items: center;
gap: 12px;
}
.user .avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #22c55e, #06b6d4);
display: grid;
place-items: center;
color: white;
font-weight: 700;
border: 2px solid rgba(255, 255, 255, 0.7);
}
.user .meta {
display: none;
}
@media (min-width: 740px) {
.user .meta {
display: block;
}
.user .meta .name {
font-weight: 700;
font-size: 14px;
}
.user .meta .plan {
font-size: 12px;
color: var(--muted);
}
}
/* ---------- Navigation ---------- */
nav.tabbar {
position: sticky;
top: calc(56px + clamp(14px, 2.4vw, 24px));
z-index: 30;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.65), rgba(255, 255, 255, 0));
backdrop-filter: saturate(1.2) blur(8px);
border: 1px solid var(--border);
border-radius: 999px;
width: fit-content;
margin: 0 auto;
padding: 6px;
display: flex;
gap: 6px;
}
.tabbar button {
padding: 8px 14px;
border-radius: 999px;
border: 0;
background: transparent;
color: var(--muted);
font-weight: 600;
cursor: pointer;
transition: 0.2s ease background, 0.2s ease color, 0.2s ease transform;
}
.tabbar button.active {
background: var(--brand);
color: white;
box-shadow: 0 6px 18px rgba(109, 94, 252, 0.35);
}
.tabbar button:hover {
transform: translateY(-1px);
}
main.content {
display: grid;
gap: 24px;
}
section.block {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: clamp(14px, 2.2vw, 18px);
box-shadow: var(--shadow);
}
.block h2 {
margin: 0 0 14px 0;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.muted {
color: var(--muted);
}
/* ---------- Activities grid ---------- */
.activities {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
@media (min-width: 620px) {
.activities {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 940px) {
.activities {
grid-template-columns: repeat(4, 1fr);
}
}
.card {
background: linear-gradient(180deg, var(--surface), var(--surface-2));
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 14px;
display: grid;
gap: 10px;
transition: 0.2s transform, 0.2s box-shadow, 0.2s border-color;
position: relative;
isolation: isolate;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.08);
}
.card .row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.pill {
font-size: 12px;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid var(--border);
color: var(--muted);
}
.card .icon {
width: 42px;
height: 42px;
border-radius: 12px;
display: grid;
place-items: center;
font-size: 22px;
background: color-mix(in oklab, var(--brand), transparent 85%);
border: 1px solid color-mix(in oklab, var(--brand), transparent 75%);
}
.card .title {
font-weight: 700;
}
.card .subtitle {
font-size: 12px;
color: var(--muted);
}
.checkin-btn {
margin-top: 4px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--surface);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-weight: 700;
transition: 0.2s ease box-shadow, 0.2s ease background, 0.2s ease color, 0.2s ease border;
}
.checkin-btn:hover {
transform: translateY(-1px);
}
.checkin-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 6px var(--ring);
}
.checkin-btn .dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--muted);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.6);
}
.checkin-btn[aria-pressed="true"] {
background: color-mix(in oklab, var(--brand), transparent 92%);
border-color: color-mix(in oklab, var(--brand), transparent 55%);
color: color-mix(in oklab, var(--brand), black 25%);
}
.checkin-btn[aria-pressed="true"] .dot {
background: var(--brand);
}
.checkin-btn[disabled] {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
/* ---------- Today's classes ---------- */
.classes {
display: grid;
gap: 12px;
}
.class {
display: grid;
gap: 8px;
grid-template-columns: auto 1fr auto;
align-items: center;
padding: 12px;
border: 1px solid var(--border);
border-radius: 12px;
background: linear-gradient(180deg, var(--surface), var(--surface-2));
transition: 0.2s transform, 0.2s box-shadow;
}
.class:hover {
transform: translateY(-1px);
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.07);
}
.class .ico {
width: 42px;
height: 42px;
border-radius: 12px;
display: grid;
place-items: center;
font-size: 20px;
background: color-mix(in oklab, var(--brand), transparent 85%);
border: 1px solid color-mix(in oklab, var(--brand), transparent 75%);
}
.class .name {
font-weight: 800;
}
.class .info {
font-size: 12px;
color: var(--muted);
}
.class .time {
font-size: 12px;
color: var(--muted);
}
.class .act {
display: flex;
align-items: center;
gap: 8px;
}
.tag {
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
border: 1px solid var(--border);
background: var(--surface);
}
.btn {
padding: 8px 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--surface);
font-weight: 700;
cursor: pointer;
transition: 0.2s ease;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.06);
}
.btn.primary {
background: var(--brand);
border-color: color-mix(in oklab, var(--brand), black 10%);
color: white;
box-shadow: 0 10px 22px rgba(109, 94, 252, 0.35);
}
.btn.ghost {
background: transparent;
}
/* ---------- Stats ---------- */
.stats {
display: grid;
gap: 12px;
grid-template-columns: 1fr;
}
@media (min-width: 800px) {
.stats {
grid-template-columns: 1.2fr 1fr;
}
}
.kpis {
display: grid;
gap: 12px;
grid-template-columns: repeat(3, 1fr);
}
.kpi {
background: linear-gradient(180deg, var(--surface), var(--surface-2));
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px;
display: grid;
gap: 6px;
}
.kpi .value {
font-size: 22px;
font-weight: 900;
letter-spacing: 0.2px;
}
.kpi .label {
font-size: 12px;
color: var(--muted);
}
.chart {
background: linear-gradient(180deg, var(--surface), var(--surface-2));
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px;
display: grid;
gap: 12px;
}
.bars {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
align-items: end;
height: 120px;
}
.bar {
--h: 0%;
background: linear-gradient(180deg, color-mix(in oklab, var(--brand), white 15%), var(--brand));
border-radius: 8px 8px 4px 4px;
height: var(--h);
position: relative;
min-height: 4px;
box-shadow: 0 6px 12px rgba(109, 94, 252, 0.25);
}
.bar::after {
content: attr(data-count);
position: absolute;
bottom: calc(var(--h) + 6px);
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: var(--muted);
}
.days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
text-align: center;
font-size: 11px;
color: var(--muted);
}
/* ---------- History ---------- */
.history-list {
display: grid;
gap: 10px;
}
.history-item {
display: grid;
gap: 8px;
grid-template-columns: auto 1fr auto;
align-items: center;
padding: 12px;
border: 1px solid var(--border);
border-radius: 12px;
background: linear-gradient(180deg, var(--surface), var(--surface-2));
}
.badge {
padding: 4px 8px;
font-size: 11px;
border-radius: 999px;
font-weight: 700;
border: 1px solid var(--border);
color: var(--muted);
}
.empty {
padding: 18px;
border: 1px dashed var(--border);
border-radius: 12px;
color: var(--muted);
text-align: center;
}
/* ---------- Footer ---------- */
footer {
text-align: center;
color: var(--muted);
font-size: 12px;
padding: 18px 0 40px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
z-index: 60;
left: 50%;
bottom: 24px;
transform: translateX(-50%) translateY(40px);
background: var(--surface);
border: 1px solid var(--border);
box-shadow: var(--shadow);
color: var(--text);
padding: 10px 14px;
border-radius: 12px;
opacity: 0;
pointer-events: none;
transition: 0.25s ease transform, 0.25s ease opacity;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* ---------- Utilities ---------- */
.spacer {
height: 4px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
</head>
<body>
<div class="app">
<header class="appbar" role="banner">
<div class="brand">
<div class="brand-badge" aria-hidden="true">GF</div>
<div>
<h1>GymFlow</h1>
<small>Your day starts here</small>
<a class="credit" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener">Built
with anycoder</a>
</div>
</div>
<div class="search" role="search">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M21 21l-4.35-4.35m1.35-5.15a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" />
</svg>
<input id="search" type="search" placeholder="Search activities, classes..." aria-label="Search">
</div>
<div class="user" role="navigation" aria-label="User menu">
<div class="avatar" aria-hidden="true">JD</div>
<div class="meta">
<div class="name">Jordan</div>
<div class="plan">Premium</div>
</div>
</div>
</header>
<nav class="tabbar" role="tablist" aria-label="Primary">
<button role="tab" aria-selected="true" class="active" data-view="home">Home</button>
<button role="tab" aria-selected="false" data-view="history">History</button>
</nav>
<main class="content">
<!-- Home View -->
<section id="view-home">
<section class="block" aria-labelledby="quick-checkin-title">
<h2 id="quick-checkin-title">Quick Check-In</h2>
<p class="muted" style="margin: 0 0 12px;">Tap an activity to check in for today. You can uncheck to remove
your check-in.</p>
<div id="activities" class="activities" role="list"></div>
</section>
<section class="block" aria-labelledby="today-classes-title">
<h2 id="today-classes-title">Today's Classes</h2>
<div id="classes" class="classes" role="list"></div>
</section>
<section class="block" aria-labelledby="stats-title">
<h2 id="stats-title">Your Activity</h2>
<div class="stats">
<div class="chart" aria-label="Weekly check-ins">
<div class="muted" style="display:flex;justify-content:space-between;align-items:center;">
<span>Last 7 days</span>
<span id="week-total" style="font-weight:800;"></span>
</div>
<div id="bars" class="bars" aria-hidden="true"></div>
<div class="days">
<div>Mon</div>
<div>Tue</div>
<div>Wed</div>
<div>Thu</div>
<div>Fri</div>
<div>Sat</div>
<div>Sun</div>
</div>
</div>
<div class="kpis" role="list">
<div class="kpi" role="listitem">
<div class="value" id="today-count">0</div>
<div class="label">Check-ins today</div>
</div>
<div class="kpi" role="listitem">
<div class="value" id="streak">0</div>
<div class="label">Current streak (days)</div>
</div>
<div class="kpi" role="listitem">
<div class="value" id="month-total">0</div>
<div class="label">This month</div>
</div>
</div>
</div>
</section>
</section>
<!-- History View -->
<section id="view-history" hidden>
<section class="block" aria-labelledby="history-title">
<h2 id="history-title">Recent Check-Ins</h2>
<div id="history-list" class="history-list"></div>
</section>
</section>
</main>
<footer>
© <span id="year"></span> GymFlow. Move better, feel better.
</footer>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script>
// ---------- Data ----------
const ACTIVITIES = [
{ id: 'crossfit', name: 'CrossFit', icon: '💪', color: '#f97316' },
{ id: 'pilates', name: 'Pilates', icon: '🧘‍♀️', color: '#06b6d4' },
{ id: 'yoga', name: 'Yoga', icon: '🧘', color: '#a78bfa' },
{ id: 'spin', name: 'Spin', icon: '🚴', color: '#22c55e' },
{ id: 'run', name: 'Run', icon: '🏃', color: '#ef4444' },
{ id: 'hiit', name: 'HIIT', icon: '⏱️', color: '#f59e0b' },
{ id: 'strength',name: 'Strength',icon: '🏋️', color: '#64748b' },
{ id: 'mobility',name: 'Mobility',icon: '🦴', color: '#14b8a6' },
];
const SCHEDULE = [
{ id:'c1', t:'06:30', name:'CrossFit: Foundation', instructor:'Sam', icon:'💪' },
{ id:'c2', t:'08:00', name:'Spin: Intervals', instructor:'Maya', icon:'🚴' },
{ id:'c3', t:'12:15', name:'Power Yoga', instructor:'Jess', icon:'🧘' },
{ id:'c4', t:'18:00', name:'Pilates Mat', instructor:'Lena', icon:'🧘‍♀️' },
{ id:'c5', t:'19:15', name:'HIIT Express', instructor:'Rae', icon:'⏱️' },
];
// ---------- Storage ----------
const STORAGE_KEY = 'gymflow_data_v1';
function loadData() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return { checkInsByDate: {}, current: [] };
const data = JSON.parse(raw);
if (!data.checkInsByDate) data.checkInsByDate = {};
if (!Array.isArray(data.current)) data.current = [];
return data;
} catch {
return { checkInsByDate: {}, current: [] };
}
}
function saveData() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.data));
}
function todayKey(d = new Date()) {
const y = d.getFullYear();
const m = String(d.getMonth()+1).padStart(2,'0');
const dd = String(d.getDate()).padStart(2,'0');
return `${y}-${m}-${dd}`;
}
// ---------- State ----------
const state = {
data: loadData(),
view: 'home',
};
// ---------- Utils ----------
function uniq(arr) {
return [...new Set(arr)];
}
function formatTime(date) {
return date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
}
function countToday() {
const key = todayKey();
return (state.data.checkInsByDate[key] || []).length;
}
function listTodaySet() {
const key = todayKey();
return new Set(state.data.checkInsByDate[key] || []);
}
function setToday(arr) {
const key = todayKey();
state.data.checkInsByDate[key] = uniq(arr);
saveData();
}
function addToday(id) {
const key = todayKey();
const s = new Set(state.data.checkInsByDate[key] || []);
s.add(id);
state.data.checkInsByDate[key] = [...s];
saveData();
}
function removeToday(id) {
const key = todayKey();
const arr = state.data.checkInsByDate[key] || [];
state.data.checkInsByDate[key] = arr.filter(x => x !== id);
saveData();
}
function computeStreak() {
// Streak of days with at least 1 check-in, ending today
const map = state.data.checkInsByDate;
const today = new Date();
let streak = 0;
for (let i=0; ; i++) {
const d = new Date(today);
d.setDate(today.getDate() - i);
const k = todayKey(d);
if ((map[k] || []).length > 0) streak++;
else break;
}
return streak;
}
function computeThisMonth() {
const map = state.data.checkInsByDate;
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
let total = 0;
for (const [k, arr] of Object.entries(map)) {
const [yy, mm] = k.split('-').map(Number);
if (yy === y && mm-1 === m) total += (arr || []).length;
}
return total;
}
function last7DaysCounts() {
const out = [];
const map = state.data.checkInsByDate;
const today = new Date();
let max = 0;
for (let i=6; i>=0; i--) {
const d = new Date(today);
d.setDate(today.getDate() - i);
const k = todayKey(d);
const count = (map[k] || []).length;
out.push({ d, k, count });
if (count > max) max = count;
}
return { days: out, max: Math.max(max, 1) };
}
// ---------- UI Builders ----------
function activityCard(a, active) {
const el = document.createElement('article');
el.className = 'card';
el.role = 'listitem';
el.dataset.activityId = a.id;
el.style.setProperty('--accent', a.color);
el.innerHTML = `
<div class="row">
<div class="row" style="gap:10px;">
<div class="icon" aria-hidden="true">${a.icon}</div>
<div>
<div class="title">${a.name}</div>
<div class="subtitle">Check in to track progress</div>
</div>
</div>
<span class="pill">${active ? 'Checked-in' : 'Available'}</span>
</div>
<button class="checkin-btn" aria-pressed="${active ? 'true':'false'}" data-id="${a.id}">
<span class="dot" aria-hidden="true"></span>
<span>${active ? 'Checked-in' : 'Check in'}</span>
</button>
`;
const btn = el.querySelector('button');
btn.addEventListener('click', () => onToggleCheckin(a.id, el));
return el;
}
function classItem(c, checkedForThisClass) {
// Determine activity id that matches closest by name
const actId = (() => {
const lc = c.name.toLowerCase();
if (lc.includes('crossfit')) return 'crossfit';
if (lc.includes('spin')) return 'spin';
if (lc.includes('yoga') || lc.includes('pilates')) return lc.includes('yoga') ? 'yoga' : 'pilates';
if (lc.includes('hiit')) return 'hiit';
return 'strength';
})();
const el = document.createElement('article');
el.className = 'class';
el.role = 'listitem';
el.innerHTML = `
<div class="ico" aria-hidden="true">${c.icon}</div>
<div>
<div class="name">${c.name}</div>
<div class="info">with ${c.instructor}</div>
<div class="time">${c.t}</div>
</div>
<div class="act">
${checkedForThisClass ? `<span class="tag" style="border-color: color-mix(in oklab, var(--brand), transparent 50%); color: var(--brand); background: color-mix(in oklab, var(--brand), transparent 92%);">Booked</span>` : ''}
<button class="btn ${checkedForThisClass ? '' : 'primary'}" data-act="${actId}" data-class="${c.id}">
${checkedForThisClass ? 'Unbook' : 'Book & Check-in'}
</button>
</div>
`;
const btn = el.querySelector('button');
btn.addEventListener('click', () => {
if (btn.dataset.action === 'unbook') return; // not used, placeholder
const id = btn.dataset.act;
onToggleCheckin(id);
renderClasses(); // refresh "Booked" state
showToast(`${c.name} ${listTodaySet().has(id) ? 'checked-in' : 'removed'}.`);
});
return el;
}
// ---------- Event Handlers ----------
function onToggleCheckin(id, cardEl = null) {
const set = listTodaySet();
if (set.has(id)) {
set.delete(id);
} else {
set.add(id);
}
setToday([...set]);
// Update the card UI quickly if provided
if (cardEl) {
const btn = cardEl.querySelector('.checkin-btn');
const pressed = set.has(id);
btn.setAttribute('aria-pressed', pressed ? 'true' : 'false');
btn.querySelector('span:last-child').textContent = pressed ? 'Checked-in' : 'Check in';
const pill = cardEl.querySelector('.pill');
pill.textContent = pressed ? 'Checked-in' : 'Available';
}
renderStats();
renderHistory();
renderClasses();
const a = ACTIVITIES.find(x => x.id === id);
showToast(`${a ? a.name : 'Activity'} ${set.has(id) ? 'checked-in' : 'removed'}.`);
}
// ---------- Toast ----------
let toastTimer = null;
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('show'), 1800);
}
// ---------- Renderers ----------
function renderActivities() {
const wrap = document.getElementById('activities');
wrap.innerHTML = '';
const q = (document.getElementById('search').value || '').trim().toLowerCase();
const activeSet = listTodaySet();
const items = ACTIVITIES.filter(a => !q || a.name.toLowerCase().includes(q) || a.id.includes(q));
if (items.length === 0) {
const empty = document.createElement('div');
empty.className = 'empty';
empty.textContent = 'No activities match your search.';
wrap.appendChild(empty);
return;
}
for (const a of items) {
wrap.appendChild(activityCard(a, activeSet.has(a.id)));
}
}
function renderClasses() {
const wrap = document.getElementById('classes');
wrap.innerHTML = '';
const activeSet = listTodaySet();
for (const c of SCHEDULE) {
const id = (() => {
const lc = c.name.toLowerCase();
if (lc.includes('crossfit')) return 'crossfit';
if (lc.includes('spin')) return 'spin';
if (lc.includes('yoga') || lc.includes('pilates')) return lc.includes('yoga') ? 'yoga' : 'pilates';
if (lc.includes('hiit')) return 'hiit';
return 'strength';
})();
wrap.appendChild(classItem(c, activeSet.has(id)));
}
}
function renderStats() {
// Today count
document.getElementById('today-count').textContent = countToday();
// Streak
document.getElementById('streak').textContent = computeStreak();
// Month total
document.getElementById('month-total').textContent = computeThisMonth();
// Weekly chart
const { days, max } = last7DaysCounts();
const bars = document.getElementById('bars');
bars.innerHTML = '';
let sum = 0;
days.forEach(d => {
sum += d.count;
const bar = document.createElement('div');
const pct = Math.round((d.count / max) * 100);
bar.className = 'bar';
bar.style.setProperty('--h', pct + '%');
bar.setAttribute('data-count', d.count);
bar.title = `${d.d.toLocaleDateString([], { weekday:'short'})}: ${d.count}`;
bars.appendChild(bar);
});
document.getElementById('week-total').textContent = `${sum} check-ins`;
}
function renderHistory() {
const wrap = document.getElementById('history-list');
wrap.innerHTML = '';
// Get last 10 check-in events
const events = [];
for (const [k, arr] of Object.entries(state.data.checkInsByDate)) {
const d = new Date(k + 'T12:00:00');
for (const id of arr) {
events.push({ date: d, key: k, id });
}
}
events.sort((a,b) => b.date - a.date);
const last = events.slice(0, 12);
if (last.length === 0) {
const empty = document.createElement('div');
empty.className = 'empty';
empty.textContent = 'No check-ins yet. Start by checking in to any activity.';
wrap.appendChild(empty);
return;
}
for (const ev of last) {
const a = ACTIVITIES.find(x => x.id === ev.id);
const item = document.createElement('article');
item.className = 'history-item';
item.innerHTML = `
<div class="ico" style="width:36px;height:36px;border-radius:10px;font-size:18px;background: color-mix(in oklab, var(--brand), transparent 85%); border:1px solid color-mix(in oklab, var(--brand), transparent 75%);">${a?.icon || '🏋️'}</div>
<div>
<div style="font-weight:800;">${a?.name || ev.id}</div>
<div class="muted" style="font-size:12px;">${ev.date.toLocaleDateString([], {weekday:'short', month:'short', day:'numeric'})}${formatTime(ev.date)}</div>
</div>
<span class="badge">Check-in</span>
`;
wrap.appendChild(item);
}
}
function renderView() {
const homeBtn = document.querySelector('.tabbar [data-view="home"]');
const histBtn = document.querySelector('.tabbar [data-view="history"]');
const homeView = document.getElementById('view-home');
const histView = document.getElementById('view-history');
if (state.view === 'home') {
homeBtn.classList.add('active'); homeBtn.setAttribute('aria-selected','true');
histBtn.classList.remove('active'); histBtn.setAttribute('aria-selected','false');
homeView.hidden = false;
histView.hidden = true;
} else {
histBtn.classList.add('active'); histBtn.setAttribute('aria-selected','true');
homeBtn.classList.remove('active'); homeBtn.setAttribute('aria-selected','false');
homeView.hidden = true;
histView.hidden = false;
}
}
function renderAll() {
renderActivities();
renderClasses();
renderStats();
renderHistory();
renderView();
}
// ---------- Events ----------
function bindEvents() {
document.querySelector('.tabbar').addEventListener('click', (e) => {
const btn = e.target.closest('button[data-view]');
if (!btn) return;
state.view = btn.dataset.view;
renderView();
});
// Search
let searchDebounce = null;
document.getElementById('search').addEventListener('input', (e) => {
clearTimeout(searchDebounce);
searchDebounce = setTimeout(() => renderActivities(), 120);
});
// Resize: ensure bars heights transition nicely (optional)
window.addEventListener('resize', () => renderStats());
}
// ---------- Init ----------
(function init() {
document.getElementById('year').textContent = new Date().getFullYear();
bindEvents();
renderAll();
})();
</script>
</body>
</html>