Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!doctype html>
- <html lang="en">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Ninja Military Bodyweight Web App</title>
- <style>
- :root{
- --bg:#071018;
- --bg2:#0c1824;
- --panel:#111d29;
- --panel2:#172736;
- --panel3:#0d1721;
- --line:#2b4258;
- --text:#eef6ff;
- --muted:#a9bacb;
- --muted2:#72869a;
- --accent:#ff3b2f;
- --accent2:#ffbd2e;
- --good:#37d982;
- --good2:#123a28;
- --warn:#ffc857;
- --bad:#ff6b6b;
- --shadow:0 18px 45px rgba(0,0,0,.35);
- --radius:18px;
- }
- *{box-sizing:border-box}
- html{scroll-behavior:smooth}
- body{
- margin:0;
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
- color:var(--text);
- background:
- radial-gradient(900px 550px at 15% -10%, rgba(255,59,47,.20), transparent 70%),
- radial-gradient(700px 500px at 90% 10%, rgba(255,189,46,.13), transparent 70%),
- linear-gradient(180deg,var(--bg),var(--bg2));
- min-height:100vh;
- }
- a{color:#9bd0ff}
- button,input,select,textarea{font:inherit}
- button{cursor:pointer}
- .app-shell{max-width:1220px;margin:0 auto;padding:22px 18px 60px}
- .hero{
- display:grid;
- grid-template-columns: 1.35fr .65fr;
- gap:18px;
- align-items:stretch;
- margin-bottom:18px;
- }
- .hero-main,.hero-side,.panel{
- background:linear-gradient(180deg,rgba(23,39,54,.92),rgba(13,23,33,.94));
- border:1px solid rgba(154,208,255,.16);
- border-radius:var(--radius);
- box-shadow:var(--shadow);
- overflow:hidden;
- position:relative;
- }
- .hero-main:before{
- content:"";
- position:absolute;inset:0;
- background:
- linear-gradient(120deg,rgba(255,59,47,.18),transparent 34%),
- repeating-linear-gradient(135deg,rgba(255,255,255,.04) 0,rgba(255,255,255,.04) 1px,transparent 1px,transparent 12px);
- pointer-events:none;
- }
- .hero-content{position:relative;padding:28px}
- .kicker{display:flex;gap:8px;align-items:center;flex-wrap:wrap;color:var(--accent2);font-weight:800;letter-spacing:.12em;text-transform:uppercase;font-size:.78rem}
- .kicker .dot{width:8px;height:8px;border-radius:999px;background:var(--accent);box-shadow:0 0 14px var(--accent)}
- h1{font-size:clamp(2.05rem,5vw,4.6rem);line-height:.95;margin:12px 0 10px;text-transform:uppercase;letter-spacing:-.06em}
- h1 span{color:var(--accent);text-shadow:0 0 20px rgba(255,59,47,.23)}
- .subtitle{color:var(--muted);font-size:1.03rem;line-height:1.55;max-width:820px;margin:0}
- .hero-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}
- .btn{
- border:1px solid rgba(255,255,255,.12);
- background:#203040;
- color:var(--text);
- border-radius:999px;
- padding:10px 14px;
- font-weight:800;
- display:inline-flex;align-items:center;gap:8px;
- transition:.18s transform,.18s background,.18s border-color;
- text-decoration:none;
- }
- .btn:hover{transform:translateY(-1px);background:#2a3d50;border-color:rgba(255,255,255,.24)}
- .btn.primary{background:linear-gradient(135deg,var(--accent),#ff7a29);border-color:transparent;color:#fff}
- .btn.good{background:linear-gradient(135deg,#20b968,#26d885);border-color:transparent;color:#04130b}
- .btn.warn{background:linear-gradient(135deg,#ffbd2e,#ffe08a);border-color:transparent;color:#2a1700}
- .btn.ghost{background:transparent}
- .btn.small{padding:7px 10px;font-size:.86rem}
- .btn.danger{background:#3a1720;border-color:#6e2635;color:#ffbdc6}
- .hero-side{padding:18px;display:flex;flex-direction:column;gap:12px}
- .stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
- .stat{
- background:rgba(255,255,255,.04);
- border:1px solid rgba(255,255,255,.09);
- border-radius:15px;
- padding:13px;
- min-height:74px;
- }
- .stat .label{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;font-weight:800}
- .stat .value{font-size:1.35rem;font-weight:900;margin-top:4px}
- .progress-wrap{height:10px;background:#09131d;border-radius:99px;border:1px solid rgba(255,255,255,.08);overflow:hidden}
- .progress-fill{height:100%;width:0;background:linear-gradient(90deg,var(--accent),var(--accent2));transition:width .25s ease}
- .tabs{
- position:sticky;top:0;z-index:10;
- display:flex;gap:8px;flex-wrap:wrap;
- padding:10px 0 14px;
- backdrop-filter: blur(10px);
- }
- .tab-btn{
- color:var(--muted);
- background:rgba(9,19,29,.78);
- border:1px solid rgba(255,255,255,.09);
- border-radius:999px;
- padding:10px 13px;
- font-weight:900;
- }
- .tab-btn.active{color:#fff;background:linear-gradient(135deg,#26394b,#1b2b3a);border-color:rgba(255,255,255,.22)}
- .grid{display:grid;gap:18px}
- .two-col{grid-template-columns:.72fr .28fr}
- .three-col{grid-template-columns:repeat(3,1fr)}
- .panel{padding:18px}
- .panel h2,.panel h3{margin:0 0 10px;letter-spacing:-.03em}
- .panel h2{font-size:1.45rem}
- .panel h3{font-size:1.05rem}
- .muted{color:var(--muted)}
- .small-text{font-size:.88rem;color:var(--muted);line-height:1.45}
- .pill-row{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0}
- .pill{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.04);padding:6px 9px;border-radius:999px;color:var(--muted);font-size:.82rem;font-weight:800}
- .pill.hot{color:#fff;background:rgba(255,59,47,.18);border-color:rgba(255,59,47,.32)}
- .pill.good{color:#baffe1;background:rgba(55,217,130,.12);border-color:rgba(55,217,130,.28)}
- .toolbar{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:12px 0 16px}
- .field{display:flex;flex-direction:column;gap:6px;min-width:135px;flex:1}
- .field.inline{flex-direction:row;align-items:center;min-width:auto;gap:9px}
- label,.field-label{font-size:.78rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:900}
- input,select,textarea{
- background:#0a141f;
- color:var(--text);
- border:1px solid rgba(255,255,255,.12);
- border-radius:12px;
- padding:10px 11px;
- outline:none;
- }
- textarea{min-height:92px;resize:vertical;width:100%}
- input:focus,select:focus,textarea:focus{border-color:rgba(255,189,46,.65);box-shadow:0 0 0 3px rgba(255,189,46,.12)}
- input[type="checkbox"]{width:19px;height:19px;accent-color:var(--good);padding:0}
- .day-header{
- display:flex;align-items:flex-start;justify-content:space-between;gap:14px;flex-wrap:wrap;
- border-bottom:1px solid rgba(255,255,255,.09);padding-bottom:14px;margin-bottom:14px;
- }
- .day-title{display:flex;gap:12px;align-items:center}
- .day-badge{width:48px;height:48px;border-radius:16px;background:linear-gradient(135deg,var(--accent),#ff7a29);display:grid;place-items:center;font-weight:1000;font-size:1.2rem;box-shadow:0 10px 28px rgba(255,59,47,.24)}
- .day-title h2{margin:0}
- .exercise-list{display:grid;gap:12px}
- .exercise-card{
- display:grid;grid-template-columns:auto 1fr auto;gap:12px;align-items:flex-start;
- background:linear-gradient(180deg,rgba(255,255,255,.045),rgba(255,255,255,.025));
- border:1px solid rgba(255,255,255,.10);
- border-radius:16px;
- padding:14px;
- transition:.18s border-color,.18s background,.18s transform;
- }
- .exercise-card:hover{border-color:rgba(255,255,255,.20);transform:translateY(-1px)}
- .exercise-card.complete{background:linear-gradient(180deg,rgba(55,217,130,.13),rgba(55,217,130,.06));border-color:rgba(55,217,130,.34)}
- .exercise-main h3{margin:0 0 4px;font-size:1.05rem}
- .exercise-sub{display:flex;gap:8px;flex-wrap:wrap;margin:7px 0 0}
- .tag{background:#08121b;border:1px solid rgba(255,255,255,.09);color:var(--muted);font-size:.76rem;border-radius:999px;padding:4px 7px;font-weight:800}
- .tag.red{color:#ffd4d0;background:rgba(255,59,47,.13);border-color:rgba(255,59,47,.26)}
- .tag.yellow{color:#ffe7a6;background:rgba(255,189,46,.13);border-color:rgba(255,189,46,.26)}
- .tag.green{color:#baffdd;background:rgba(55,217,130,.13);border-color:rgba(55,217,130,.26)}
- .exercise-actions{display:flex;gap:7px;align-items:center;flex-wrap:wrap;justify-content:flex-end}
- .complete-check{margin-top:8px}
- .empty-state{padding:28px;border:1px dashed rgba(255,255,255,.18);border-radius:16px;color:var(--muted);text-align:center}
- .quote-box{border-left:4px solid var(--accent);padding:12px 14px;background:rgba(255,59,47,.08);border-radius:0 13px 13px 0;color:#ffd8d5;line-height:1.45;margin:12px 0}
- .warning-box{border-left:4px solid var(--warn);padding:12px 14px;background:rgba(255,189,46,.08);border-radius:0 13px 13px 0;color:#ffe8ae;line-height:1.45;margin:12px 0}
- .level-grid,.bank-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px}
- .level-card,.bank-card,.week-card{
- background:rgba(255,255,255,.04);
- border:1px solid rgba(255,255,255,.10);
- border-radius:16px;
- padding:14px;
- }
- .level-card.active{border-color:rgba(255,189,46,.62);background:rgba(255,189,46,.09)}
- .level-card h3,.bank-card h3,.week-card h3{margin:0 0 8px}
- .week-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
- .mini-progress{height:8px;border-radius:99px;background:#071018;overflow:hidden;border:1px solid rgba(255,255,255,.08);margin-top:10px}
- .mini-progress span{display:block;height:100%;background:linear-gradient(90deg,var(--good),var(--accent2));width:0}
- .calendar-grid{overflow:auto;padding-bottom:8px}
- .calendar-table{border-collapse:separate;border-spacing:6px;min-width:640px;width:100%}
- .calendar-table th{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;text-align:center;padding:5px}
- .calendar-table td{padding:0;text-align:center}
- .cal-cell{
- width:100%;min-height:42px;border-radius:10px;background:#0b1520;border:1px solid rgba(255,255,255,.10);color:var(--muted);font-weight:900;
- display:grid;place-items:center;
- }
- .cal-cell.done{background:linear-gradient(135deg,rgba(55,217,130,.40),rgba(55,217,130,.12));border-color:rgba(55,217,130,.48);color:#dfffee}
- .cal-cell.rest.done{background:linear-gradient(135deg,rgba(255,189,46,.28),rgba(255,189,46,.10));border-color:rgba(255,189,46,.42);color:#fff4d1}
- .settings-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}
- .checkbox-panel{display:flex;align-items:flex-start;gap:10px;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.09);border-radius:14px;padding:12px}
- .checkbox-panel strong{display:block;margin-bottom:3px}
- .table-like{width:100%;border-collapse:collapse;margin-top:8px}
- .table-like th,.table-like td{border-bottom:1px solid rgba(255,255,255,.08);padding:9px;text-align:left;vertical-align:top}
- .table-like th{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
- .search-row{display:flex;gap:10px;align-items:center;margin:12px 0;flex-wrap:wrap}
- .search-row input{flex:1;min-width:220px}
- .modal{position:fixed;inset:0;z-index:999;display:none}
- .modal.open{display:block}
- .modal-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(6px)}
- .modal-card{
- position:relative;
- width:min(980px,calc(100vw - 24px));
- max-height:calc(100vh - 32px);
- margin:16px auto;
- background:#0d1721;
- border:1px solid rgba(255,255,255,.16);
- border-radius:20px;
- box-shadow:var(--shadow);
- overflow:hidden;
- display:flex;
- flex-direction:column;
- }
- .modal-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid rgba(255,255,255,.10)}
- .modal-head h2{margin:0;font-size:1.08rem}
- .modal-body{padding:16px;overflow:auto}
- .video-box{aspect-ratio:16/9;width:100%;background:#050a0f;border:1px solid rgba(255,255,255,.09);border-radius:16px;overflow:hidden;display:grid;place-items:center}
- .video-box iframe{width:100%;height:100%;border:0;background:#050a0f}
- .video-thumb{position:relative;display:block;width:100%;height:100%;color:#fff;text-decoration:none;background:#050a0f}
- .video-thumb img{width:100%;height:100%;object-fit:cover;display:block;opacity:.78}
- .play-badge{position:absolute;inset:auto 20px 20px 20px;display:flex;align-items:center;gap:10px;font-weight:1000;text-shadow:0 2px 12px rgba(0,0,0,.75)}
- .play-badge:before{content:"";width:58px;height:40px;border-radius:12px;background:#ff0000;box-shadow:0 10px 28px rgba(0,0,0,.35)}
- .play-badge:after{content:"";position:absolute;left:24px;border-left:15px solid white;border-top:10px solid transparent;border-bottom:10px solid transparent}
- .link-grid{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
- .placeholder{padding:24px;text-align:center;color:var(--muted);line-height:1.5}
- .toast{
- position:fixed;right:16px;bottom:16px;background:#101d29;border:1px solid rgba(255,255,255,.14);color:#fff;
- border-radius:14px;padding:12px 14px;box-shadow:var(--shadow);z-index:1000;opacity:0;transform:translateY(10px);transition:.25s;pointer-events:none;max-width:min(440px,calc(100vw - 32px))
- }
- .toast.show{opacity:1;transform:translateY(0)}
- .hidden{display:none!important}
- @media (max-width:900px){
- .hero,.two-col{grid-template-columns:1fr}
- .stat-grid{grid-template-columns:repeat(2,1fr)}
- .exercise-card{grid-template-columns:auto 1fr}
- .exercise-actions{grid-column:1 / -1;justify-content:flex-start}
- .tabs{position:relative}
- }
- @media (max-width:560px){
- .app-shell{padding:14px 10px 40px}
- .hero-content{padding:22px 18px}
- .stat-grid{grid-template-columns:1fr}
- .toolbar .field{min-width:100%}
- .day-badge{width:42px;height:42px}
- .exercise-card{padding:12px}
- }
- @media print{
- body{background:white;color:black}
- .tabs,.hero-actions,.exercise-actions,.toolbar button,.modal,.toast{display:none!important}
- .panel,.hero-main,.hero-side{box-shadow:none;border:1px solid #bbb;background:white;color:black}
- .subtitle,.muted,.small-text,.pill,.tag{color:#333}
- }
- </style>
- </head>
- <body>
- <div class="app-shell">
- <section class="hero" aria-label="App header">
- <div class="hero-main">
- <div class="hero-content">
- <div class="kicker"><span class="dot"></span><span>No-bar hard mode</span><span>•</span><span>calisthenics + military PRT</span></div>
- <h1>Ninja <span>Bodyweight</span> Command</h1>
- <p class="subtitle">A single-file training app rebuilt from the “Forever Street Calisthenics and Military PRT Blueprint” for your current situation: no bars, no dumbbells, just bodyweight, hard work, level-based progressions, and daily completion tracking.</p>
- <div class="hero-actions">
- <button class="btn primary" data-tab-jump="today" data-set-today>Start today</button>
- <button class="btn ghost" data-tab-jump="settings">Change level</button>
- <button class="btn ghost" data-tab-jump="progress">See progress</button>
- <button class="btn ghost" id="printBtn">Print plan</button>
- </div>
- </div>
- </div>
- <aside class="hero-side" aria-label="Quick stats">
- <div class="stat-grid">
- <div class="stat"><div class="label">Week</div><div class="value" id="statWeek">1</div></div>
- <div class="stat"><div class="label">Level</div><div class="value" id="statLevel">3</div></div>
- <div class="stat"><div class="label">Today</div><div class="value" id="statDay">Mon</div></div>
- <div class="stat"><div class="label">Mode</div><div class="value" id="statMode">No bar</div></div>
- </div>
- <div>
- <div class="field-label">Current week completion</div>
- <div class="progress-wrap" aria-label="Current week completion"><div class="progress-fill" id="weekProgressFill"></div></div>
- <div class="small-text" id="weekProgressText" style="margin-top:8px">0% complete</div>
- </div>
- </aside>
- </section>
- <nav class="tabs" aria-label="Main tabs">
- <button class="tab-btn active" data-tab="today">Today</button>
- <button class="tab-btn" data-tab="week">Week Plan</button>
- <button class="tab-btn" data-tab="settings">Settings</button>
- <button class="tab-btn" data-tab="progress">Progress</button>
- <button class="tab-btn" data-tab="bank">Exercise Bank</button>
- </nav>
- <main>
- <section class="tab-panel" id="tab-today">
- <div class="grid two-col">
- <section class="panel">
- <div class="toolbar">
- <div class="field">
- <label for="weekSelect">Training week</label>
- <select id="weekSelect"></select>
- </div>
- <div class="field">
- <label for="daySelect">Day</label>
- <select id="daySelect"></select>
- </div>
- <button class="btn ghost" id="todayDayBtn" type="button">Today</button>
- <div class="field">
- <label for="quickLevelSelect">Level</label>
- <select id="quickLevelSelect"></select>
- </div>
- <button class="btn good" id="markAllBtn">Mark all complete</button>
- <button class="btn danger" id="clearDayBtn">Clear day</button>
- </div>
- <div id="todayContent"></div>
- </section>
- <aside class="panel">
- <h2>Rules of the app</h2>
- <div class="quote-box"><strong>Hard, not stupid.</strong><br>Use clean tempo. Stop most main sets with about 1-2 reps left. Push the finishers hard, not every single set.</div>
- <div class="quote-box"><strong>Tempo code:</strong><br><code>@ 3010</code> means 3 seconds down, 0 seconds pause at the bottom, 1 second up, 0 seconds pause at the top. <code>X</code> means explode up with control.</div>
- <div class="warning-box"><strong>No-bar truth:</strong><br>The back/biceps work here protects balance and builds some muscle, but it does not fully replace real pull-ups or rows. When you get a safe bar later, turn off No-bar mode.</div>
- <div class="pill-row" id="todayPills"></div>
- <h3 style="margin-top:16px">Day notes</h3>
- <textarea id="dayNotes" placeholder="Write soreness, reps achieved, run time, or anything you noticed..."></textarea>
- <div class="small-text" style="margin-top:8px">Notes and checkboxes save automatically in this browser.</div>
- </aside>
- </div>
- </section>
- <section class="tab-panel hidden" id="tab-week">
- <section class="panel">
- <div class="day-header">
- <div>
- <h2>Full week plan</h2>
- <p class="muted">The week follows the original blueprint structure, but bar work is swapped when No-bar mode is on.</p>
- </div>
- <div class="pill-row"><span class="pill hot" id="weekLevelPill">Level 3</span><span class="pill good" id="weekEquipmentPill">No-bar mode</span></div>
- </div>
- <div class="week-grid" id="weekOverview"></div>
- </section>
- </section>
- <section class="tab-panel hidden" id="tab-settings">
- <div class="grid two-col">
- <section class="panel">
- <h2>General settings</h2>
- <p class="muted">Change your body stats, level, and equipment. The daily exercises update instantly.</p>
- <div class="settings-grid">
- <div class="field"><label for="nameInput">Name / call sign</label><input id="nameInput" placeholder="Ninja" /></div>
- <div class="field"><label for="kgInput">Bodyweight kg</label><input id="kgInput" type="number" min="20" max="250" step="0.1" placeholder="kg" /></div>
- <div class="field"><label for="cmInput">Height cm</label><input id="cmInput" type="number" min="90" max="230" step="1" placeholder="cm" /></div>
- <div class="field"><label for="ageInput">Age</label><input id="ageInput" type="number" min="8" max="100" step="1" placeholder="age" /></div>
- <div class="field"><label for="levelSelect">Training level</label><select id="levelSelect"></select></div>
- <div class="field"><label for="weekInput">Current week</label><input id="weekInput" type="number" min="1" max="12" step="1" /></div>
- <div class="field"><label for="pushupTest">Max clean push-ups</label><input id="pushupTest" type="number" min="0" max="300" step="1" placeholder="optional" /></div>
- <div class="field"><label for="hollowTest">Hollow hold seconds</label><input id="hollowTest" type="number" min="0" max="300" step="1" placeholder="optional" /></div>
- </div>
- <div class="pill-row" style="margin-top:14px">
- <button class="btn warn" id="autoLevelBtn">Auto-pick level from tests</button>
- <button class="btn ghost" id="saveSettingsBtn">Save settings</button>
- </div>
- <div id="bodyStats" class="small-text" style="margin-top:10px"></div>
- <h2 style="margin-top:24px">Equipment and recovery</h2>
- <div class="settings-grid">
- <label class="checkbox-panel"><input type="checkbox" id="noBarToggle"><span><strong>No-bar mode</strong><span class="small-text">Default for you. Replaces pull-ups, hanging abs, front lever and muscle-up work.</span></span></label>
- <label class="checkbox-panel"><input type="checkbox" id="wallToggle"><span><strong>Wall available</strong><span class="small-text">Adds wall handstand holds, wall walks, and HSPU negatives when your level allows.</span></span></label>
- <label class="checkbox-panel"><input type="checkbox" id="elevatedToggle"><span><strong>Chair / step available</strong><span class="small-text">Adds decline push-ups, Bulgarian split squats, and elevated bridge options.</span></span></label>
- <label class="checkbox-panel"><input type="checkbox" id="backpackToggle"><span><strong>Backpack load available</strong><span class="small-text">Adds optional load to push-ups, squats, bridges, and split squats.</span></span></label>
- <label class="checkbox-panel"><input type="checkbox" id="hardModeToggle"><span><strong>Hard mode</strong><span class="small-text">Adds a little more volume on higher levels. Keep form clean.</span></span></label>
- <label class="checkbox-panel"><input type="checkbox" id="deloadToggle"><span><strong>Deload this week</strong><span class="small-text">Cuts working sets roughly in half when recovery or joints are not good.</span></span></label>
- </div>
- </section>
- <aside class="panel">
- <h2>Level guide</h2>
- <div class="level-grid" id="levelCards"></div>
- <div class="warning-box"><strong>Upgrade rule:</strong> move up only when you can finish the top of the target range twice with honest tempo. If reps get ugly, stay down.</div>
- <h3>Data tools</h3>
- <div class="pill-row">
- <button class="btn ghost" id="exportBtn">Export data</button>
- <label class="btn ghost" for="importInput">Import data</label>
- <input id="importInput" type="file" accept="application/json" class="hidden" />
- <button class="btn danger" id="resetBtn">Reset progress</button>
- </div>
- </aside>
- </div>
- </section>
- <section class="tab-panel hidden" id="tab-progress">
- <section class="panel">
- <div class="day-header">
- <div>
- <h2>12-week completion grid</h2>
- <p class="muted">Use it like a command board. Green means the day’s exercises are checked complete.</p>
- </div>
- <div class="pill-row"><span class="pill good" id="totalProgressPill">0% total</span></div>
- </div>
- <div class="calendar-grid" id="calendarGrid"></div>
- </section>
- </section>
- <section class="tab-panel hidden" id="tab-bank">
- <section class="panel">
- <div class="day-header">
- <div>
- <h2>Exercise bank</h2>
- <p class="muted">Exercises with a saved YouTube ID play directly in the modal on localhost or any normal web URL. Double-click file mode shows a thumbnail link because YouTube blocks embedded playback from file pages.</p>
- </div>
- </div>
- <div class="search-row">
- <input id="bankSearch" placeholder="Search push, leg, core, back, burpee..." />
- <select id="bankCategory"><option value="all">All categories</option></select>
- </div>
- <div class="bank-grid" id="exerciseBank"></div>
- </section>
- </section>
- </main>
- </div>
- <div class="modal" id="demoModal" aria-hidden="true" role="dialog" aria-modal="true">
- <div class="modal-backdrop" data-close-modal></div>
- <div class="modal-card">
- <div class="modal-head">
- <h2 id="modalTitle">Exercise demo</h2>
- <button class="btn small" data-close-modal>Close</button>
- </div>
- <div class="modal-body" id="modalBody"></div>
- </div>
- </div>
- <div class="toast" id="toast"></div>
- <script>
- (function(){
- 'use strict';
- const STORAGE_KEY = 'ninjaMilitaryBodyweightApp.v1';
- const DAYS = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'];
- const LEVELS = {
- 1:{name:'Foundation', short:'L1', desc:'Build positions, joints, and basic reps. Hard but controlled.'},
- 2:{name:'Base Soldier', short:'L2', desc:'Standard bodyweight strength and clean endurance.'},
- 3:{name:'Hard Mode', short:'L3', desc:'Your default: strong work, honest tempo, real finishers.'},
- 4:{name:'Elite Street', short:'L4', desc:'Hard leverage, unilateral legs, more density.'},
- 5:{name:'Savage Ninja', short:'L5', desc:'Advanced leverage, high discipline, only if form stays clean.'}
- };
- const DEFAULT_STATE = {
- settings:{
- name:'Ninja', kg:'', cm:'', age:'', level:3, week:1,
- pushupTest:'', hollowTest:'',
- noBar:true, wall:true, elevated:false, backpack:false, hardMode:true, deload:false
- },
- selectedDay: browserDay(),
- completions:{},
- notes:{},
- activeTab:'today'
- };
- const EXERCISES = {
- wallPushup:{name:'Wall Push-Up', category:'Push', cue:'Straight body, ribs down, hands under shoulders.', demo:{type:'search', id:'5NPvv40gd3Q', search:'wall push up proper form tutorial'}},
- inclinePushup:{name:'Incline Push-Up', category:'Push', cue:'Use a high surface only if you have one; body stays straight.', demo:{type:'url', id:'wunqa8TX8V0', url:'https://calisthenics.com/exercise/incline-push-ups/', search:'incline push up proper form tutorial'}},
- kneePushup:{name:'Knee Push-Up', category:'Push', cue:'Hips forward, elbows 30-45 degrees, chest moves first.', demo:{type:'search', id:'lFR1GWy1Dcs', search:'knee push up proper form tutorial'}},
- pushup:{name:'Push-Up', category:'Push', cue:'Chest between hands; elbows 30-45 degrees; full-body plank.', demo:{type:'youtube', id:'SK1rjMpvN2g', search:'push up tutorial proper form'}},
- slowPushup:{name:'Slow Tempo Push-Up', category:'Push', cue:'3 seconds down, pause, explode up. No sagging hips.', demo:{type:'youtube', id:'SK1rjMpvN2g', search:'slow tempo push up tutorial'}},
- diamondPushup:{name:'Diamond Push-Up', category:'Push', cue:'Hands close, elbows tucked, keep shoulders down.', demo:{type:'search', id:'PPTj-MW2tcs', search:'diamond push up proper form tutorial'}},
- declinePushup:{name:'Decline Push-Up', category:'Push', cue:'Feet elevated, shoulders packed, full plank.', demo:{type:'url', id:'CqXnyT2GvyM', url:'https://calisthenics.com/exercise/decline-push-ups/', search:'decline push up tutorial proper form'}},
- archerPushup:{name:'Archer Push-Up', category:'Push', cue:'One arm bends, the other reaches long as an outrigger.', demo:{type:'url', id:'IDu6pRAPChg', url:'https://www.nasm.org/resource-center/exercise-library/archer-push-up', search:'archer push up tutorial proper form'}},
- pseudoPlanchePushup:{name:'Pseudo Planche Push-Up', category:'Push', cue:'Shoulders lean ahead of hands; body rigid like a board.', demo:{type:'url', id:'U_cN7q5etk4', url:'https://calisthenics.com/exercise/pseudo-planche-push-up/', search:'pseudo planche push up tutorial proper form'}},
- pikeHold:{name:'Pike Hold', category:'Vertical Push', cue:'Hips high, push the floor away, shoulders active.', demo:{type:'search', id:'eG20L9cl81w', search:'pike hold calisthenics shoulder exercise tutorial'}},
- pikePushup:{name:'Pike Push-Up', category:'Vertical Push', cue:'Hips high in an inverted V; head follows a tripod line.', demo:{type:'url', id:'mzSeCZ0ybgo', url:'https://calisthenics.com/exercise/pike-push-up/', search:'pike push up tutorial proper form'}},
- hardPikePushup:{name:'Hard Pike Push-Up', category:'Vertical Push', cue:'Longer range, slower negative, hips high.', demo:{type:'url', id:'mzSeCZ0ybgo', url:'https://calisthenics.com/exercise/pike-push-up/', search:'hard pike push up tutorial'}},
- elevatedPikePushup:{name:'Feet-Elevated Pike Push-Up', category:'Vertical Push', cue:'Feet up, vertical angle, slow lower.', demo:{type:'url', id:'R_c4snh-Zrs', url:'https://calisthenics.com/exercise/pike-push-up-elevated-feet/', search:'feet elevated pike push up tutorial'}},
- wallWalk:{name:'Wall Walk', category:'Skill', cue:'Feet climb wall, hands walk in, ribs down.', demo:{type:'url', id:'NK_OcHEm8yM', url:'https://calisthenics.com/exercise/wall-walk/', search:'wall walk exercise tutorial'}},
- wallHandstandHold:{name:'Wall Handstand Hold', category:'Skill', cue:'Stacked line, active shoulders, tight hollow body.', demo:{type:'youtube', id:'H2KSIMwNOfA', search:'wall handstand hold tutorial'}},
- wallHspuNegative:{name:'Wall HSPU Negative', category:'Skill', cue:'Lower slowly, elbows over hands, press the floor away.', demo:{type:'url', id:'EMYzJyBY6X0', url:'https://www.mpcalisthenics.com/tutorial/handstand-push-up-tutorial', search:'wall handstand push up negative tutorial'}},
- proneWRaise:{name:'Prone W Raise', category:'Back Balance', cue:'Lie face down, make a W, squeeze shoulder blades down and back.', demo:{type:'search', id:'cYLucJwoiFI', search:'prone W raise exercise tutorial'}},
- proneTRaise:{name:'Prone T Raise', category:'Back Balance', cue:'Arms in a T, thumbs up, lift from upper back not lower back.', demo:{type:'search', id:'cYLucJwoiFI', search:'prone T raise exercise tutorial'}},
- reverseSnowAngel:{name:'Reverse Snow Angel', category:'Back Balance', cue:'Face down, sweep arms from overhead to hips slowly.', demo:{type:'search', id:'0qLP2RNKX4A', search:'reverse snow angel exercise tutorial'}},
- proneCobra:{name:'Prone Cobra Hold', category:'Back Balance', cue:'Chest slightly up, palms out, shoulder blades back and down.', demo:{type:'search', id:'HyZDhy6EaSQ', search:'prone cobra hold exercise tutorial'}},
- supermanPulldown:{name:'Superman Pulldown', category:'Back Balance', cue:'Arms overhead, lift slightly, pull elbows to ribs like a lat pulldown.', demo:{type:'search', id:'D5LDqf2O41I', search:'superman pulldown bodyweight back exercise tutorial'}},
- selfRow:{name:'Self-Resisted One-Arm Row', category:'Back Balance', cue:'One arm rows while the other arm fights it. Pull hard toward ribs.', demo:{type:'search', id:'0_RaFyFTBjM', search:'self resisted one arm row exercise tutorial'}},
- selfCurl:{name:'Self-Resisted Curl', category:'Arms', cue:'One hand curls while the other presses down. Slow and honest.', demo:{type:'search', id:'vn7XSCqoas8', search:'self resisted biceps curl bodyweight tutorial'}},
- australianPullup:{name:'Australian Pull-Up', category:'Pull', cue:'Chest to bar, shoulders down/back, body stiff.', demo:{type:'url', id:'LKJTHSi6cUI', url:'https://calisthenics.com/exercise/australian-pull-up/', search:'australian pull up tutorial'}},
- sittingPullup:{name:'Sitting Pull-Up', category:'Pull', cue:'Use legs only as much as needed.', demo:{type:'url', id:'jhosgaDLDqw', url:'https://calisthenics.com/exercise/sitting-pull-up/', search:'sitting pull up tutorial'}},
- negativePullup:{name:'Negative Pull-Up', category:'Pull', cue:'Step to top; lower 3-8 seconds with control.', demo:{type:'url', id:'jC3iegvFqCM', url:'https://calisthenics.com/exercise/negative-pull-up/', search:'negative pull up tutorial'}},
- strictPullup:{name:'Strict Pull-Up', category:'Pull', cue:'Elbows drive down to ribs, no neck yanking.', demo:{type:'url', id:'56aJwxjkqqU', url:'https://calisthenics.com/exercise/strict-pull-up/', search:'strict pull up tutorial proper form'}},
- slowPullup:{name:'Slow Motion Pull-Up', category:'Pull', cue:'Own every inch up and down.', demo:{type:'url', id:'m_lV4i1x_2o', url:'https://calisthenics.com/exercise/slow-motion-pull-up/', search:'slow motion pull up tutorial'}},
- tuckFrontLever:{name:'Tuck Front Lever', category:'Skill', cue:'Knees tucked, shoulders depressed, back nearly parallel.', demo:{type:'url', id:'0BkXWaWNdZI', url:'https://calisthenics.com/exercise/tuck-front-lever/', search:'tuck front lever tutorial'}},
- lHangPullup:{name:'L-Hang Pull-Up', category:'Pull', cue:'Legs straight and horizontal while pulling.', demo:{type:'url', id:'6lIb1IZD2xw', url:'https://calisthenics.com/exercise/l-hang-pull-up/', search:'L hang pull up tutorial'}},
- prisonerSquat:{name:'Prisoner Squat', category:'Legs', cue:'Elbows wide, chest tall, heels heavy.', demo:{type:'url', id:'dfp0FCglFD8', url:'https://calisthenics.com/exercise/prisoner-squats/', search:'prisoner squat tutorial'}},
- squat:{name:'Bodyweight Squat', category:'Legs', cue:'Sit between knees, heels heavy, stand tall.', demo:{type:'url', id:'dfp0FCglFD8', url:'https://calisthenics.com/exercise/prisoner-squats/', search:'bodyweight squat tutorial proper form'}},
- reverseLunge:{name:'Reverse Lunge', category:'Legs', cue:'Step back softly; front shin mostly vertical; torso tall.', demo:{type:'url', id:'rJ8554LX64E', url:'https://calisthenics.com/exercise/reverse-lunges/', search:'reverse lunge tutorial'}},
- splitSquat:{name:'Floor Split Squat', category:'Legs', cue:'Long stance, front leg does the work, control down.', demo:{type:'search', id:'hXpGSa5HYqY', search:'bodyweight split squat tutorial proper form'}},
- bulgarianSplitSquat:{name:'Bulgarian Split Squat', category:'Legs', cue:'Use a chair/step only if safe. Front leg works.', demo:{type:'url', id:'DvtDA5zzeZs', url:'https://calisthenics.com/exercise/bulgarian-split-squats/', search:'bulgarian split squat tutorial'}},
- oneLegSquatNegative:{name:'Single-Leg Squat Negative', category:'Legs', cue:'Slow descent, control the knee, use hands for balance if needed.', demo:{type:'url', id:'yHO6qA1lXsI', url:'https://calisthenics.com/exercise/single-leg-squat-negatives/', search:'single leg squat negative tutorial'}},
- shrimpSquatNegative:{name:'Shrimp Squat Negative', category:'Legs', cue:'Slow negative, tap knee lightly, stand with control or assistance.', demo:{type:'search', id:'yQ6uKIYAezk', search:'shrimp squat negative tutorial'}},
- gluteBridge:{name:'Glute Bridge', category:'Posterior Chain', cue:'Ribs down, squeeze glutes, hips finish in line.', demo:{type:'url', id:'p02Vuc45ZqM', url:'https://calisthenics.com/exercise/glute-bridge/', search:'glute bridge tutorial'}},
- singleLegGluteBridge:{name:'Single-Leg Glute Bridge', category:'Posterior Chain', cue:'Keep hips square, squeeze hard at the top.', demo:{type:'url', id:'yGGtLacP0Rk', url:'https://calisthenics.com/exercise/single-leg-glute-bridges/', search:'single leg glute bridge tutorial'}},
- elevatedGluteBridge:{name:'Elevated Glute Bridge', category:'Posterior Chain', cue:'Feet elevated only if stable; make the bridge longer and harder.', demo:{type:'url', id:'kg6SBmgnGvo', url:'https://calisthenics.com/exercise/elevated-glute-bridges/', search:'elevated glute bridge tutorial'}},
- deadBug:{name:'Dead Bug', category:'Core', cue:'Lower back glued down; opposite arm and leg reach long.', demo:{type:'url', id:'5hCf41U7L9Y', url:'https://calisthenics.com/exercise/dead-bug/', search:'dead bug exercise tutorial'}},
- elbowPlank:{name:'Elbow Plank', category:'Core', cue:'Straight line ears to heels, glutes squeezed, no hip sag.', demo:{type:'url', id:'LzZCCl5x99Q', url:'https://calisthenics.com/exercise/elbow-plank/', search:'elbow plank proper form'}},
- sidePlank:{name:'Side Plank', category:'Core', cue:'Stack shoulder over elbow, hips high.', demo:{type:'url', id:'DTGQd-lR0E8', url:'https://calisthenics.com/exercise/side-plank/', search:'side plank tutorial'}},
- hollowHold:{name:'Hollow Body Hold', category:'Core', cue:'Banana shape; lower back pinned to floor; toes pointed.', demo:{type:'url', id:'zEKHs6f5W2k', url:'https://calisthenics.com/exercise/hollow-body-hold/', search:'hollow body hold tutorial'}},
- hollowRock:{name:'Hollow Rocks', category:'Core', cue:'Keep hollow shape while rocking; do not arch lower back.', demo:{type:'search', id:'tWZBamV2tjc', search:'hollow rocks exercise tutorial'}},
- floorLegRaise:{name:'Floor Leg Raise', category:'Core', cue:'Low back down, legs move as one lever, no swinging.', demo:{type:'search', id:'VWK9kgM2hBE', search:'floor leg raise exercise tutorial proper form'}},
- vUp:{name:'V-Up', category:'Core', cue:'Reach hands to feet, control down, keep abs tight.', demo:{type:'search', id:'zDkDAET3GUA', search:'V up exercise tutorial proper form'}},
- hangingKneeRaise:{name:'Hanging Knee Raise', category:'Core', cue:'Active hang first, knees rise with minimal swing.', demo:{type:'url', id:'6FwOvRdAE4k', url:'https://calisthenics.com/exercise/hanging-knee-raises/', search:'hanging knee raise tutorial'}},
- hangingLegRaise:{name:'Hanging Leg Raise', category:'Core', cue:'Legs rise as one lever, shoulders active, no kip.', demo:{type:'url', id:'YcCQhtWd4Rw', url:'https://calisthenics.com/exercise/hanging-leg-raises/', search:'hanging leg raise tutorial'}},
- toesToBar:{name:'Toes to Bar', category:'Core', cue:'Strict lever first; no wild kipping.', demo:{type:'url', id:'4_gTdfkbi5w', url:'https://calisthenics.com/exercise/toes-to-bar/', search:'toes to bar strict tutorial'}},
- flutterKicks:{name:'Flutter Kicks', category:'Core / Military', cue:'Low back down, small fast kicks, breathe.', demo:{type:'youtube', id:'bfMYh_-o8qA', search:'flutter kicks exercise tutorial'}},
- burpee:{name:'Burpee', category:'Conditioning', cue:'Squat, plank, push, jump. Land soft, keep rhythm.', demo:{type:'url', id:'5_ao1YpHYIY', url:'https://calisthenics.com/exercise/burpee/', search:'burpee tutorial proper form'}},
- mountainClimbers:{name:'Mountain Climbers', category:'Conditioning', cue:'Hands under shoulders, knees drive, hips stable.', demo:{type:'url', id:'kv_TpwgyklA', url:'https://calisthenics.com/exercise/mountain-climbers/', search:'mountain climbers tutorial'}},
- jumpingSquats:{name:'Jumping Squats', category:'Power / Conditioning', cue:'Explode up, land quietly, reset if form drops.', demo:{type:'url', id:'qrC2O-QN5SI', url:'https://calisthenics.com/exercise/jumping-squats/', search:'jump squat tutorial proper form'}},
- easyAerobic:{name:'Easy Walk / Jog', category:'Recovery', cue:'Conversational pace. You should feel fresher after it.', demo:{type:'search', id:'4gVN6S4Pe1Y', search:'easy run walk warm up tutorial conversational pace'}},
- mobilityFlow:{name:'Mobility Flow', category:'Recovery', cue:'Hips, ankles, shoulders, spine. Smooth, not painful.', demo:{type:'search', id:'G2ciDpFx1GM', search:'full body mobility flow no equipment tutorial'}},
- rest:{name:'Rest / Sleep / Walk', category:'Recovery', cue:'Recover so the next week is stronger.', demo:{type:'search', id:'wuWW5j54qEk', search:'active recovery walking mobility routine tutorial'}}
- };
- let state = loadState();
- function browserDay(){
- const d = new Date().getDay();
- return DAYS[(d + 6) % 7];
- }
- function loadState(){
- try{
- const raw = localStorage.getItem(STORAGE_KEY);
- if(!raw) return structuredClone(DEFAULT_STATE);
- const parsed = JSON.parse(raw);
- return deepMerge(structuredClone(DEFAULT_STATE), parsed);
- }catch(e){
- console.warn('Could not load state', e);
- return structuredClone(DEFAULT_STATE);
- }
- }
- function saveState(){localStorage.setItem(STORAGE_KEY, JSON.stringify(state));}
- function deepMerge(target, source){
- for(const key in source){
- if(source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])){
- target[key] = deepMerge(target[key] || {}, source[key]);
- }else target[key] = source[key];
- }
- return target;
- }
- function clamp(num,min,max){return Math.max(min, Math.min(max, Number(num)||min));}
- function esc(str){return String(str ?? '').replace(/[&<>'"]/g, c => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]));}
- function youtubeSearchUrl(q){return `https://www.youtube.com/results?search_query=${encodeURIComponent(q)}`;}
- function youtubeWatchUrl(id){return `https://www.youtube.com/watch?v=${encodeURIComponent(id)}`;}
- function youtubeEmbedUrl(id){
- const origin = /^https?:$/.test(location.protocol) ? `&origin=${encodeURIComponent(location.origin)}` : '';
- return `https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}?rel=0&modestbranding=1${origin}`;
- }
- function youtubeThumbUrl(id){return `https://img.youtube.com/vi/${encodeURIComponent(id)}/hqdefault.jpg`;}
- function isFileMode(){return location.protocol === 'file:';}
- function hasEmbeddableDemo(demo){return !!(demo && demo.id);}
- function demoButtonLabel(ex){return hasEmbeddableDemo(ex.demo) ? 'Demo' : 'Find demo';}
- function tempoText(prescription){
- const match = String(prescription || '').match(/@\s*([0-9Xx]{4})/);
- if(!match) return '';
- const code = match[1].toUpperCase();
- const part = ch => ch === 'X' ? 'explosive' : `${ch}s`;
- return `Tempo ${code}: ${part(code[0])} down, ${part(code[1])} bottom pause, ${part(code[2])} up, ${part(code[3])} top pause.`;
- }
- function getSettings(){return state.settings;}
- function level(){return clamp(getSettings().level,1,5);}
- function isNoBar(){return !!getSettings().noBar;}
- function setCount(base){
- const s = getSettings();
- let sets = base;
- if(s.hardMode && level() >= 4) sets += 1;
- if(s.hardMode && level() === 5 && base >= 4) sets += 1;
- if(s.deload) sets = Math.max(1, Math.ceil(sets * .55));
- return sets;
- }
- function reps(kind){
- const l = level();
- if(kind === 'strength') return l <= 2 ? '4-8 reps' : '3-6 reps';
- if(kind === 'hypertrophy') return l <= 2 ? '8-12 reps' : '8-15 reps';
- if(kind === 'endurance') return l <= 2 ? '20-30 sec' : '30-45 sec';
- if(kind === 'legs') return l <= 2 ? '8-12 each leg' : '8-15 each leg';
- return 'reps';
- }
- function maybeLoadNote(){
- if(getSettings().backpack) return 'Optional: add backpack only if form stays clean.';
- return '';
- }
- function select(slot){
- const s = getSettings();
- const l = level();
- const elevated = !!s.elevated;
- const wall = !!s.wall;
- const noBar = !!s.noBar;
- if(slot === 'horizontalPush'){
- if(l === 1) return s.elevated ? 'inclinePushup' : 'wallPushup';
- if(l === 2) return 'pushup';
- if(l === 3) return elevated ? 'declinePushup' : 'slowPushup';
- if(l === 4) return 'archerPushup';
- return 'pseudoPlanchePushup';
- }
- if(slot === 'verticalPush'){
- if(l === 1) return 'pikeHold';
- if(l === 2) return 'pikePushup';
- if(l === 3) return elevated ? 'elevatedPikePushup' : 'hardPikePushup';
- if(l === 4) return wall ? 'wallHandstandHold' : 'hardPikePushup';
- return wall ? 'wallHspuNegative' : 'hardPikePushup';
- }
- if(slot === 'wallSkill'){
- if(!wall) return 'pikeHold';
- if(l <= 2) return 'wallWalk';
- if(l === 3) return 'wallHandstandHold';
- return l === 4 ? 'wallHandstandHold' : 'wallHspuNegative';
- }
- if(slot === 'pullVertical'){
- if(noBar){
- if(l === 1) return 'proneWRaise';
- if(l === 2) return 'reverseSnowAngel';
- if(l === 3) return 'supermanPulldown';
- if(l === 4) return 'selfRow';
- return 'selfRow';
- }
- if(l === 1) return 'sittingPullup';
- if(l === 2) return 'negativePullup';
- if(l === 3) return 'strictPullup';
- if(l === 4) return 'slowPullup';
- return 'lHangPullup';
- }
- if(slot === 'pullHorizontal'){
- if(noBar){
- if(l === 1) return 'proneTRaise';
- if(l === 2) return 'proneWRaise';
- if(l === 3) return 'reverseSnowAngel';
- if(l === 4) return 'supermanPulldown';
- return 'proneCobra';
- }
- return 'australianPullup';
- }
- if(slot === 'pullSkill'){
- if(noBar) return l <= 2 ? 'proneCobra' : 'selfRow';
- return 'tuckFrontLever';
- }
- if(slot === 'armPull'){
- return noBar ? 'selfCurl' : 'strictPullup';
- }
- if(slot === 'legsMain'){
- if(l === 1) return 'squat';
- if(l === 2) return 'prisonerSquat';
- if(l === 3) return 'reverseLunge';
- if(l === 4) return elevated ? 'bulgarianSplitSquat' : 'splitSquat';
- return 'oneLegSquatNegative';
- }
- if(slot === 'posterior'){
- if(l === 1) return 'gluteBridge';
- if(l === 2) return 'singleLegGluteBridge';
- if(l === 3) return elevated ? 'elevatedGluteBridge' : 'singleLegGluteBridge';
- if(l === 4) return 'singleLegGluteBridge';
- return 'singleLegGluteBridge';
- }
- if(slot === 'core'){
- if(noBar){
- if(l === 1) return 'deadBug';
- if(l === 2) return 'elbowPlank';
- if(l === 3) return 'hollowHold';
- if(l === 4) return 'floorLegRaise';
- return 'hollowRock';
- }
- if(l === 1) return 'deadBug';
- if(l === 2) return 'elbowPlank';
- if(l === 3) return 'hollowHold';
- if(l === 4) return 'hangingKneeRaise';
- return 'hangingLegRaise';
- }
- if(slot === 'coreHard'){
- if(noBar){
- if(l <= 2) return 'flutterKicks';
- if(l === 3) return 'floorLegRaise';
- if(l === 4) return 'vUp';
- return 'hollowRock';
- }
- return l >= 5 ? 'toesToBar' : 'hangingKneeRaise';
- }
- return 'pushup';
- }
- function item(slot, exId, title, prescription, lane, rest, notes){
- return {slot, exId, title, prescription, lane, rest, notes: notes || ''};
- }
- function generateDay(day, weekOverride){
- const l = level();
- const s = getSettings();
- const mainRest = '2-3 min';
- const hypRest = '90-150 sec';
- const shortRest = '30-90 sec';
- const loadNote = maybeLoadNote();
- const deloadNote = s.deload ? 'Deload on: reduced sets this week.' : '';
- if(day === 'Monday'){
- return {
- title:'Push strength + handstand skill',
- theme:'Strength-skill lane',
- summary:'Vertical push first, horizontal push second, then skill and core. This is your upper-body strength day.',
- items:[
- item('mon-vertical', select('verticalPush'), 'Vertical push strength', `${setCount(5)} x ${reps('strength')} @ 31X1`, 'Strength', mainRest, 'Clean slow lower. ' + deloadNote),
- item('mon-horizontal', select('horizontalPush'), 'Horizontal push strength', `${setCount(4)} x 6-12 reps @ 3110`, 'Hypertrophy strength', hypRest, loadNote),
- item('mon-skill', select('wallSkill'), 'Skill hold / wall skill', `${setCount(4)} x 20-40 sec or 3-5 controlled reps`, 'Skill', '60-90 sec', 'Skip wall work if wrists or shoulders complain.'),
- item('mon-core', 'flutterKicks', 'Flutter kicks finisher', `${setCount(3)} x 30-45 sec`, 'Military core', '30 sec', 'Low back stays down.')
- ]
- };
- }
- if(day === 'Tuesday'){
- if(isNoBar()){
- return {
- title:'No-bar pull substitute + back balance',
- theme:'Back, rear delts, biceps, core',
- summary:'This replaces pull-ups/front lever work until you get a bar. It keeps the program balanced and trains your back as much as possible with no equipment.',
- items:[
- item('tue-back1', select('pullVertical'), 'Back substitute strength', `${setCount(5)} x 8-15 reps or 20-40 sec hold`, 'Back balance', hypRest, 'Squeeze hard. Imagine pulling elbows to ribs.'),
- item('tue-back2', select('pullHorizontal'), 'Rear delt / upper-back volume', `${setCount(4)} x 12-20 reps`, 'Hypertrophy', '60-90 sec', 'Slow reps. Do not rush.'),
- item('tue-row', 'selfRow', 'Self-resisted row', `${setCount(4)} x 8-12 each side`, 'Self-resistance', '60-90 sec', 'Your other arm creates the load. Make it hard.'),
- item('tue-curl', 'selfCurl', 'Self-resisted curl', `${setCount(3)} x 8-15 each side`, 'Arms', '45-75 sec', 'Slow up, slow down.'),
- item('tue-core', select('core'), 'Core position work', `${setCount(4)} x 20-40 sec or 8-20 reps`, 'Core', shortRest, '')
- ]
- };
- }
- return {
- title:'Pull strength + front lever skill',
- theme:'Original bar mode',
- summary:'Use this when you have a safe bar. Vertical pull, horizontal pull, lever work, then hanging abs.',
- items:[
- item('tue-vpull', select('pullVertical'), 'Vertical pull strength', `${setCount(5)} x ${reps('strength')} @ 31X1`, 'Strength', mainRest, ''),
- item('tue-hpull', select('pullHorizontal'), 'Horizontal pull hypertrophy', `${setCount(4)} x 6-12 reps @ 3111`, 'Hypertrophy', hypRest, ''),
- item('tue-lever', select('pullSkill'), 'Front lever lane', `${setCount(4)}-6 holds x 8-15 sec`, 'Skill', '60-90 sec', ''),
- item('tue-hangcore', select('coreHard'), 'Hanging core', `${setCount(3)} x 8-15 reps`, 'Core', shortRest, '')
- ]
- };
- }
- if(day === 'Wednesday'){
- return {
- title:'Legs hypertrophy + core',
- theme:'Legs and trunk',
- summary:'Main leg pattern, posterior chain, core, optional climber finisher.',
- items:[
- item('wed-legs', select('legsMain'), 'Main leg move', `${setCount(5)} x ${reps('legs')} or 8-15 bilateral @ 31X1`, 'Hypertrophy', hypRest, loadNote),
- item('wed-post', select('posterior'), 'Posterior-chain add-on', `${setCount(4)} x 10-20 reps @ 3012`, 'Glutes / hamstrings', '60-90 sec', loadNote),
- item('wed-core', select('core'), 'Core level work', `${setCount(4)} x 8-20 reps or 20-40 sec`, 'Core', shortRest, ''),
- item('wed-finisher', 'mountainClimbers', 'Optional climber finisher', `${s.deload ? 3 : 6} x 30 sec on / 30 sec off`, 'Military endurance', '30 sec', 'Optional. Skip if legs feel cooked.')
- ]
- };
- }
- if(day === 'Thursday'){
- return {
- title:'Recovery + base endurance',
- theme:'Easy aerobic work',
- summary:'Keep it easy enough that Friday feels fresh. This builds duration without stealing too much recovery.',
- items:[
- item('thu-aerobic', 'easyAerobic', 'Easy walk / jog', `${l <= 2 ? '20-30' : '30-40'} minutes at conversational pace`, 'Base endurance', 'None', 'You should be able to talk.'),
- item('thu-sideplank', 'sidePlank', 'Side plank', `${setCount(3)} x 20-40 sec each side`, 'Core stability', '30-60 sec', ''),
- item('thu-mobility', 'mobilityFlow', 'Mobility flow', '8-15 minutes', 'Recovery', 'None', 'Hips, ankles, shoulders, spine. No pain.')
- ]
- };
- }
- if(day === 'Friday'){
- return {
- title:'Upper hypertrophy + PRT density',
- theme:'Muscle + military density',
- summary:'Upper-body volume followed by a push-up/burpee EMOM. This is hard but short.',
- items:[
- item('fri-push', select('horizontalPush'), 'Horizontal push volume', `${setCount(4)} x 8-15 reps @ 3010`, 'Hypertrophy', hypRest, loadNote),
- item('fri-back', select('pullHorizontal'), isNoBar() ? 'Back balance volume' : 'Horizontal pull volume', `${setCount(4)} x 8-15 reps`, 'Hypertrophy', hypRest, ''),
- item('fri-vertical', select('verticalPush'), 'Vertical push volume', `${setCount(3)} x 6-10 reps`, 'Shoulders', '90-120 sec', ''),
- item('fri-pull', select('pullVertical'), isNoBar() ? 'Back substitute volume' : 'Vertical pull volume', `${setCount(3)} x 6-12 reps or 20-40 sec`, 'Pull balance', '90-120 sec', ''),
- item('fri-emom-push', 'pushup', 'EMOM odd minutes: push-ups', `${l <= 2 ? '10-14' : l === 3 ? '12-18' : '15-20'} reps x 5 rounds`, 'PRT density', 'Every odd minute', 'Leave a few seconds to breathe.'),
- item('fri-emom-burpee', 'burpee', 'EMOM even minutes: burpees', `${l <= 2 ? '5-6' : l === 3 ? '6-8' : '8-10'} reps x 5 rounds`, 'PRT density', 'Every even minute', '10 total minutes with push-ups.')
- ]
- };
- }
- if(day === 'Saturday'){
- const rounds = s.deload ? Math.max(2, l) : (l <= 2 ? 4 : l === 3 ? 5 : 6);
- return {
- title:'Legs work-capacity + hard military circuit',
- theme:'Grit day',
- summary:'Leg volume first, then the hard circuit. This is the weekly place for controlled suffering.',
- items:[
- item('sat-leg', select('legsMain'), 'Leg main move', `${setCount(4)} x 8-15 reps`, 'Legs', hypRest, loadNote),
- item('sat-post', select('posterior'), 'Posterior-chain add-on', `${setCount(3)} x 12-20 reps`, 'Glutes / hamstrings', '60-90 sec', loadNote),
- item('sat-c1', 'burpee', `Circuit: burpees`, `${rounds} rounds x ${l <= 2 ? '6-8' : '8-12'} reps`, 'Anaerobic', '60-90 sec after each round', 'Complete all circuit moves, then rest.'),
- item('sat-c2', 'jumpingSquats', `Circuit: jumping squats`, `${rounds} rounds x ${l <= 2 ? '8-10' : '10-15'} reps`, 'Power', 'In circuit', 'Land quietly.'),
- item('sat-c3', 'flutterKicks', `Circuit: flutter kicks`, `${rounds} rounds x ${l <= 2 ? '20-30' : '30-40'} sec`, 'Core endurance', 'In circuit', ''),
- item('sat-c4', 'mountainClimbers', `Circuit: mountain climbers`, `${rounds} rounds x ${l <= 2 ? '20-30' : '30-40'} sec`, 'Conditioning', 'In circuit', '')
- ]
- };
- }
- return {
- title:'Full rest',
- theme:'Recover and rebuild',
- summary:'Walk, stretch, eat, sleep. Do not turn rest day into another punishment day.',
- items:[
- item('sun-rest', 'rest', 'Full rest', 'No hard training', 'Recovery', 'All day', 'Easy walk is fine.'),
- item('sun-mobility', 'mobilityFlow', 'Optional easy mobility', '5-12 minutes', 'Recovery', 'None', 'Only if it helps you feel better.')
- ]
- };
- }
- function completionKey(week, day, slot, exId){return `w${week}:${day}:${slot}:${exId}`;}
- function noteKey(week, day){return `w${week}:${day}:notes`;}
- function isItemComplete(week, day, item){return !!state.completions[completionKey(week, day, item.slot, item.exId)];}
- function setItemComplete(week, day, item, val){
- const key = completionKey(week, day, item.slot, item.exId);
- if(val) state.completions[key] = true; else delete state.completions[key];
- saveState();
- }
- function dayItems(day, week){return generateDay(day, week).items;}
- function dayCompletion(week, day){
- const items = dayItems(day, week);
- if(!items.length) return {done:0,total:0,percent:0,complete:false};
- const done = items.filter(it => isItemComplete(week, day, it)).length;
- return {done,total:items.length,percent:Math.round((done/items.length)*100),complete:done===items.length};
- }
- function weekCompletion(week){
- let done=0,total=0;
- DAYS.forEach(d => { const c = dayCompletion(week,d); done += c.done; total += c.total; });
- return {done,total,percent: total ? Math.round((done/total)*100) : 0};
- }
- function totalCompletion(){
- let done=0,total=0;
- for(let w=1; w<=12; w++) DAYS.forEach(d=>{const c=dayCompletion(w,d); done+=c.done; total+=c.total;});
- return {done,total,percent: total ? Math.round((done/total)*100) : 0};
- }
- function render(){
- normalizeSettings();
- renderSelectors();
- renderStats();
- renderToday();
- renderWeek();
- renderSettings();
- renderProgress();
- renderBank();
- saveState();
- }
- function normalizeSettings(){
- const s = getSettings();
- s.level = clamp(s.level,1,5);
- s.week = clamp(s.week,1,12);
- if(!DAYS.includes(state.selectedDay)) state.selectedDay = browserDay();
- }
- function renderSelectors(){
- const weekSelect = document.getElementById('weekSelect');
- if(weekSelect.options.length !== 12){
- weekSelect.innerHTML = Array.from({length:12},(_,i)=>`<option value="${i+1}">Week ${i+1}</option>`).join('');
- }
- weekSelect.value = getSettings().week;
- const daySelect = document.getElementById('daySelect');
- if(daySelect.options.length !== DAYS.length){
- daySelect.innerHTML = DAYS.map(d=>`<option value="${d}">${d}</option>`).join('');
- }
- daySelect.value = state.selectedDay;
- const levelOptions = Object.entries(LEVELS).map(([k,v])=>`<option value="${k}">${k} - ${v.name}</option>`).join('');
- const quick = document.getElementById('quickLevelSelect');
- const full = document.getElementById('levelSelect');
- if(quick.innerHTML !== levelOptions) quick.innerHTML = levelOptions;
- if(full.innerHTML !== levelOptions) full.innerHTML = levelOptions;
- quick.value = getSettings().level;
- full.value = getSettings().level;
- }
- function renderStats(){
- const s = getSettings();
- document.getElementById('statWeek').textContent = s.week;
- document.getElementById('statLevel').textContent = LEVELS[level()].short;
- document.getElementById('statDay').textContent = state.selectedDay.slice(0,3);
- document.getElementById('statMode').textContent = s.noBar ? 'No bar' : 'Bar';
- const wc = weekCompletion(s.week);
- document.getElementById('weekProgressFill').style.width = wc.percent + '%';
- document.getElementById('weekProgressText').textContent = `${wc.percent}% complete (${wc.done}/${wc.total} items)`;
- }
- function renderToday(){
- const s = getSettings();
- const day = state.selectedDay;
- const plan = generateDay(day, s.week);
- const c = dayCompletion(s.week, day);
- const badge = day.slice(0,2).toUpperCase();
- document.getElementById('todayContent').innerHTML = `
- <div class="day-header">
- <div class="day-title">
- <div class="day-badge">${esc(badge)}</div>
- <div>
- <h2>${esc(day)} - ${esc(plan.title)}</h2>
- <div class="muted">${esc(plan.summary)}</div>
- </div>
- </div>
- <div>
- <span class="pill hot">${esc(plan.theme)}</span>
- <span class="pill good">${c.percent}% done</span>
- </div>
- </div>
- <div class="exercise-list">
- ${plan.items.map(it => renderExerciseCard(it, s.week, day)).join('')}
- </div>
- `;
- document.getElementById('todayPills').innerHTML = `
- <span class="pill hot">${esc(LEVELS[level()].name)}</span>
- <span class="pill good">${s.noBar ? 'No-bar substitutions on' : 'Bar mode on'}</span>
- <span class="pill">${s.deload ? 'Deload week' : 'Normal volume'}</span>
- <span class="pill">${s.hardMode ? 'Hard mode' : 'Standard mode'}</span>
- `;
- const noteEl = document.getElementById('dayNotes');
- noteEl.value = state.notes[noteKey(s.week, day)] || '';
- }
- function renderExerciseCard(it, week, day){
- const ex = EXERCISES[it.exId] || EXERCISES.pushup;
- const complete = isItemComplete(week, day, it);
- const tempo = tempoText(it.prescription);
- return `
- <article class="exercise-card ${complete ? 'complete' : ''}">
- <input class="complete-check" type="checkbox" ${complete ? 'checked' : ''} data-check="${esc(it.slot)}" data-exid="${esc(it.exId)}" aria-label="Mark ${esc(ex.name)} complete" />
- <div class="exercise-main">
- <h3>${esc(it.title)}: ${esc(ex.name)}</h3>
- <div class="small-text">${esc(ex.cue)}</div>
- <div class="exercise-sub">
- <span class="tag red">${esc(it.prescription)}</span>
- <span class="tag yellow">Rest: ${esc(it.rest)}</span>
- <span class="tag green">${esc(it.lane)}</span>
- ${it.notes ? `<span class="tag">${esc(it.notes)}</span>` : ''}
- </div>
- ${tempo ? `<div class="small-text" style="margin-top:7px">${esc(tempo)}</div>` : ''}
- </div>
- <div class="exercise-actions">
- <button class="btn small" data-demo="${esc(it.exId)}">${demoButtonLabel(ex)}</button>
- <button class="btn small ghost" data-toggle-check="${esc(it.slot)}" data-exid="${esc(it.exId)}">${complete ? 'Undo' : 'Done'}</button>
- </div>
- </article>
- `;
- }
- function renderWeek(){
- const s = getSettings();
- document.getElementById('weekLevelPill').textContent = `${LEVELS[level()].short} ${LEVELS[level()].name}`;
- document.getElementById('weekEquipmentPill').textContent = s.noBar ? 'No-bar mode' : 'Bar mode';
- document.getElementById('weekOverview').innerHTML = DAYS.map(day=>{
- const p = generateDay(day, s.week);
- const c = dayCompletion(s.week, day);
- const names = p.items.map(it => EXERCISES[it.exId]?.name || it.exId).slice(0,5).join(', ');
- return `<article class="week-card">
- <h3>${esc(day)}</h3>
- <div class="muted"><strong>${esc(p.title)}</strong></div>
- <p class="small-text">${esc(names)}${p.items.length > 5 ? '...' : ''}</p>
- <div class="mini-progress"><span style="width:${c.percent}%"></span></div>
- <div class="small-text" style="margin-top:8px">${c.done}/${c.total} complete</div>
- <button class="btn small ghost" data-open-day="${esc(day)}">Open day</button>
- </article>`;
- }).join('');
- }
- function renderSettings(){
- const s = getSettings();
- document.getElementById('nameInput').value = s.name || '';
- document.getElementById('kgInput').value = s.kg || '';
- document.getElementById('cmInput').value = s.cm || '';
- document.getElementById('ageInput').value = s.age || '';
- document.getElementById('weekInput').value = s.week;
- document.getElementById('pushupTest').value = s.pushupTest || '';
- document.getElementById('hollowTest').value = s.hollowTest || '';
- document.getElementById('noBarToggle').checked = !!s.noBar;
- document.getElementById('wallToggle').checked = !!s.wall;
- document.getElementById('elevatedToggle').checked = !!s.elevated;
- document.getElementById('backpackToggle').checked = !!s.backpack;
- document.getElementById('hardModeToggle').checked = !!s.hardMode;
- document.getElementById('deloadToggle').checked = !!s.deload;
- document.getElementById('levelCards').innerHTML = Object.entries(LEVELS).map(([k,v]) => `
- <div class="level-card ${Number(k)===level()?'active':''}">
- <h3>${esc(v.short)} - ${esc(v.name)}</h3>
- <p class="small-text">${esc(v.desc)}</p>
- <button class="btn small ghost" data-set-level="${k}">Use level ${k}</button>
- </div>
- `).join('');
- document.getElementById('bodyStats').innerHTML = bodyStatsHtml();
- }
- function bodyStatsHtml(){
- const s = getSettings();
- const kg = Number(s.kg), cm = Number(s.cm), age = Number(s.age);
- const parts = [];
- if(s.name) parts.push(`<strong>Call sign:</strong> ${esc(s.name)}`);
- if(kg && cm){
- const bmi = kg / Math.pow(cm/100, 2);
- parts.push(`<strong>BMI:</strong> ${bmi.toFixed(1)} <span class="muted">(just a rough stat; performance matters more here)</span>`);
- }
- if(age) parts.push(`<strong>Age:</strong> ${esc(age)}`);
- if(!parts.length) return 'Add kg, cm, and age if you want them saved. They do not leave this browser.';
- return parts.join(' • ');
- }
- function renderProgress(){
- const total = totalCompletion();
- document.getElementById('totalProgressPill').textContent = `${total.percent}% total (${total.done}/${total.total})`;
- const head = `<tr><th>Week</th>${DAYS.map(d=>`<th>${d.slice(0,3)}</th>`).join('')}</tr>`;
- const rows = Array.from({length:12},(_,i)=>{
- const w=i+1;
- const cells = DAYS.map(day=>{
- const c=dayCompletion(w,day);
- const restClass = day==='Sunday' ? ' rest' : '';
- return `<td><button class="cal-cell ${c.complete?'done':''}${restClass}" data-grid-week="${w}" data-grid-day="${esc(day)}" title="Week ${w} ${day}: ${c.done}/${c.total}">${c.complete?'✓':`${c.done}/${c.total}`}</button></td>`;
- }).join('');
- return `<tr><th>Week ${w}</th>${cells}</tr>`;
- }).join('');
- document.getElementById('calendarGrid').innerHTML = `<table class="calendar-table">${head}${rows}</table>`;
- }
- function renderBank(){
- const categorySel = document.getElementById('bankCategory');
- const categories = ['all', ...Array.from(new Set(Object.values(EXERCISES).map(e=>e.category))).sort()];
- const currentCat = categorySel.value || 'all';
- categorySel.innerHTML = categories.map(c=>`<option value="${esc(c)}">${esc(c === 'all' ? 'All categories' : c)}</option>`).join('');
- categorySel.value = categories.includes(currentCat) ? currentCat : 'all';
- const q = (document.getElementById('bankSearch').value || '').toLowerCase().trim();
- const filtered = Object.entries(EXERCISES).filter(([id,ex]) => {
- const catOK = categorySel.value === 'all' || ex.category === categorySel.value;
- const text = `${ex.name} ${ex.category} ${ex.cue}`.toLowerCase();
- return catOK && (!q || text.includes(q));
- });
- document.getElementById('exerciseBank').innerHTML = filtered.map(([id,ex]) => `
- <article class="bank-card">
- <h3>${esc(ex.name)}</h3>
- <div class="pill-row"><span class="pill">${esc(ex.category)}</span><span class="pill ${hasEmbeddableDemo(ex.demo)?'good':''}">${hasEmbeddableDemo(ex.demo) ? 'YouTube demo' : ex.demo.type === 'url' ? 'Tutorial page' : 'Search fallback'}</span></div>
- <p class="small-text">${esc(ex.cue)}</p>
- <button class="btn small" data-demo="${esc(id)}">${demoButtonLabel(ex)}</button>
- </article>
- `).join('') || `<div class="empty-state">No exercises match your search.</div>`;
- }
- function openDemo(exId){
- const ex = EXERCISES[exId];
- if(!ex) return;
- const demo = ex.demo || {type:'search', search:ex.name + ' tutorial'};
- const search = demo.search || `${ex.name} exercise tutorial proper form`;
- const modal = document.getElementById('demoModal');
- document.getElementById('modalTitle').textContent = `${ex.name} demo`;
- let body = '';
- if(hasEmbeddableDemo(demo)){
- if(isFileMode()){
- body = `<div class="video-box"><div class="video-thumb" role="img" aria-label="${esc(ex.name)} demo thumbnail"><img src="${esc(youtubeThumbUrl(demo.id))}" alt="${esc(ex.name)} demo thumbnail"><span class="play-badge">Use button below</span></div></div>`;
- }else{
- const src = youtubeEmbedUrl(demo.id);
- body = `<div class="video-box"><iframe src="${src}" title="${esc(ex.name)} YouTube demo" referrerpolicy="strict-origin-when-cross-origin" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe></div>`;
- }
- body += `<div class="link-grid"><a class="btn small ghost" target="_blank" rel="noopener" href="${esc(youtubeWatchUrl(demo.id))}">Open on YouTube</a>${demo.url ? `<a class="btn small ghost" target="_blank" rel="noopener" href="${esc(demo.url)}">Open tutorial page</a>` : ''}<a class="btn small ghost" target="_blank" rel="noopener" href="${esc(youtubeSearchUrl(search))}">Search more videos</a></div>`;
- }else if(demo.type === 'url' && demo.url){
- body = `<div class="video-box"><div class="placeholder"><strong>This tutorial page blocks embedded viewing.</strong><br>Use the tutorial page or YouTube search button below. I no longer load blocked pages inside this box, so the browser will not show the refused-to-connect iframe error.</div></div>`;
- body += `<div class="link-grid"><a class="btn small ghost" target="_blank" rel="noopener" href="${esc(demo.url)}">Open tutorial page</a><a class="btn small ghost" target="_blank" rel="noopener" href="${esc(youtubeSearchUrl(search))}">Search YouTube videos</a></div>`;
- }else{
- body = `<div class="video-box"><div class="placeholder"><strong>No fixed video ID saved for this drill.</strong><br>The button below opens a YouTube search for this exact exercise. I used search fallbacks when the PDF did not provide a direct YouTube video.</div></div>`;
- body += `<div class="link-grid"><a class="btn small primary" target="_blank" rel="noopener" href="${esc(youtubeSearchUrl(search))}">Search YouTube videos</a></div>`;
- }
- body += `<div class="quote-box"><strong>Form cue:</strong> ${esc(ex.cue)}</div>`;
- document.getElementById('modalBody').innerHTML = body;
- modal.classList.add('open');
- modal.setAttribute('aria-hidden','false');
- }
- function closeModal(){
- const modal = document.getElementById('demoModal');
- modal.classList.remove('open');
- modal.setAttribute('aria-hidden','true');
- document.getElementById('modalBody').innerHTML = '';
- }
- function toast(msg){
- const el = document.getElementById('toast');
- el.textContent = msg;
- el.classList.add('show');
- clearTimeout(toast._t);
- toast._t = setTimeout(()=>el.classList.remove('show'), 2400);
- }
- function setActiveTab(tab){
- state.activeTab = tab;
- document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tab));
- document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('hidden', panel.id !== `tab-${tab}`));
- saveState();
- }
- function updateSettingsFromForm(){
- const s = getSettings();
- s.name = document.getElementById('nameInput').value.trim();
- s.kg = document.getElementById('kgInput').value;
- s.cm = document.getElementById('cmInput').value;
- s.age = document.getElementById('ageInput').value;
- s.level = clamp(document.getElementById('levelSelect').value,1,5);
- s.week = clamp(document.getElementById('weekInput').value,1,12);
- s.pushupTest = document.getElementById('pushupTest').value;
- s.hollowTest = document.getElementById('hollowTest').value;
- s.noBar = document.getElementById('noBarToggle').checked;
- s.wall = document.getElementById('wallToggle').checked;
- s.elevated = document.getElementById('elevatedToggle').checked;
- s.backpack = document.getElementById('backpackToggle').checked;
- s.hardMode = document.getElementById('hardModeToggle').checked;
- s.deload = document.getElementById('deloadToggle').checked;
- saveState();
- }
- function autoPickLevel(){
- const s = getSettings();
- const push = Number(document.getElementById('pushupTest').value || s.pushupTest || 0);
- const hollow = Number(document.getElementById('hollowTest').value || s.hollowTest || 0);
- let score = 1;
- if(push >= 10 || hollow >= 20) score = 2;
- if(push >= 20 && hollow >= 30) score = 3;
- if(push >= 35 && hollow >= 45) score = 4;
- if(push >= 50 && hollow >= 60) score = 5;
- s.level = score;
- s.pushupTest = document.getElementById('pushupTest').value;
- s.hollowTest = document.getElementById('hollowTest').value;
- render();
- toast(`Auto-set to Level ${score}: ${LEVELS[score].name}`);
- }
- function exportData(){
- const payload = {
- app:'Ninja Military Bodyweight Web App',
- storageKey:STORAGE_KEY,
- exportedAt:new Date().toISOString(),
- state:structuredClone(state)
- };
- const blob = new Blob([JSON.stringify(payload,null,2)], {type:'application/json'});
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `ninja-bodyweight-data-week-${getSettings().week}.json`;
- document.body.appendChild(a);
- a.click();
- a.remove();
- URL.revokeObjectURL(url);
- }
- function importData(file){
- if(!file) return;
- const reader = new FileReader();
- reader.onload = () => {
- try{
- const imported = JSON.parse(reader.result);
- const importedState = imported && imported.state ? imported.state : imported;
- if(!importedState || typeof importedState !== 'object' || !importedState.settings){
- throw new Error('Missing app state');
- }
- state = deepMerge(structuredClone(DEFAULT_STATE), importedState);
- saveState();
- render();
- toast('Imported data.');
- }catch(e){
- alert('That file was not valid JSON for this app.');
- }
- };
- reader.readAsText(file);
- }
- function bindEvents(){
- document.querySelectorAll('.tab-btn').forEach(btn => btn.addEventListener('click', () => setActiveTab(btn.dataset.tab)));
- document.querySelectorAll('[data-tab-jump]').forEach(btn => btn.addEventListener('click', () => {
- if(btn.hasAttribute('data-set-today')) state.selectedDay = browserDay();
- setActiveTab(btn.dataset.tabJump);
- render();
- }));
- document.getElementById('printBtn').addEventListener('click', () => window.print());
- document.getElementById('weekSelect').addEventListener('change', e => {getSettings().week = clamp(e.target.value,1,12); render();});
- document.getElementById('daySelect').addEventListener('change', e => {state.selectedDay = e.target.value; render();});
- document.getElementById('todayDayBtn').addEventListener('click', () => {state.selectedDay = browserDay(); render(); toast(`Selected ${state.selectedDay}.`);});
- document.getElementById('quickLevelSelect').addEventListener('change', e => {getSettings().level = clamp(e.target.value,1,5); render();});
- document.getElementById('dayNotes').addEventListener('input', e => {state.notes[noteKey(getSettings().week, state.selectedDay)] = e.target.value; saveState();});
- document.getElementById('markAllBtn').addEventListener('click', () => {
- const w = getSettings().week, d = state.selectedDay;
- dayItems(d,w).forEach(it => setItemComplete(w,d,it,true));
- render(); toast('Day marked complete.');
- });
- document.getElementById('clearDayBtn').addEventListener('click', () => {
- const w = getSettings().week, d = state.selectedDay;
- dayItems(d,w).forEach(it => setItemComplete(w,d,it,false));
- render(); toast('Day cleared.');
- });
- ['nameInput','kgInput','cmInput','ageInput','levelSelect','weekInput','pushupTest','hollowTest','noBarToggle','wallToggle','elevatedToggle','backpackToggle','hardModeToggle','deloadToggle'].forEach(id => {
- document.getElementById(id).addEventListener('change', () => { updateSettingsFromForm(); render(); toast('Settings saved.'); });
- });
- document.getElementById('saveSettingsBtn').addEventListener('click', () => {updateSettingsFromForm(); render(); toast('Settings saved.');});
- document.getElementById('autoLevelBtn').addEventListener('click', autoPickLevel);
- document.getElementById('exportBtn').addEventListener('click', exportData);
- document.getElementById('importInput').addEventListener('change', e => importData(e.target.files[0]));
- document.getElementById('resetBtn').addEventListener('click', () => {
- if(confirm('Reset all completion marks and notes? Settings will stay.')){
- state.completions = {}; state.notes = {}; saveState(); render(); toast('Progress reset.');
- }
- });
- document.getElementById('bankSearch').addEventListener('input', renderBank);
- document.getElementById('bankCategory').addEventListener('change', renderBank);
- document.addEventListener('click', e => {
- const demoBtn = e.target.closest('[data-demo]');
- if(demoBtn){ openDemo(demoBtn.dataset.demo); return; }
- const close = e.target.closest('[data-close-modal]');
- if(close){ closeModal(); return; }
- const doneBtn = e.target.closest('[data-toggle-check]');
- if(doneBtn){
- const w=getSettings().week, d=state.selectedDay;
- const slot=doneBtn.dataset.toggleCheck, exId=doneBtn.dataset.exid;
- const it = dayItems(d,w).find(x => x.slot===slot && x.exId===exId);
- if(it) setItemComplete(w,d,it,!isItemComplete(w,d,it));
- render();
- return;
- }
- const openDay = e.target.closest('[data-open-day]');
- if(openDay){ state.selectedDay = openDay.dataset.openDay; setActiveTab('today'); render(); return; }
- const setLevel = e.target.closest('[data-set-level]');
- if(setLevel){ getSettings().level = clamp(setLevel.dataset.setLevel,1,5); render(); toast(`Level ${getSettings().level} selected.`); return; }
- const grid = e.target.closest('[data-grid-week]');
- if(grid){ getSettings().week = clamp(grid.dataset.gridWeek,1,12); state.selectedDay = grid.dataset.gridDay; setActiveTab('today'); render(); return; }
- });
- document.addEventListener('change', e => {
- const cb = e.target.closest('[data-check]');
- if(cb){
- const w = getSettings().week, d = state.selectedDay;
- const it = dayItems(d,w).find(x => x.slot === cb.dataset.check && x.exId === cb.dataset.exid);
- if(it) setItemComplete(w,d,it,cb.checked);
- render();
- }
- });
- document.addEventListener('keydown', e => { if(e.key === 'Escape') closeModal(); });
- }
- bindEvents();
- renderSelectors();
- setActiveTab(state.activeTab || 'today');
- render();
- })();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment