anycoder-82086c20 / index.html
cemo's picture
Upload folder using huggingface_hub
bc24ada verified
Raw History Blame Contribute Delete
33.3 kB
<!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>
<!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;