<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Daily Fix for 26</title>
<meta name="description" content="12-month challenge tracker: tick off daily reps, swaps, and notes." />
<style>
:root{
--bg:#0b0f14; --card:#121826; --muted:#9aa4b2; --text:#e6edf3;
--accent:#58a6ff; --ok:#3fb950; --warn:#d29922; --bad:#f85149;
}
*{box-sizing:border-box}
body{
margin:0;
font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;
background:linear-gradient(180deg,#080b10,#0b0f14);
color:var(--text)
}
header{
position:sticky; top:0; z-index:10;
background:rgba(11,15,20,.9);
backdrop-filter: blur(10px);
border-bottom:1px solid rgba(255,255,255,.06);
}
.wrap{max-width:980px;margin:0 auto;padding:16px}
h1{font-size:20px;margin:0;letter-spacing:.3px}
.sub{color:var(--muted);font-size:13px;margin-top:6px}
.grid{display:grid;gap:12px}
@media(min-width:900px){.grid{grid-template-columns:1.15fr .85fr}}
.card{
background:rgba(18,24,38,.92);
border:1px solid rgba(255,255,255,.07);
border-radius:16px;
box-shadow:0 10px 30px rgba(0,0,0,.25)
}
.card .hd{padding:14px 14px 0 14px}
.card .bd{padding:14px}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
button, input, textarea{
font:inherit;
border-radius:12px;
border:1px solid rgba(255,255,255,.10);
background:#0c111b;
color:var(--text);
padding:10px 12px
}
button{cursor:pointer}
button.small{padding:8px 10px;border-radius:12px;font-size:13px}
textarea{width:100%;min-height:72px;resize:vertical}
.pill{
display:inline-flex;gap:8px;align-items:center;
padding:8px 10px;border-radius:999px;
border:1px solid rgba(255,255,255,.10);
background:#0c111b;color:var(--muted);font-size:13px
}
.pill strong{color:var(--text)}
.table{width:100%;border-collapse:separate;border-spacing:0 10px}
.table th{color:var(--muted);font-weight:600;font-size:12px;text-align:left;padding:0 10px}
.table td{
padding:12px 10px;background:#0c111b;
border-top:1px solid rgba(255,255,255,.07);
border-bottom:1px solid rgba(255,255,255,.07)
}
.table td:first-child{
border-left:1px solid rgba(255,255,255,.07);
border-top-left-radius:14px;border-bottom-left-radius:14px
}
.table td:last-child{
border-right:1px solid rgba(255,255,255,.07);
border-top-right-radius:14px;border-bottom-right-radius:14px
}
.chk{transform:scale(1.35)}
.muted{color:var(--muted)}
.kpi{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.kpi .box{padding:12px;background:#0c111b;border:1px solid rgba(255,255,255,.07);border-radius:14px}
.kpi .num{font-size:20px;font-weight:800}
.kpi .lbl{font-size:12px;color:var(--muted)}
.tabs{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.tab{
padding:10px 12px;border-radius:12px;
border:1px solid rgba(255,255,255,.10);background:transparent
}
.tab.active{background:#0c111b;border-color:rgba(88,166,255,.35)}
.footer-note{font-size:12px;color:var(--muted);line-height:1.4}
.hide{display:none!important}
.two{display:grid;gap:10px}
@media(min-width:700px){.two{grid-template-columns:1fr 1fr}}
/* Season banner + totals + progress */
.banner{
display:flex;align-items:center;justify-content:space-between;gap:10px;
padding:12px 14px;border-radius:16px;
background:linear-gradient(135deg, rgba(88,166,255,.18), rgba(63,185,80,.10));
border:1px solid rgba(255,255,255,.10);
margin-bottom:12px;
}
.banner .title{font-weight:900;letter-spacing:.2px}
.banner .meta{font-size:12px;color:var(--muted);margin-top:2px}
.totals{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}
.totals .box{padding:12px;background:#0c111b;border:1px solid rgba(255,255,255,.07);border-radius:14px}
.totals .num{font-size:18px;font-weight:900}
.totals .lbl{font-size:12px;color:var(--muted)}
.mini-card{
background:#0c111b;
border:1px solid rgba(255,255,255,.07);
border-radius:14px;
box-shadow:none;
margin-top:10px;
}
</style>
</head>
<body>
<header>
<div class="wrap">
<div class="row" style="justify-content:space-between;">
<div>
<h1>Daily Fix for 26</h1>
<div class="sub">Jan–Jun: 50 reps each • Jul–Dec: 100 reps each • Tick, swap, note.</div>
</div>
<div class="row">
<button class="tab active" data-view="today">Today</button>
<button class="tab" data-view="calendar">Calendar</button>
<button class="tab" data-view="rules">Rules</button>
<button class="tab" data-view="settings">Settings</button>
</div>
</div>
</div>
</header>
<main class="wrap grid">
<!-- LEFT -->
<section class="card">
<div class="hd">
<div class="row" style="justify-content:space-between;">
<div class="pill"><strong id="dateLabel">—</strong><span class="muted" id="dayLabel">—</span></div>
<div class="row">
<button class="small" id="prevDay">◀</button>
<button class="small" id="goToday">Today</button>
<button class="small" id="nextDay">▶</button>
</div>
</div>
</div>
<div class="bd">
<!-- TODAY -->
<div id="view-today">
<div class="banner">
<div>
<div class="title" id="seasonTitle">—</div>
<div class="meta" id="seasonMeta">—</div>
</div>
<div class="pill"><span class="muted">Reps per movement</span> <strong id="seasonReps">—</strong></div>
</div>
<div class="kpi">
<div class="box"><div class="num" id="kpiDone">0</div><div class="lbl">Movements done today</div></div>
<div class="box"><div class="num" id="kpiTotal">0</div><div class="lbl">Total movements today</div></div>
<div class="box"><div class="num" id="kpiReps">0</div><div class="lbl">Reps completed today</div></div>
</div>
<div class="totals">
<div class="box"><div class="num" id="totWeek">0</div><div class="lbl">Reps this week</div></div>
<div class="box"><div class="num" id="totMonth">0</div><div class="lbl">Reps this month</div></div>
<div class="box"><div class="num" id="totYear">0</div><div class="lbl">Reps this year</div></div>
</div>
<!-- Year progress -->
<div class="mini-card">
<div class="bd" style="padding:14px">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px">
<div>
<strong>Year progress</strong>
<div class="muted" style="font-size:12px">Total reps completed this year</div>
</div>
<strong id="yearReps">0</strong>
</div>
<div style="height:10px"></div>
<div style="background:#111827;border-radius:999px;overflow:hidden;height:10px">
<div id="yearBar" style="height:100%;width:0%;background:linear-gradient(90deg,#58a6ff,#3fb950)"></div>
</div>
<div class="muted" style="font-size:12px;margin-top:8px">
Target is based on your movements and the 50→100 reps schedule.
</div>
</div>
</div>
<!-- Best week -->
<div class="mini-card">
<div class="bd" style="padding:14px">
<strong>Best week (all time)</strong>
<div class="muted" style="font-size:12px">Highest reps in any Mon–Sun week</div>
<div style="font-size:22px;font-weight:900;margin-top:6px" id="bestWeek">0</div>
</div>
</div>
<div style="height:12px"></div>
<table class="table" id="todayTable">
<thead>
<tr>
<th style="width:80px;">Done</th>
<th>Movement</th>
<th style="width:120px;">Reps</th>
<th>Swap / Notes</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="two">
<div class="card" style="background:transparent;border:none;box-shadow:none">
<div class="footer-note">
<div><strong>Photo check-in (optional):</strong> take a private photo at the <strong>start</strong> (optional) and then <strong>once each month</strong> (only share if you want).</div>
</div>
</div>
<div class="card" style="background:transparent;border:none;box-shadow:none">
<div class="row" style="justify-content:flex-end;">
<button id="markAll" class="small">Mark all done</button>
<button id="clearDay" class="small">Clear today</button>
</div>
<div class="footer-note" style="margin-top:10px">
Progress saves automatically on this device.
<br/>Publish the file to share a link for others.
</div>
</div>
</div>
</div>
<!-- CALENDAR -->
<div id="view-calendar" class="hide">
<div class="row" style="justify-content:space-between;">
<div class="pill"><strong id="monthLabel">—</strong></div>
<div class="row">
<button class="small" id="prevMonth">◀</button>
<button class="small" id="nextMonth">▶</button>
</div>
</div>
<div style="height:12px"></div>
<div id="calendarGrid" class="grid" style="grid-template-columns:repeat(7,1fr);gap:8px"></div>
<div style="height:10px"></div>
<div class="footer-note">
Tap any day to open it. Green = all movements done. Yellow = partial. Red = none.
</div>
</div>
<!-- RULES -->
<div id="view-rules" class="hide">
<h3 style="margin:0 0 10px 0">Rules</h3>
<div class="mini-card">
<div class="bd" style="padding:14px">
<ol style="margin:0;padding-left:18px;line-height:1.5">
<li><strong>Daily goal:</strong> complete Squats, Press ups, Sit ups / crunches.</li>
<li><strong>Reps:</strong> <strong>50</strong> each day from <strong>Jan–Jun</strong>, then <strong>100</strong> each day from <strong>Jul–Dec</strong>.</li>
<li><strong>If you’re injured/limited:</strong> swap the movement for a safe alternative (record it in “Swap / Notes”).</li>
<li><strong>Missed day:</strong> mark it honestly. No pressure to double up.</li>
<li><strong>Photos:</strong> optional start photo + monthly private check-in.</li>
</ol>
<div style="margin-top:10px" class="muted">Not medical advice — if pain is sharp/worsening, get it checked.</div>
</div>
</div>
</div>
<!-- SETTINGS -->
<div id="view-settings" class="hide">
<h3 style="margin:0 0 10px 0">Settings</h3>
<div class="mini-card">
<div class="bd" style="padding:14px">
<div class="two">
<div>
<div class="muted" style="font-size:12px;margin-bottom:6px">Challenge start date</div>
<input type="date" id="startDate" />
</div>
<div>
<div class="muted" style="font-size:12px;margin-bottom:6px">Duration (months)</div>
<input type="number" id="durationMonths" min="1" max="24" />
</div>
</div>
<div style="height:12px"></div>
<div class="muted" style="font-size:12px;margin-bottom:6px">Movements (names only)</div>
<div id="movesList"></div>
<div style="height:10px"></div>
<div class="row">
<button id="addMove" class="small">+ Add movement</button>
<button id="resetDefaults" class="small">Reset defaults</button>
</div>
<hr style="border:none;border-top:1px solid rgba(255,255,255,.08);margin:14px 0"/>
<div class="muted" style="font-size:12px;margin-bottom:6px">Export / Import (backup)</div>
<textarea id="exportBox" placeholder="Click Export to generate a backup blob. Paste here then Import to restore."></textarea>
<div class="row" style="margin-top:10px">
<button id="btnExport" class="small">Export</button>
<button id="btnImport" class="small">Import</button>
<button id="btnWipe" class="small" title="Clears all saved progress on this device">Wipe this device</button>
</div>
</div>
</div>
<div style="height:12px"></div>
<div class="footer-note">
<strong>Publish tip:</strong> Netlify → drag & drop this HTML file → share the link → “Add to Home Screen”.
</div>
</div>
</div>
</section>
<!-- RIGHT -->
<aside class="card">
<div class="hd"><h3 style="margin:0;padding:14px 14px 0 14px;">Quick stats</h3></div>
<div class="bd">
<div class="row" style="justify-content:space-between;">
<div class="pill"><span class="muted">Streak (all done)</span> <strong id="streak">0</strong></div>
<div class="pill"><span class="muted">This month</span> <strong id="monthPct">0%</strong></div>
<div class="pill"><span class="muted">All time</span> <strong id="allPct">0%</strong></div>
</div>
<div style="height:12px"></div>
<div class="mini-card">
<div class="bd" style="padding:14px">
<div style="font-weight:700;margin-bottom:6px">Publish (easiest)</div>
<ol style="margin:0;padding-left:18px;line-height:1.55" class="muted">
<li>Go to <strong>netlify.com</strong> and create a free account.</li>
<li>Drag & drop this <strong>daily-fix-26.html</strong> file into “Sites”.</li>
<li>Netlify gives you a link — share it.</li>
<li>On phone: open link → menu → <strong>Add to Home Screen</strong>.</li>
</ol>
</div>
</div>
<div style="height:12px"></div>
<div class="mini-card">
<div class="bd" style="padding:14px">
<div style="font-weight:700;margin-bottom:6px">Important note</div>
<div class="muted" style="line-height:1.55">
This version saves progress on <strong>each person’s own phone</strong>.
If you want a shared leaderboard later, we can add logins + a database.
</div>
</div>
</div>
</div>
</aside>
</main>
<script>
const LS_KEY = "dailyFix26_vFinal";
// Reps rule: Jan–Jun = 50, Jul–Dec = 100
function repsForDate(dateObj){
const month = dateObj.getMonth() + 1; // 1-12
return (month <= 6) ? 50 : 100;
}
function seasonLabel(dateObj){
const month = dateObj.getMonth() + 1;
if (month <= 6) return { title: "50s Season", meta: "Jan – Jun (build the habit)" };
return { title: "100s Season", meta: "Jul – Dec (turn it up)" };
}
const DEFAULTS = {
// Default to Jan 1 of the current year
startDate: (() => {
const d = new Date();
return `${d.getFullYear()}-01-01`;
})(),
durationMonths: 12,
movements: [
{ name: "Squats" },
{ name: "Press ups" },
{ name: "Sit ups / crunches" }
],
days: {} // "YYYY-MM-DD": { done:[bool...], notes:[""...] }
};
function load(){
try{
const raw = localStorage.getItem(LS_KEY);
if (!raw) return structuredClone(DEFAULTS);
const parsed = JSON.parse(raw);
return { ...structuredClone(DEFAULTS), ...parsed };
}catch{
return structuredClone(DEFAULTS);
}
}
function save(){ localStorage.setItem(LS_KEY, JSON.stringify(state)); }
let state = load();
let current = new Date();
let calMonth = new Date(current.getFullYear(), current.getMonth(), 1);
const $ = (id) => document.getElementById(id);
function ymd(d){
const z = new Date(d);
const yyyy = z.getFullYear();
const mm = String(z.getMonth()+1).padStart(2,"0");
const dd = String(z.getDate()).padStart(2,"0");
return `${yyyy}-${mm}-${dd}`;
}
function niceDate(d){
return d.toLocaleDateString(undefined, { weekday:"short", year:"numeric", month:"short", day:"numeric" });
}
function ensureDay(key){
if (!state.days[key]){
state.days[key] = {
done: state.movements.map(()=>false),
notes: state.movements.map(()=> "")
};
} else {
// keep arrays aligned if movements change
const m = state.movements.length;
state.days[key].done = (state.days[key].done || []).slice(0,m);
state.days[key].notes = (state.days[key].notes || []).slice(0,m);
while (state.days[key].done.length < m) state.days[key].done.push(false);
while (state.days[key].notes.length < m) state.days[key].notes.push("");
}
}
function daySummary(key){
ensureDay(key);
const d = state.days[key];
const total = state.movements.length;
const doneCount = d.done.filter(Boolean).length;
const dateObj = new Date(key + "T00:00:00");
const repsEach = repsForDate(dateObj);
const reps = d.done.reduce((acc, isDone)=> acc + (isDone ? repsEach : 0), 0);
return { total, doneCount, reps, repsEach };
}
// Monday-based week start
function startOfWeek(dateObj){
const d = new Date(dateObj);
const day = d.getDay(); // 0 Sun, 1 Mon...
const diff = (day === 0) ? -6 : (1 - day);
d.setDate(d.getDate() + diff);
d.setHours(0,0,0,0);
return d;
}
function repsTotalForRange(startDateObj, endDateObj){
let total = 0;
for (let d = new Date(startDateObj); d <= endDateObj; d.setDate(d.getDate()+1)){
const key = ymd(d);
total += daySummary(key).reps;
}
return total;
}
function totalYearTarget(year){
// total possible reps in that year based on schedule + movement count
let total = 0;
for (let m = 1; m <= 12; m++){
const daysInMonth = new Date(year, m, 0).getDate();
const repsEach = (m <= 6) ? 50 : 100;
total += daysInMonth * repsEach * state.movements.length;
}
return total;
}
function bestWeekEver(){
let best = 0;
const keys = Object.keys(state.days);
if (!keys.length) return 0;
keys.sort();
for (const k of keys){
const d = new Date(k + "T00:00:00");
const ws = startOfWeek(d);
const we = new Date(ws); we.setDate(ws.getDate()+6);
const total = repsTotalForRange(ws, we);
if (total > best) best = total;
}
return best;
}
function withinChallenge(key){
const start = new Date(state.startDate + "T00:00:00");
const end = new Date(start);
end.setMonth(end.getMonth() + Number(state.durationMonths||12));
const d = new Date(key + "T00:00:00");
return d >= start && d < end;
}
function calcStreak(){
// streak counts consecutive days ending today where ALL movements are done
let streak = 0;
let d = new Date();
while (true){
const key = ymd(d);
if (!withinChallenge(key)) break;
const sum = daySummary(key);
if (sum.total > 0 && sum.doneCount === sum.total) streak++;
else break;
d.setDate(d.getDate()-1);
}
return streak;
}
function pctForRange(keys){
let total = 0, done = 0;
for (const k of keys){
const sum = daySummary(k);
total += sum.total;
done += sum.doneCount;
}
return total ? Math.round((done/total)*100) : 0;
}
function setView(name){
document.querySelectorAll(".tab").forEach(b=>{
b.classList.toggle("active", b.dataset.view === name);
});
["today","calendar","rules","settings"].forEach(v=>{
$("view-"+v).classList.toggle("hide", v !== name);
});
if (name === "settings") renderSettings();
if (name === "calendar") renderCalendar();
}
function renderHeader(){
$("dateLabel").textContent = niceDate(current);
$("dayLabel").textContent = ymd(current);
}
function renderSeason(){
const s = seasonLabel(current);
$("seasonTitle").textContent = s.title;
$("seasonMeta").textContent = s.meta;
$("seasonReps").textContent = repsForDate(current);
}
function renderTotals(){
const today = new Date(current);
const weekStart = startOfWeek(today);
const weekEnd = new Date(weekStart); weekEnd.setDate(weekStart.getDate()+6);
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const monthEnd = new Date(today.getFullYear(), today.getMonth()+1, 0);
const yearStart = new Date(today.getFullYear(), 0, 1);
const yearEnd = new Date(today.getFullYear(), 11, 31);
$("totWeek").textContent = repsTotalForRange(weekStart, weekEnd).toLocaleString();
$("totMonth").textContent = repsTotalForRange(monthStart, monthEnd).toLocaleString();
$("totYear").textContent = repsTotalForRange(yearStart, yearEnd).toLocaleString();
}
function renderYearProgress(){
const year = current.getFullYear();
const yearStart = new Date(year,0,1);
const yearEnd = new Date(year,11,31);
const done = repsTotalForRange(yearStart, yearEnd);
const target = totalYearTarget(year);
const pct = target ? Math.min(100, Math.round((done/target)*100)) : 0;
$("yearReps").textContent = done.toLocaleString();
$("yearBar").style.width = pct + "%";
}
function renderBestWeek(){
$("bestWeek").textContent = bestWeekEver().toLocaleString();
}
function renderToday(){
const key = ymd(current);
ensureDay(key);
const d = state.days[key];
const tbody = $("todayTable").querySelector("tbody");
tbody.innerHTML = "";
const repsEach = repsForDate(current);
state.movements.forEach((mv, i) => {
const tr = document.createElement("tr");
const td0 = document.createElement("td");
const cb = document.createElement("input");
cb.type = "checkbox";
cb.className = "chk";
cb.checked = !!d.done[i];
cb.addEventListener("change", () => {
d.done[i] = cb.checked;
save();
renderAll();
});
td0.appendChild(cb);
const td1 = document.createElement("td");
td1.innerHTML = `<div style="font-weight:700">${mv.name}</div><div class="muted" style="font-size:12px">Daily</div>`;
const td2 = document.createElement("td");
td2.innerHTML = `<div style="font-weight:800;font-size:16px">${repsEach}</div><div class="muted" style="font-size:12px">reps</div>`;
const td3 = document.createElement("td");
const note = document.createElement("input");
note.placeholder = "Swap / notes (e.g., wall sit, incline press-ups, etc.)";
note.value = d.notes[i] || "";
note.style.width = "100%";
note.addEventListener("input", () => {
d.notes[i] = note.value;
save();
});
td3.appendChild(note);
tr.appendChild(td0); tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3);
tbody.appendChild(tr);
});
const sum = daySummary(key);
$("kpiDone").textContent = sum.doneCount;
$("kpiTotal").textContent = sum.total;
$("kpiReps").textContent = sum.reps.toLocaleString();
}
function renderCalendar(){
$("monthLabel").textContent = calMonth.toLocaleDateString(undefined, { year:"numeric", month:"long" });
const grid = $("calendarGrid");
grid.innerHTML = "";
const first = new Date(calMonth.getFullYear(), calMonth.getMonth(), 1);
const startDay = first.getDay(); // 0 Sun
const start = new Date(first);
start.setDate(first.getDate() - startDay);
const dayNames = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
dayNames.forEach(n=>{
const el = document.createElement("div");
el.className = "muted";
el.style.fontSize = "12px";
el.style.padding = "0 4px";
el.textContent = n;
grid.appendChild(el);
});
for (let i=0; i<42; i++){
const d = new Date(start);
d.setDate(start.getDate() + i);
const key = ymd(d);
const sum = daySummary(key);
let color = "rgba(248,81,73,.20)", border = "rgba(248,81,73,.35)";
if (sum.doneCount === 0) { }
else if (sum.doneCount < sum.total) { color = "rgba(210,153,34,.20)"; border="rgba(210,153,34,.45)"; }
else { color = "rgba(63,185,80,.20)"; border="rgba(63,185,80,.45)"; }
const cell = document.createElement("button");
cell.style.padding = "10px 8px";
cell.style.borderRadius = "14px";
cell.style.border = `1px solid ${border}`;
cell.style.background = color;
cell.style.textAlign = "left";
cell.style.minHeight = "64px";
const inMonth = d.getMonth() === calMonth.getMonth();
cell.style.opacity = inMonth ? "1" : ".45";
const isToday = key === ymd(new Date());
const isCurrent = key === ymd(current);
cell.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
<div style="font-weight:800">${d.getDate()}</div>
<div class="muted" style="font-size:11px">${sum.doneCount}/${sum.total}</div>
</div>
<div class="muted" style="font-size:11px;margin-top:6px">${sum.reps.toLocaleString()} reps</div>
`;
if (isToday) cell.style.boxShadow = "0 0 0 2px rgba(88,166,255,.35) inset";
if (isCurrent) cell.style.outline = "2px solid rgba(255,255,255,.18)";
cell.addEventListener("click", () => {
current = new Date(d);
setView("today");
renderAll();
});
grid.appendChild(cell);
}
}
function renderStats(){
$("streak").textContent = calcStreak();
// Month %
const now = new Date();
const first = new Date(now.getFullYear(), now.getMonth(), 1);
const last = new Date(now.getFullYear(), now.getMonth()+1, 0);
const keys = [];
for (let d=new Date(first); d<=last; d.setDate(d.getDate()+1)){
const k = ymd(d);
if (withinChallenge(k)) keys.push(k);
}
$("monthPct").textContent = `${pctForRange(keys)}%`;
// All time %
const start = new Date(state.startDate + "T00:00:00");
const end = new Date(start);
end.setMonth(end.getMonth() + Number(state.durationMonths||12));
const allKeys = [];
for (let d=new Date(start); d<end; d.setDate(d.getDate()+1)){
allKeys.push(ymd(d));
}
$("allPct").textContent = `${pctForRange(allKeys)}%`;
}
function renderSettings(){
$("startDate").value = state.startDate;
$("durationMonths").value = state.durationMonths;
const list = $("movesList");
list.innerHTML = "";
state.movements.forEach((mv, idx) => {
const wrap = document.createElement("div");
wrap.style.display = "grid";
wrap.style.gridTemplateColumns = "1fr auto";
wrap.style.gap = "10px";
wrap.style.marginBottom = "10px";
const name = document.createElement("input");
name.value = mv.name;
name.placeholder = "Movement name";
name.addEventListener("input", () => {
mv.name = name.value;
save();
renderAll();
});
const del = document.createElement("button");
del.textContent = "Remove";
del.className = "small";
del.addEventListener("click", () => {
state.movements.splice(idx, 1);
Object.keys(state.days).forEach(k=>{
const d = state.days[k];
if (d){
d.done.splice(idx,1);
d.notes.splice(idx,1);
}
});
save();
renderAll();
});
wrap.appendChild(name);
wrap.appendChild(del);
list.appendChild(wrap);
});
}
function renderAll(){
renderHeader();
renderSeason();
renderTotals();
renderYearProgress();
renderBestWeek();
renderToday();
renderStats();
renderCalendar();
}
// Tabs
document.querySelectorAll(".tab").forEach(btn=>{
btn.addEventListener("click", ()=> setView(btn.dataset.view));
});
// Day nav
$("prevDay").addEventListener("click", ()=>{ current.setDate(current.getDate()-1); renderAll(); });
$("nextDay").addEventListener("click", ()=>{ current.setDate(current.getDate()+1); renderAll(); });
$("goToday").addEventListener("click", ()=>{ current = new Date(); renderAll(); });
// Month nav
$("prevMonth").addEventListener("click", ()=>{ calMonth = new Date(calMonth.getFullYear(), calMonth.getMonth()-1, 1); renderCalendar(); });
$("nextMonth").addEventListener("click", ()=>{ calMonth = new Date(calMonth.getFullYear(), calMonth.getMonth()+1, 1); renderCalendar(); });
// Actions
$("markAll").addEventListener("click", ()=>{
const key = ymd(current);
ensureDay(key);
state.days[key].done = state.movements.map(()=>true);
save(); renderAll();
});
$("clearDay").addEventListener("click", ()=>{
const key = ymd(current);
ensureDay(key);
state.days[key].done = state.movements.map(()=>false);
state.days[key].notes = state.movements.map(()=> "");
save(); renderAll();
});
// Settings changes
$("startDate").addEventListener("change", ()=>{
state.startDate = $("startDate").value || state.startDate;
save(); renderAll();
});
$("durationMonths").addEventListener("change", ()=>{
state.durationMonths = Number($("durationMonths").value || 12);
save(); renderAll();
});
$("addMove").addEventListener("click", ()=>{
state.movements.push({ name:"New movement" });
Object.keys(state.days).forEach(k=>{
ensureDay(k);
state.days[k].done.push(false);
state.days[k].notes.push("");
});
save(); renderAll(); setView("settings");
});
$("resetDefaults").addEventListener("click", ()=>{
state.movements = structuredClone(DEFAULTS.movements);
Object.keys(state.days).forEach(k=> ensureDay(k));
save(); renderAll(); setView("settings");
});
// Export/Import/Wipe
$("btnExport").addEventListener("click", ()=>{
const blob = { version: "final", exportedAt: new Date().toISOString(), state };
$("exportBox").value = btoa(unescape(encodeURIComponent(JSON.stringify(blob))));
});
$("btnImport").addEventListener("click", ()=>{
try{
const raw = $("exportBox").value.trim();
if (!raw) return alert("Paste an export first.");
const json = decodeURIComponent(escape(atob(raw)));
const blob = JSON.parse(json);
if (!blob.state) return alert("That export doesn't look right.");
state = blob.state;
save();
current = new Date();
calMonth = new Date(current.getFullYear(), current.getMonth(), 1);
renderAll();
alert("Imported ✅");
}catch(e){
alert("Import failed. Make sure you pasted the full export text.");
}
});
$("btnWipe").addEventListener("click", ()=>{
if (!confirm("Wipe all progress saved on THIS device?")) return;
localStorage.removeItem(LS_KEY);
state = load();
save();
current = new Date();
calMonth = new Date(current.getFullYear(), current.getMonth(), 1);
renderAll();
});
// Init
save();
renderAll();
</script>
</body>
</html>