noob6969

Untitled

May 31st, 2026
25
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 77.32 KB | None | 0 0
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>Ninja Military Bodyweight Web App</title>
  7. <style>
  8. :root{
  9. --bg:#071018;
  10. --bg2:#0c1824;
  11. --panel:#111d29;
  12. --panel2:#172736;
  13. --panel3:#0d1721;
  14. --line:#2b4258;
  15. --text:#eef6ff;
  16. --muted:#a9bacb;
  17. --muted2:#72869a;
  18. --accent:#ff3b2f;
  19. --accent2:#ffbd2e;
  20. --good:#37d982;
  21. --good2:#123a28;
  22. --warn:#ffc857;
  23. --bad:#ff6b6b;
  24. --shadow:0 18px 45px rgba(0,0,0,.35);
  25. --radius:18px;
  26. }
  27. *{box-sizing:border-box}
  28. html{scroll-behavior:smooth}
  29. body{
  30. margin:0;
  31. font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
  32. color:var(--text);
  33. background:
  34. radial-gradient(900px 550px at 15% -10%, rgba(255,59,47,.20), transparent 70%),
  35. radial-gradient(700px 500px at 90% 10%, rgba(255,189,46,.13), transparent 70%),
  36. linear-gradient(180deg,var(--bg),var(--bg2));
  37. min-height:100vh;
  38. }
  39. a{color:#9bd0ff}
  40. button,input,select,textarea{font:inherit}
  41. button{cursor:pointer}
  42. .app-shell{max-width:1220px;margin:0 auto;padding:22px 18px 60px}
  43. .hero{
  44. display:grid;
  45. grid-template-columns: 1.35fr .65fr;
  46. gap:18px;
  47. align-items:stretch;
  48. margin-bottom:18px;
  49. }
  50. .hero-main,.hero-side,.panel{
  51. background:linear-gradient(180deg,rgba(23,39,54,.92),rgba(13,23,33,.94));
  52. border:1px solid rgba(154,208,255,.16);
  53. border-radius:var(--radius);
  54. box-shadow:var(--shadow);
  55. overflow:hidden;
  56. position:relative;
  57. }
  58. .hero-main:before{
  59. content:"";
  60. position:absolute;inset:0;
  61. background:
  62. linear-gradient(120deg,rgba(255,59,47,.18),transparent 34%),
  63. repeating-linear-gradient(135deg,rgba(255,255,255,.04) 0,rgba(255,255,255,.04) 1px,transparent 1px,transparent 12px);
  64. pointer-events:none;
  65. }
  66. .hero-content{position:relative;padding:28px}
  67. .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}
  68. .kicker .dot{width:8px;height:8px;border-radius:999px;background:var(--accent);box-shadow:0 0 14px var(--accent)}
  69. h1{font-size:clamp(2.05rem,5vw,4.6rem);line-height:.95;margin:12px 0 10px;text-transform:uppercase;letter-spacing:-.06em}
  70. h1 span{color:var(--accent);text-shadow:0 0 20px rgba(255,59,47,.23)}
  71. .subtitle{color:var(--muted);font-size:1.03rem;line-height:1.55;max-width:820px;margin:0}
  72. .hero-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}
  73. .btn{
  74. border:1px solid rgba(255,255,255,.12);
  75. background:#203040;
  76. color:var(--text);
  77. border-radius:999px;
  78. padding:10px 14px;
  79. font-weight:800;
  80. display:inline-flex;align-items:center;gap:8px;
  81. transition:.18s transform,.18s background,.18s border-color;
  82. text-decoration:none;
  83. }
  84. .btn:hover{transform:translateY(-1px);background:#2a3d50;border-color:rgba(255,255,255,.24)}
  85. .btn.primary{background:linear-gradient(135deg,var(--accent),#ff7a29);border-color:transparent;color:#fff}
  86. .btn.good{background:linear-gradient(135deg,#20b968,#26d885);border-color:transparent;color:#04130b}
  87. .btn.warn{background:linear-gradient(135deg,#ffbd2e,#ffe08a);border-color:transparent;color:#2a1700}
  88. .btn.ghost{background:transparent}
  89. .btn.small{padding:7px 10px;font-size:.86rem}
  90. .btn.danger{background:#3a1720;border-color:#6e2635;color:#ffbdc6}
  91. .hero-side{padding:18px;display:flex;flex-direction:column;gap:12px}
  92. .stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
  93. .stat{
  94. background:rgba(255,255,255,.04);
  95. border:1px solid rgba(255,255,255,.09);
  96. border-radius:15px;
  97. padding:13px;
  98. min-height:74px;
  99. }
  100. .stat .label{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;font-weight:800}
  101. .stat .value{font-size:1.35rem;font-weight:900;margin-top:4px}
  102. .progress-wrap{height:10px;background:#09131d;border-radius:99px;border:1px solid rgba(255,255,255,.08);overflow:hidden}
  103. .progress-fill{height:100%;width:0;background:linear-gradient(90deg,var(--accent),var(--accent2));transition:width .25s ease}
  104. .tabs{
  105. position:sticky;top:0;z-index:10;
  106. display:flex;gap:8px;flex-wrap:wrap;
  107. padding:10px 0 14px;
  108. backdrop-filter: blur(10px);
  109. }
  110. .tab-btn{
  111. color:var(--muted);
  112. background:rgba(9,19,29,.78);
  113. border:1px solid rgba(255,255,255,.09);
  114. border-radius:999px;
  115. padding:10px 13px;
  116. font-weight:900;
  117. }
  118. .tab-btn.active{color:#fff;background:linear-gradient(135deg,#26394b,#1b2b3a);border-color:rgba(255,255,255,.22)}
  119. .grid{display:grid;gap:18px}
  120. .two-col{grid-template-columns:.72fr .28fr}
  121. .three-col{grid-template-columns:repeat(3,1fr)}
  122. .panel{padding:18px}
  123. .panel h2,.panel h3{margin:0 0 10px;letter-spacing:-.03em}
  124. .panel h2{font-size:1.45rem}
  125. .panel h3{font-size:1.05rem}
  126. .muted{color:var(--muted)}
  127. .small-text{font-size:.88rem;color:var(--muted);line-height:1.45}
  128. .pill-row{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0}
  129. .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}
  130. .pill.hot{color:#fff;background:rgba(255,59,47,.18);border-color:rgba(255,59,47,.32)}
  131. .pill.good{color:#baffe1;background:rgba(55,217,130,.12);border-color:rgba(55,217,130,.28)}
  132. .toolbar{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:12px 0 16px}
  133. .field{display:flex;flex-direction:column;gap:6px;min-width:135px;flex:1}
  134. .field.inline{flex-direction:row;align-items:center;min-width:auto;gap:9px}
  135. label,.field-label{font-size:.78rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:900}
  136. input,select,textarea{
  137. background:#0a141f;
  138. color:var(--text);
  139. border:1px solid rgba(255,255,255,.12);
  140. border-radius:12px;
  141. padding:10px 11px;
  142. outline:none;
  143. }
  144. textarea{min-height:92px;resize:vertical;width:100%}
  145. input:focus,select:focus,textarea:focus{border-color:rgba(255,189,46,.65);box-shadow:0 0 0 3px rgba(255,189,46,.12)}
  146. input[type="checkbox"]{width:19px;height:19px;accent-color:var(--good);padding:0}
  147. .day-header{
  148. display:flex;align-items:flex-start;justify-content:space-between;gap:14px;flex-wrap:wrap;
  149. border-bottom:1px solid rgba(255,255,255,.09);padding-bottom:14px;margin-bottom:14px;
  150. }
  151. .day-title{display:flex;gap:12px;align-items:center}
  152. .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)}
  153. .day-title h2{margin:0}
  154. .exercise-list{display:grid;gap:12px}
  155. .exercise-card{
  156. display:grid;grid-template-columns:auto 1fr auto;gap:12px;align-items:flex-start;
  157. background:linear-gradient(180deg,rgba(255,255,255,.045),rgba(255,255,255,.025));
  158. border:1px solid rgba(255,255,255,.10);
  159. border-radius:16px;
  160. padding:14px;
  161. transition:.18s border-color,.18s background,.18s transform;
  162. }
  163. .exercise-card:hover{border-color:rgba(255,255,255,.20);transform:translateY(-1px)}
  164. .exercise-card.complete{background:linear-gradient(180deg,rgba(55,217,130,.13),rgba(55,217,130,.06));border-color:rgba(55,217,130,.34)}
  165. .exercise-main h3{margin:0 0 4px;font-size:1.05rem}
  166. .exercise-sub{display:flex;gap:8px;flex-wrap:wrap;margin:7px 0 0}
  167. .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}
  168. .tag.red{color:#ffd4d0;background:rgba(255,59,47,.13);border-color:rgba(255,59,47,.26)}
  169. .tag.yellow{color:#ffe7a6;background:rgba(255,189,46,.13);border-color:rgba(255,189,46,.26)}
  170. .tag.green{color:#baffdd;background:rgba(55,217,130,.13);border-color:rgba(55,217,130,.26)}
  171. .exercise-actions{display:flex;gap:7px;align-items:center;flex-wrap:wrap;justify-content:flex-end}
  172. .complete-check{margin-top:8px}
  173. .empty-state{padding:28px;border:1px dashed rgba(255,255,255,.18);border-radius:16px;color:var(--muted);text-align:center}
  174. .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}
  175. .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}
  176. .level-grid,.bank-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px}
  177. .level-card,.bank-card,.week-card{
  178. background:rgba(255,255,255,.04);
  179. border:1px solid rgba(255,255,255,.10);
  180. border-radius:16px;
  181. padding:14px;
  182. }
  183. .level-card.active{border-color:rgba(255,189,46,.62);background:rgba(255,189,46,.09)}
  184. .level-card h3,.bank-card h3,.week-card h3{margin:0 0 8px}
  185. .week-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
  186. .mini-progress{height:8px;border-radius:99px;background:#071018;overflow:hidden;border:1px solid rgba(255,255,255,.08);margin-top:10px}
  187. .mini-progress span{display:block;height:100%;background:linear-gradient(90deg,var(--good),var(--accent2));width:0}
  188. .calendar-grid{overflow:auto;padding-bottom:8px}
  189. .calendar-table{border-collapse:separate;border-spacing:6px;min-width:640px;width:100%}
  190. .calendar-table th{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;text-align:center;padding:5px}
  191. .calendar-table td{padding:0;text-align:center}
  192. .cal-cell{
  193. width:100%;min-height:42px;border-radius:10px;background:#0b1520;border:1px solid rgba(255,255,255,.10);color:var(--muted);font-weight:900;
  194. display:grid;place-items:center;
  195. }
  196. .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}
  197. .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}
  198. .settings-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}
  199. .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}
  200. .checkbox-panel strong{display:block;margin-bottom:3px}
  201. .table-like{width:100%;border-collapse:collapse;margin-top:8px}
  202. .table-like th,.table-like td{border-bottom:1px solid rgba(255,255,255,.08);padding:9px;text-align:left;vertical-align:top}
  203. .table-like th{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
  204. .search-row{display:flex;gap:10px;align-items:center;margin:12px 0;flex-wrap:wrap}
  205. .search-row input{flex:1;min-width:220px}
  206. .modal{position:fixed;inset:0;z-index:999;display:none}
  207. .modal.open{display:block}
  208. .modal-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(6px)}
  209. .modal-card{
  210. position:relative;
  211. width:min(980px,calc(100vw - 24px));
  212. max-height:calc(100vh - 32px);
  213. margin:16px auto;
  214. background:#0d1721;
  215. border:1px solid rgba(255,255,255,.16);
  216. border-radius:20px;
  217. box-shadow:var(--shadow);
  218. overflow:hidden;
  219. display:flex;
  220. flex-direction:column;
  221. }
  222. .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)}
  223. .modal-head h2{margin:0;font-size:1.08rem}
  224. .modal-body{padding:16px;overflow:auto}
  225. .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}
  226. .video-box iframe{width:100%;height:100%;border:0;background:#050a0f}
  227. .video-thumb{position:relative;display:block;width:100%;height:100%;color:#fff;text-decoration:none;background:#050a0f}
  228. .video-thumb img{width:100%;height:100%;object-fit:cover;display:block;opacity:.78}
  229. .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)}
  230. .play-badge:before{content:"";width:58px;height:40px;border-radius:12px;background:#ff0000;box-shadow:0 10px 28px rgba(0,0,0,.35)}
  231. .play-badge:after{content:"";position:absolute;left:24px;border-left:15px solid white;border-top:10px solid transparent;border-bottom:10px solid transparent}
  232. .link-grid{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
  233. .placeholder{padding:24px;text-align:center;color:var(--muted);line-height:1.5}
  234. .toast{
  235. position:fixed;right:16px;bottom:16px;background:#101d29;border:1px solid rgba(255,255,255,.14);color:#fff;
  236. 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))
  237. }
  238. .toast.show{opacity:1;transform:translateY(0)}
  239. .hidden{display:none!important}
  240. @media (max-width:900px){
  241. .hero,.two-col{grid-template-columns:1fr}
  242. .stat-grid{grid-template-columns:repeat(2,1fr)}
  243. .exercise-card{grid-template-columns:auto 1fr}
  244. .exercise-actions{grid-column:1 / -1;justify-content:flex-start}
  245. .tabs{position:relative}
  246. }
  247. @media (max-width:560px){
  248. .app-shell{padding:14px 10px 40px}
  249. .hero-content{padding:22px 18px}
  250. .stat-grid{grid-template-columns:1fr}
  251. .toolbar .field{min-width:100%}
  252. .day-badge{width:42px;height:42px}
  253. .exercise-card{padding:12px}
  254. }
  255. @media print{
  256. body{background:white;color:black}
  257. .tabs,.hero-actions,.exercise-actions,.toolbar button,.modal,.toast{display:none!important}
  258. .panel,.hero-main,.hero-side{box-shadow:none;border:1px solid #bbb;background:white;color:black}
  259. .subtitle,.muted,.small-text,.pill,.tag{color:#333}
  260. }
  261. </style>
  262. </head>
  263. <body>
  264. <div class="app-shell">
  265. <section class="hero" aria-label="App header">
  266. <div class="hero-main">
  267. <div class="hero-content">
  268. <div class="kicker"><span class="dot"></span><span>No-bar hard mode</span><span>•</span><span>calisthenics + military PRT</span></div>
  269. <h1>Ninja <span>Bodyweight</span> Command</h1>
  270. <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>
  271. <div class="hero-actions">
  272. <button class="btn primary" data-tab-jump="today" data-set-today>Start today</button>
  273. <button class="btn ghost" data-tab-jump="settings">Change level</button>
  274. <button class="btn ghost" data-tab-jump="progress">See progress</button>
  275. <button class="btn ghost" id="printBtn">Print plan</button>
  276. </div>
  277. </div>
  278. </div>
  279. <aside class="hero-side" aria-label="Quick stats">
  280. <div class="stat-grid">
  281. <div class="stat"><div class="label">Week</div><div class="value" id="statWeek">1</div></div>
  282. <div class="stat"><div class="label">Level</div><div class="value" id="statLevel">3</div></div>
  283. <div class="stat"><div class="label">Today</div><div class="value" id="statDay">Mon</div></div>
  284. <div class="stat"><div class="label">Mode</div><div class="value" id="statMode">No bar</div></div>
  285. </div>
  286. <div>
  287. <div class="field-label">Current week completion</div>
  288. <div class="progress-wrap" aria-label="Current week completion"><div class="progress-fill" id="weekProgressFill"></div></div>
  289. <div class="small-text" id="weekProgressText" style="margin-top:8px">0% complete</div>
  290. </div>
  291. </aside>
  292. </section>
  293.  
  294. <nav class="tabs" aria-label="Main tabs">
  295. <button class="tab-btn active" data-tab="today">Today</button>
  296. <button class="tab-btn" data-tab="week">Week Plan</button>
  297. <button class="tab-btn" data-tab="settings">Settings</button>
  298. <button class="tab-btn" data-tab="progress">Progress</button>
  299. <button class="tab-btn" data-tab="bank">Exercise Bank</button>
  300. </nav>
  301.  
  302. <main>
  303. <section class="tab-panel" id="tab-today">
  304. <div class="grid two-col">
  305. <section class="panel">
  306. <div class="toolbar">
  307. <div class="field">
  308. <label for="weekSelect">Training week</label>
  309. <select id="weekSelect"></select>
  310. </div>
  311. <div class="field">
  312. <label for="daySelect">Day</label>
  313. <select id="daySelect"></select>
  314. </div>
  315. <button class="btn ghost" id="todayDayBtn" type="button">Today</button>
  316. <div class="field">
  317. <label for="quickLevelSelect">Level</label>
  318. <select id="quickLevelSelect"></select>
  319. </div>
  320. <button class="btn good" id="markAllBtn">Mark all complete</button>
  321. <button class="btn danger" id="clearDayBtn">Clear day</button>
  322. </div>
  323. <div id="todayContent"></div>
  324. </section>
  325.  
  326. <aside class="panel">
  327. <h2>Rules of the app</h2>
  328. <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>
  329. <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>
  330. <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>
  331. <div class="pill-row" id="todayPills"></div>
  332. <h3 style="margin-top:16px">Day notes</h3>
  333. <textarea id="dayNotes" placeholder="Write soreness, reps achieved, run time, or anything you noticed..."></textarea>
  334. <div class="small-text" style="margin-top:8px">Notes and checkboxes save automatically in this browser.</div>
  335. </aside>
  336. </div>
  337. </section>
  338.  
  339. <section class="tab-panel hidden" id="tab-week">
  340. <section class="panel">
  341. <div class="day-header">
  342. <div>
  343. <h2>Full week plan</h2>
  344. <p class="muted">The week follows the original blueprint structure, but bar work is swapped when No-bar mode is on.</p>
  345. </div>
  346. <div class="pill-row"><span class="pill hot" id="weekLevelPill">Level 3</span><span class="pill good" id="weekEquipmentPill">No-bar mode</span></div>
  347. </div>
  348. <div class="week-grid" id="weekOverview"></div>
  349. </section>
  350. </section>
  351.  
  352. <section class="tab-panel hidden" id="tab-settings">
  353. <div class="grid two-col">
  354. <section class="panel">
  355. <h2>General settings</h2>
  356. <p class="muted">Change your body stats, level, and equipment. The daily exercises update instantly.</p>
  357. <div class="settings-grid">
  358. <div class="field"><label for="nameInput">Name / call sign</label><input id="nameInput" placeholder="Ninja" /></div>
  359. <div class="field"><label for="kgInput">Bodyweight kg</label><input id="kgInput" type="number" min="20" max="250" step="0.1" placeholder="kg" /></div>
  360. <div class="field"><label for="cmInput">Height cm</label><input id="cmInput" type="number" min="90" max="230" step="1" placeholder="cm" /></div>
  361. <div class="field"><label for="ageInput">Age</label><input id="ageInput" type="number" min="8" max="100" step="1" placeholder="age" /></div>
  362. <div class="field"><label for="levelSelect">Training level</label><select id="levelSelect"></select></div>
  363. <div class="field"><label for="weekInput">Current week</label><input id="weekInput" type="number" min="1" max="12" step="1" /></div>
  364. <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>
  365. <div class="field"><label for="hollowTest">Hollow hold seconds</label><input id="hollowTest" type="number" min="0" max="300" step="1" placeholder="optional" /></div>
  366. </div>
  367. <div class="pill-row" style="margin-top:14px">
  368. <button class="btn warn" id="autoLevelBtn">Auto-pick level from tests</button>
  369. <button class="btn ghost" id="saveSettingsBtn">Save settings</button>
  370. </div>
  371. <div id="bodyStats" class="small-text" style="margin-top:10px"></div>
  372.  
  373. <h2 style="margin-top:24px">Equipment and recovery</h2>
  374. <div class="settings-grid">
  375. <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>
  376. <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>
  377. <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>
  378. <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>
  379. <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>
  380. <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>
  381. </div>
  382. </section>
  383.  
  384. <aside class="panel">
  385. <h2>Level guide</h2>
  386. <div class="level-grid" id="levelCards"></div>
  387. <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>
  388. <h3>Data tools</h3>
  389. <div class="pill-row">
  390. <button class="btn ghost" id="exportBtn">Export data</button>
  391. <label class="btn ghost" for="importInput">Import data</label>
  392. <input id="importInput" type="file" accept="application/json" class="hidden" />
  393. <button class="btn danger" id="resetBtn">Reset progress</button>
  394. </div>
  395. </aside>
  396. </div>
  397. </section>
  398.  
  399. <section class="tab-panel hidden" id="tab-progress">
  400. <section class="panel">
  401. <div class="day-header">
  402. <div>
  403. <h2>12-week completion grid</h2>
  404. <p class="muted">Use it like a command board. Green means the day’s exercises are checked complete.</p>
  405. </div>
  406. <div class="pill-row"><span class="pill good" id="totalProgressPill">0% total</span></div>
  407. </div>
  408. <div class="calendar-grid" id="calendarGrid"></div>
  409. </section>
  410. </section>
  411.  
  412. <section class="tab-panel hidden" id="tab-bank">
  413. <section class="panel">
  414. <div class="day-header">
  415. <div>
  416. <h2>Exercise bank</h2>
  417. <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>
  418. </div>
  419. </div>
  420. <div class="search-row">
  421. <input id="bankSearch" placeholder="Search push, leg, core, back, burpee..." />
  422. <select id="bankCategory"><option value="all">All categories</option></select>
  423. </div>
  424. <div class="bank-grid" id="exerciseBank"></div>
  425. </section>
  426. </section>
  427. </main>
  428. </div>
  429.  
  430. <div class="modal" id="demoModal" aria-hidden="true" role="dialog" aria-modal="true">
  431. <div class="modal-backdrop" data-close-modal></div>
  432. <div class="modal-card">
  433. <div class="modal-head">
  434. <h2 id="modalTitle">Exercise demo</h2>
  435. <button class="btn small" data-close-modal>Close</button>
  436. </div>
  437. <div class="modal-body" id="modalBody"></div>
  438. </div>
  439. </div>
  440.  
  441. <div class="toast" id="toast"></div>
  442.  
  443. <script>
  444. (function(){
  445. 'use strict';
  446.  
  447. const STORAGE_KEY = 'ninjaMilitaryBodyweightApp.v1';
  448. const DAYS = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'];
  449. const LEVELS = {
  450. 1:{name:'Foundation', short:'L1', desc:'Build positions, joints, and basic reps. Hard but controlled.'},
  451. 2:{name:'Base Soldier', short:'L2', desc:'Standard bodyweight strength and clean endurance.'},
  452. 3:{name:'Hard Mode', short:'L3', desc:'Your default: strong work, honest tempo, real finishers.'},
  453. 4:{name:'Elite Street', short:'L4', desc:'Hard leverage, unilateral legs, more density.'},
  454. 5:{name:'Savage Ninja', short:'L5', desc:'Advanced leverage, high discipline, only if form stays clean.'}
  455. };
  456.  
  457. const DEFAULT_STATE = {
  458. settings:{
  459. name:'Ninja', kg:'', cm:'', age:'', level:3, week:1,
  460. pushupTest:'', hollowTest:'',
  461. noBar:true, wall:true, elevated:false, backpack:false, hardMode:true, deload:false
  462. },
  463. selectedDay: browserDay(),
  464. completions:{},
  465. notes:{},
  466. activeTab:'today'
  467. };
  468.  
  469. const EXERCISES = {
  470. 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'}},
  471. 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'}},
  472. 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'}},
  473. 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'}},
  474. 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'}},
  475. 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'}},
  476. 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'}},
  477. 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'}},
  478. 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'}},
  479. 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'}},
  480. 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'}},
  481. 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'}},
  482. 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'}},
  483. 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'}},
  484. 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'}},
  485. 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'}},
  486.  
  487. 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'}},
  488. 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'}},
  489. 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'}},
  490. 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'}},
  491. 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'}},
  492. 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'}},
  493. 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'}},
  494.  
  495. 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'}},
  496. 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'}},
  497. 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'}},
  498. 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'}},
  499. 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'}},
  500. 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'}},
  501. 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'}},
  502.  
  503. 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'}},
  504. 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'}},
  505. 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'}},
  506. 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'}},
  507. 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'}},
  508. 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'}},
  509. 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'}},
  510. 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'}},
  511. 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'}},
  512. 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'}},
  513.  
  514. 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'}},
  515. 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'}},
  516. 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'}},
  517. 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'}},
  518. 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'}},
  519. 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'}},
  520. 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'}},
  521. 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'}},
  522. 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'}},
  523. 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'}},
  524. 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'}},
  525.  
  526. 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'}},
  527. 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'}},
  528. 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'}},
  529. 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'}},
  530. 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'}},
  531. 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'}}
  532. };
  533.  
  534. let state = loadState();
  535.  
  536. function browserDay(){
  537. const d = new Date().getDay();
  538. return DAYS[(d + 6) % 7];
  539. }
  540.  
  541. function loadState(){
  542. try{
  543. const raw = localStorage.getItem(STORAGE_KEY);
  544. if(!raw) return structuredClone(DEFAULT_STATE);
  545. const parsed = JSON.parse(raw);
  546. return deepMerge(structuredClone(DEFAULT_STATE), parsed);
  547. }catch(e){
  548. console.warn('Could not load state', e);
  549. return structuredClone(DEFAULT_STATE);
  550. }
  551. }
  552. function saveState(){localStorage.setItem(STORAGE_KEY, JSON.stringify(state));}
  553. function deepMerge(target, source){
  554. for(const key in source){
  555. if(source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])){
  556. target[key] = deepMerge(target[key] || {}, source[key]);
  557. }else target[key] = source[key];
  558. }
  559. return target;
  560. }
  561. function clamp(num,min,max){return Math.max(min, Math.min(max, Number(num)||min));}
  562. function esc(str){return String(str ?? '').replace(/[&<>'"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));}
  563. function youtubeSearchUrl(q){return `https://www.youtube.com/results?search_query=${encodeURIComponent(q)}`;}
  564. function youtubeWatchUrl(id){return `https://www.youtube.com/watch?v=${encodeURIComponent(id)}`;}
  565. function youtubeEmbedUrl(id){
  566. const origin = /^https?:$/.test(location.protocol) ? `&origin=${encodeURIComponent(location.origin)}` : '';
  567. return `https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}?rel=0&modestbranding=1${origin}`;
  568. }
  569. function youtubeThumbUrl(id){return `https://img.youtube.com/vi/${encodeURIComponent(id)}/hqdefault.jpg`;}
  570. function isFileMode(){return location.protocol === 'file:';}
  571. function hasEmbeddableDemo(demo){return !!(demo && demo.id);}
  572. function demoButtonLabel(ex){return hasEmbeddableDemo(ex.demo) ? 'Demo' : 'Find demo';}
  573. function tempoText(prescription){
  574. const match = String(prescription || '').match(/@\s*([0-9Xx]{4})/);
  575. if(!match) return '';
  576. const code = match[1].toUpperCase();
  577. const part = ch => ch === 'X' ? 'explosive' : `${ch}s`;
  578. return `Tempo ${code}: ${part(code[0])} down, ${part(code[1])} bottom pause, ${part(code[2])} up, ${part(code[3])} top pause.`;
  579. }
  580. function getSettings(){return state.settings;}
  581. function level(){return clamp(getSettings().level,1,5);}
  582. function isNoBar(){return !!getSettings().noBar;}
  583. function setCount(base){
  584. const s = getSettings();
  585. let sets = base;
  586. if(s.hardMode && level() >= 4) sets += 1;
  587. if(s.hardMode && level() === 5 && base >= 4) sets += 1;
  588. if(s.deload) sets = Math.max(1, Math.ceil(sets * .55));
  589. return sets;
  590. }
  591. function reps(kind){
  592. const l = level();
  593. if(kind === 'strength') return l <= 2 ? '4-8 reps' : '3-6 reps';
  594. if(kind === 'hypertrophy') return l <= 2 ? '8-12 reps' : '8-15 reps';
  595. if(kind === 'endurance') return l <= 2 ? '20-30 sec' : '30-45 sec';
  596. if(kind === 'legs') return l <= 2 ? '8-12 each leg' : '8-15 each leg';
  597. return 'reps';
  598. }
  599. function maybeLoadNote(){
  600. if(getSettings().backpack) return 'Optional: add backpack only if form stays clean.';
  601. return '';
  602. }
  603.  
  604. function select(slot){
  605. const s = getSettings();
  606. const l = level();
  607. const elevated = !!s.elevated;
  608. const wall = !!s.wall;
  609. const noBar = !!s.noBar;
  610.  
  611. if(slot === 'horizontalPush'){
  612. if(l === 1) return s.elevated ? 'inclinePushup' : 'wallPushup';
  613. if(l === 2) return 'pushup';
  614. if(l === 3) return elevated ? 'declinePushup' : 'slowPushup';
  615. if(l === 4) return 'archerPushup';
  616. return 'pseudoPlanchePushup';
  617. }
  618. if(slot === 'verticalPush'){
  619. if(l === 1) return 'pikeHold';
  620. if(l === 2) return 'pikePushup';
  621. if(l === 3) return elevated ? 'elevatedPikePushup' : 'hardPikePushup';
  622. if(l === 4) return wall ? 'wallHandstandHold' : 'hardPikePushup';
  623. return wall ? 'wallHspuNegative' : 'hardPikePushup';
  624. }
  625. if(slot === 'wallSkill'){
  626. if(!wall) return 'pikeHold';
  627. if(l <= 2) return 'wallWalk';
  628. if(l === 3) return 'wallHandstandHold';
  629. return l === 4 ? 'wallHandstandHold' : 'wallHspuNegative';
  630. }
  631. if(slot === 'pullVertical'){
  632. if(noBar){
  633. if(l === 1) return 'proneWRaise';
  634. if(l === 2) return 'reverseSnowAngel';
  635. if(l === 3) return 'supermanPulldown';
  636. if(l === 4) return 'selfRow';
  637. return 'selfRow';
  638. }
  639. if(l === 1) return 'sittingPullup';
  640. if(l === 2) return 'negativePullup';
  641. if(l === 3) return 'strictPullup';
  642. if(l === 4) return 'slowPullup';
  643. return 'lHangPullup';
  644. }
  645. if(slot === 'pullHorizontal'){
  646. if(noBar){
  647. if(l === 1) return 'proneTRaise';
  648. if(l === 2) return 'proneWRaise';
  649. if(l === 3) return 'reverseSnowAngel';
  650. if(l === 4) return 'supermanPulldown';
  651. return 'proneCobra';
  652. }
  653. return 'australianPullup';
  654. }
  655. if(slot === 'pullSkill'){
  656. if(noBar) return l <= 2 ? 'proneCobra' : 'selfRow';
  657. return 'tuckFrontLever';
  658. }
  659. if(slot === 'armPull'){
  660. return noBar ? 'selfCurl' : 'strictPullup';
  661. }
  662. if(slot === 'legsMain'){
  663. if(l === 1) return 'squat';
  664. if(l === 2) return 'prisonerSquat';
  665. if(l === 3) return 'reverseLunge';
  666. if(l === 4) return elevated ? 'bulgarianSplitSquat' : 'splitSquat';
  667. return 'oneLegSquatNegative';
  668. }
  669. if(slot === 'posterior'){
  670. if(l === 1) return 'gluteBridge';
  671. if(l === 2) return 'singleLegGluteBridge';
  672. if(l === 3) return elevated ? 'elevatedGluteBridge' : 'singleLegGluteBridge';
  673. if(l === 4) return 'singleLegGluteBridge';
  674. return 'singleLegGluteBridge';
  675. }
  676. if(slot === 'core'){
  677. if(noBar){
  678. if(l === 1) return 'deadBug';
  679. if(l === 2) return 'elbowPlank';
  680. if(l === 3) return 'hollowHold';
  681. if(l === 4) return 'floorLegRaise';
  682. return 'hollowRock';
  683. }
  684. if(l === 1) return 'deadBug';
  685. if(l === 2) return 'elbowPlank';
  686. if(l === 3) return 'hollowHold';
  687. if(l === 4) return 'hangingKneeRaise';
  688. return 'hangingLegRaise';
  689. }
  690. if(slot === 'coreHard'){
  691. if(noBar){
  692. if(l <= 2) return 'flutterKicks';
  693. if(l === 3) return 'floorLegRaise';
  694. if(l === 4) return 'vUp';
  695. return 'hollowRock';
  696. }
  697. return l >= 5 ? 'toesToBar' : 'hangingKneeRaise';
  698. }
  699. return 'pushup';
  700. }
  701.  
  702. function item(slot, exId, title, prescription, lane, rest, notes){
  703. return {slot, exId, title, prescription, lane, rest, notes: notes || ''};
  704. }
  705.  
  706. function generateDay(day, weekOverride){
  707. const l = level();
  708. const s = getSettings();
  709. const mainRest = '2-3 min';
  710. const hypRest = '90-150 sec';
  711. const shortRest = '30-90 sec';
  712. const loadNote = maybeLoadNote();
  713. const deloadNote = s.deload ? 'Deload on: reduced sets this week.' : '';
  714.  
  715. if(day === 'Monday'){
  716. return {
  717. title:'Push strength + handstand skill',
  718. theme:'Strength-skill lane',
  719. summary:'Vertical push first, horizontal push second, then skill and core. This is your upper-body strength day.',
  720. items:[
  721. item('mon-vertical', select('verticalPush'), 'Vertical push strength', `${setCount(5)} x ${reps('strength')} @ 31X1`, 'Strength', mainRest, 'Clean slow lower. ' + deloadNote),
  722. item('mon-horizontal', select('horizontalPush'), 'Horizontal push strength', `${setCount(4)} x 6-12 reps @ 3110`, 'Hypertrophy strength', hypRest, loadNote),
  723. 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.'),
  724. item('mon-core', 'flutterKicks', 'Flutter kicks finisher', `${setCount(3)} x 30-45 sec`, 'Military core', '30 sec', 'Low back stays down.')
  725. ]
  726. };
  727. }
  728. if(day === 'Tuesday'){
  729. if(isNoBar()){
  730. return {
  731. title:'No-bar pull substitute + back balance',
  732. theme:'Back, rear delts, biceps, core',
  733. 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.',
  734. items:[
  735. 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.'),
  736. 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.'),
  737. 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.'),
  738. item('tue-curl', 'selfCurl', 'Self-resisted curl', `${setCount(3)} x 8-15 each side`, 'Arms', '45-75 sec', 'Slow up, slow down.'),
  739. item('tue-core', select('core'), 'Core position work', `${setCount(4)} x 20-40 sec or 8-20 reps`, 'Core', shortRest, '')
  740. ]
  741. };
  742. }
  743. return {
  744. title:'Pull strength + front lever skill',
  745. theme:'Original bar mode',
  746. summary:'Use this when you have a safe bar. Vertical pull, horizontal pull, lever work, then hanging abs.',
  747. items:[
  748. item('tue-vpull', select('pullVertical'), 'Vertical pull strength', `${setCount(5)} x ${reps('strength')} @ 31X1`, 'Strength', mainRest, ''),
  749. item('tue-hpull', select('pullHorizontal'), 'Horizontal pull hypertrophy', `${setCount(4)} x 6-12 reps @ 3111`, 'Hypertrophy', hypRest, ''),
  750. item('tue-lever', select('pullSkill'), 'Front lever lane', `${setCount(4)}-6 holds x 8-15 sec`, 'Skill', '60-90 sec', ''),
  751. item('tue-hangcore', select('coreHard'), 'Hanging core', `${setCount(3)} x 8-15 reps`, 'Core', shortRest, '')
  752. ]
  753. };
  754. }
  755. if(day === 'Wednesday'){
  756. return {
  757. title:'Legs hypertrophy + core',
  758. theme:'Legs and trunk',
  759. summary:'Main leg pattern, posterior chain, core, optional climber finisher.',
  760. items:[
  761. item('wed-legs', select('legsMain'), 'Main leg move', `${setCount(5)} x ${reps('legs')} or 8-15 bilateral @ 31X1`, 'Hypertrophy', hypRest, loadNote),
  762. item('wed-post', select('posterior'), 'Posterior-chain add-on', `${setCount(4)} x 10-20 reps @ 3012`, 'Glutes / hamstrings', '60-90 sec', loadNote),
  763. item('wed-core', select('core'), 'Core level work', `${setCount(4)} x 8-20 reps or 20-40 sec`, 'Core', shortRest, ''),
  764. 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.')
  765. ]
  766. };
  767. }
  768. if(day === 'Thursday'){
  769. return {
  770. title:'Recovery + base endurance',
  771. theme:'Easy aerobic work',
  772. summary:'Keep it easy enough that Friday feels fresh. This builds duration without stealing too much recovery.',
  773. items:[
  774. 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.'),
  775. item('thu-sideplank', 'sidePlank', 'Side plank', `${setCount(3)} x 20-40 sec each side`, 'Core stability', '30-60 sec', ''),
  776. item('thu-mobility', 'mobilityFlow', 'Mobility flow', '8-15 minutes', 'Recovery', 'None', 'Hips, ankles, shoulders, spine. No pain.')
  777. ]
  778. };
  779. }
  780. if(day === 'Friday'){
  781. return {
  782. title:'Upper hypertrophy + PRT density',
  783. theme:'Muscle + military density',
  784. summary:'Upper-body volume followed by a push-up/burpee EMOM. This is hard but short.',
  785. items:[
  786. item('fri-push', select('horizontalPush'), 'Horizontal push volume', `${setCount(4)} x 8-15 reps @ 3010`, 'Hypertrophy', hypRest, loadNote),
  787. item('fri-back', select('pullHorizontal'), isNoBar() ? 'Back balance volume' : 'Horizontal pull volume', `${setCount(4)} x 8-15 reps`, 'Hypertrophy', hypRest, ''),
  788. item('fri-vertical', select('verticalPush'), 'Vertical push volume', `${setCount(3)} x 6-10 reps`, 'Shoulders', '90-120 sec', ''),
  789. 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', ''),
  790. 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.'),
  791. 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.')
  792. ]
  793. };
  794. }
  795. if(day === 'Saturday'){
  796. const rounds = s.deload ? Math.max(2, l) : (l <= 2 ? 4 : l === 3 ? 5 : 6);
  797. return {
  798. title:'Legs work-capacity + hard military circuit',
  799. theme:'Grit day',
  800. summary:'Leg volume first, then the hard circuit. This is the weekly place for controlled suffering.',
  801. items:[
  802. item('sat-leg', select('legsMain'), 'Leg main move', `${setCount(4)} x 8-15 reps`, 'Legs', hypRest, loadNote),
  803. item('sat-post', select('posterior'), 'Posterior-chain add-on', `${setCount(3)} x 12-20 reps`, 'Glutes / hamstrings', '60-90 sec', loadNote),
  804. 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.'),
  805. item('sat-c2', 'jumpingSquats', `Circuit: jumping squats`, `${rounds} rounds x ${l <= 2 ? '8-10' : '10-15'} reps`, 'Power', 'In circuit', 'Land quietly.'),
  806. item('sat-c3', 'flutterKicks', `Circuit: flutter kicks`, `${rounds} rounds x ${l <= 2 ? '20-30' : '30-40'} sec`, 'Core endurance', 'In circuit', ''),
  807. item('sat-c4', 'mountainClimbers', `Circuit: mountain climbers`, `${rounds} rounds x ${l <= 2 ? '20-30' : '30-40'} sec`, 'Conditioning', 'In circuit', '')
  808. ]
  809. };
  810. }
  811. return {
  812. title:'Full rest',
  813. theme:'Recover and rebuild',
  814. summary:'Walk, stretch, eat, sleep. Do not turn rest day into another punishment day.',
  815. items:[
  816. item('sun-rest', 'rest', 'Full rest', 'No hard training', 'Recovery', 'All day', 'Easy walk is fine.'),
  817. item('sun-mobility', 'mobilityFlow', 'Optional easy mobility', '5-12 minutes', 'Recovery', 'None', 'Only if it helps you feel better.')
  818. ]
  819. };
  820. }
  821.  
  822. function completionKey(week, day, slot, exId){return `w${week}:${day}:${slot}:${exId}`;}
  823. function noteKey(week, day){return `w${week}:${day}:notes`;}
  824. function isItemComplete(week, day, item){return !!state.completions[completionKey(week, day, item.slot, item.exId)];}
  825. function setItemComplete(week, day, item, val){
  826. const key = completionKey(week, day, item.slot, item.exId);
  827. if(val) state.completions[key] = true; else delete state.completions[key];
  828. saveState();
  829. }
  830. function dayItems(day, week){return generateDay(day, week).items;}
  831. function dayCompletion(week, day){
  832. const items = dayItems(day, week);
  833. if(!items.length) return {done:0,total:0,percent:0,complete:false};
  834. const done = items.filter(it => isItemComplete(week, day, it)).length;
  835. return {done,total:items.length,percent:Math.round((done/items.length)*100),complete:done===items.length};
  836. }
  837. function weekCompletion(week){
  838. let done=0,total=0;
  839. DAYS.forEach(d => { const c = dayCompletion(week,d); done += c.done; total += c.total; });
  840. return {done,total,percent: total ? Math.round((done/total)*100) : 0};
  841. }
  842. function totalCompletion(){
  843. let done=0,total=0;
  844. for(let w=1; w<=12; w++) DAYS.forEach(d=>{const c=dayCompletion(w,d); done+=c.done; total+=c.total;});
  845. return {done,total,percent: total ? Math.round((done/total)*100) : 0};
  846. }
  847.  
  848. function render(){
  849. normalizeSettings();
  850. renderSelectors();
  851. renderStats();
  852. renderToday();
  853. renderWeek();
  854. renderSettings();
  855. renderProgress();
  856. renderBank();
  857. saveState();
  858. }
  859.  
  860. function normalizeSettings(){
  861. const s = getSettings();
  862. s.level = clamp(s.level,1,5);
  863. s.week = clamp(s.week,1,12);
  864. if(!DAYS.includes(state.selectedDay)) state.selectedDay = browserDay();
  865. }
  866.  
  867. function renderSelectors(){
  868. const weekSelect = document.getElementById('weekSelect');
  869. if(weekSelect.options.length !== 12){
  870. weekSelect.innerHTML = Array.from({length:12},(_,i)=>`<option value="${i+1}">Week ${i+1}</option>`).join('');
  871. }
  872. weekSelect.value = getSettings().week;
  873. const daySelect = document.getElementById('daySelect');
  874. if(daySelect.options.length !== DAYS.length){
  875. daySelect.innerHTML = DAYS.map(d=>`<option value="${d}">${d}</option>`).join('');
  876. }
  877. daySelect.value = state.selectedDay;
  878. const levelOptions = Object.entries(LEVELS).map(([k,v])=>`<option value="${k}">${k} - ${v.name}</option>`).join('');
  879. const quick = document.getElementById('quickLevelSelect');
  880. const full = document.getElementById('levelSelect');
  881. if(quick.innerHTML !== levelOptions) quick.innerHTML = levelOptions;
  882. if(full.innerHTML !== levelOptions) full.innerHTML = levelOptions;
  883. quick.value = getSettings().level;
  884. full.value = getSettings().level;
  885. }
  886.  
  887. function renderStats(){
  888. const s = getSettings();
  889. document.getElementById('statWeek').textContent = s.week;
  890. document.getElementById('statLevel').textContent = LEVELS[level()].short;
  891. document.getElementById('statDay').textContent = state.selectedDay.slice(0,3);
  892. document.getElementById('statMode').textContent = s.noBar ? 'No bar' : 'Bar';
  893. const wc = weekCompletion(s.week);
  894. document.getElementById('weekProgressFill').style.width = wc.percent + '%';
  895. document.getElementById('weekProgressText').textContent = `${wc.percent}% complete (${wc.done}/${wc.total} items)`;
  896. }
  897.  
  898. function renderToday(){
  899. const s = getSettings();
  900. const day = state.selectedDay;
  901. const plan = generateDay(day, s.week);
  902. const c = dayCompletion(s.week, day);
  903. const badge = day.slice(0,2).toUpperCase();
  904. document.getElementById('todayContent').innerHTML = `
  905. <div class="day-header">
  906. <div class="day-title">
  907. <div class="day-badge">${esc(badge)}</div>
  908. <div>
  909. <h2>${esc(day)} - ${esc(plan.title)}</h2>
  910. <div class="muted">${esc(plan.summary)}</div>
  911. </div>
  912. </div>
  913. <div>
  914. <span class="pill hot">${esc(plan.theme)}</span>
  915. <span class="pill good">${c.percent}% done</span>
  916. </div>
  917. </div>
  918. <div class="exercise-list">
  919. ${plan.items.map(it => renderExerciseCard(it, s.week, day)).join('')}
  920. </div>
  921. `;
  922. document.getElementById('todayPills').innerHTML = `
  923. <span class="pill hot">${esc(LEVELS[level()].name)}</span>
  924. <span class="pill good">${s.noBar ? 'No-bar substitutions on' : 'Bar mode on'}</span>
  925. <span class="pill">${s.deload ? 'Deload week' : 'Normal volume'}</span>
  926. <span class="pill">${s.hardMode ? 'Hard mode' : 'Standard mode'}</span>
  927. `;
  928. const noteEl = document.getElementById('dayNotes');
  929. noteEl.value = state.notes[noteKey(s.week, day)] || '';
  930. }
  931.  
  932. function renderExerciseCard(it, week, day){
  933. const ex = EXERCISES[it.exId] || EXERCISES.pushup;
  934. const complete = isItemComplete(week, day, it);
  935. const tempo = tempoText(it.prescription);
  936. return `
  937. <article class="exercise-card ${complete ? 'complete' : ''}">
  938. <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" />
  939. <div class="exercise-main">
  940. <h3>${esc(it.title)}: ${esc(ex.name)}</h3>
  941. <div class="small-text">${esc(ex.cue)}</div>
  942. <div class="exercise-sub">
  943. <span class="tag red">${esc(it.prescription)}</span>
  944. <span class="tag yellow">Rest: ${esc(it.rest)}</span>
  945. <span class="tag green">${esc(it.lane)}</span>
  946. ${it.notes ? `<span class="tag">${esc(it.notes)}</span>` : ''}
  947. </div>
  948. ${tempo ? `<div class="small-text" style="margin-top:7px">${esc(tempo)}</div>` : ''}
  949. </div>
  950. <div class="exercise-actions">
  951. <button class="btn small" data-demo="${esc(it.exId)}">${demoButtonLabel(ex)}</button>
  952. <button class="btn small ghost" data-toggle-check="${esc(it.slot)}" data-exid="${esc(it.exId)}">${complete ? 'Undo' : 'Done'}</button>
  953. </div>
  954. </article>
  955. `;
  956. }
  957.  
  958. function renderWeek(){
  959. const s = getSettings();
  960. document.getElementById('weekLevelPill').textContent = `${LEVELS[level()].short} ${LEVELS[level()].name}`;
  961. document.getElementById('weekEquipmentPill').textContent = s.noBar ? 'No-bar mode' : 'Bar mode';
  962. document.getElementById('weekOverview').innerHTML = DAYS.map(day=>{
  963. const p = generateDay(day, s.week);
  964. const c = dayCompletion(s.week, day);
  965. const names = p.items.map(it => EXERCISES[it.exId]?.name || it.exId).slice(0,5).join(', ');
  966. return `<article class="week-card">
  967. <h3>${esc(day)}</h3>
  968. <div class="muted"><strong>${esc(p.title)}</strong></div>
  969. <p class="small-text">${esc(names)}${p.items.length > 5 ? '...' : ''}</p>
  970. <div class="mini-progress"><span style="width:${c.percent}%"></span></div>
  971. <div class="small-text" style="margin-top:8px">${c.done}/${c.total} complete</div>
  972. <button class="btn small ghost" data-open-day="${esc(day)}">Open day</button>
  973. </article>`;
  974. }).join('');
  975. }
  976.  
  977. function renderSettings(){
  978. const s = getSettings();
  979. document.getElementById('nameInput').value = s.name || '';
  980. document.getElementById('kgInput').value = s.kg || '';
  981. document.getElementById('cmInput').value = s.cm || '';
  982. document.getElementById('ageInput').value = s.age || '';
  983. document.getElementById('weekInput').value = s.week;
  984. document.getElementById('pushupTest').value = s.pushupTest || '';
  985. document.getElementById('hollowTest').value = s.hollowTest || '';
  986. document.getElementById('noBarToggle').checked = !!s.noBar;
  987. document.getElementById('wallToggle').checked = !!s.wall;
  988. document.getElementById('elevatedToggle').checked = !!s.elevated;
  989. document.getElementById('backpackToggle').checked = !!s.backpack;
  990. document.getElementById('hardModeToggle').checked = !!s.hardMode;
  991. document.getElementById('deloadToggle').checked = !!s.deload;
  992. document.getElementById('levelCards').innerHTML = Object.entries(LEVELS).map(([k,v]) => `
  993. <div class="level-card ${Number(k)===level()?'active':''}">
  994. <h3>${esc(v.short)} - ${esc(v.name)}</h3>
  995. <p class="small-text">${esc(v.desc)}</p>
  996. <button class="btn small ghost" data-set-level="${k}">Use level ${k}</button>
  997. </div>
  998. `).join('');
  999. document.getElementById('bodyStats').innerHTML = bodyStatsHtml();
  1000. }
  1001.  
  1002. function bodyStatsHtml(){
  1003. const s = getSettings();
  1004. const kg = Number(s.kg), cm = Number(s.cm), age = Number(s.age);
  1005. const parts = [];
  1006. if(s.name) parts.push(`<strong>Call sign:</strong> ${esc(s.name)}`);
  1007. if(kg && cm){
  1008. const bmi = kg / Math.pow(cm/100, 2);
  1009. parts.push(`<strong>BMI:</strong> ${bmi.toFixed(1)} <span class="muted">(just a rough stat; performance matters more here)</span>`);
  1010. }
  1011. if(age) parts.push(`<strong>Age:</strong> ${esc(age)}`);
  1012. if(!parts.length) return 'Add kg, cm, and age if you want them saved. They do not leave this browser.';
  1013. return parts.join(' &nbsp; • &nbsp; ');
  1014. }
  1015.  
  1016. function renderProgress(){
  1017. const total = totalCompletion();
  1018. document.getElementById('totalProgressPill').textContent = `${total.percent}% total (${total.done}/${total.total})`;
  1019. const head = `<tr><th>Week</th>${DAYS.map(d=>`<th>${d.slice(0,3)}</th>`).join('')}</tr>`;
  1020. const rows = Array.from({length:12},(_,i)=>{
  1021. const w=i+1;
  1022. const cells = DAYS.map(day=>{
  1023. const c=dayCompletion(w,day);
  1024. const restClass = day==='Sunday' ? ' rest' : '';
  1025. 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>`;
  1026. }).join('');
  1027. return `<tr><th>Week ${w}</th>${cells}</tr>`;
  1028. }).join('');
  1029. document.getElementById('calendarGrid').innerHTML = `<table class="calendar-table">${head}${rows}</table>`;
  1030. }
  1031.  
  1032. function renderBank(){
  1033. const categorySel = document.getElementById('bankCategory');
  1034. const categories = ['all', ...Array.from(new Set(Object.values(EXERCISES).map(e=>e.category))).sort()];
  1035. const currentCat = categorySel.value || 'all';
  1036. categorySel.innerHTML = categories.map(c=>`<option value="${esc(c)}">${esc(c === 'all' ? 'All categories' : c)}</option>`).join('');
  1037. categorySel.value = categories.includes(currentCat) ? currentCat : 'all';
  1038. const q = (document.getElementById('bankSearch').value || '').toLowerCase().trim();
  1039. const filtered = Object.entries(EXERCISES).filter(([id,ex]) => {
  1040. const catOK = categorySel.value === 'all' || ex.category === categorySel.value;
  1041. const text = `${ex.name} ${ex.category} ${ex.cue}`.toLowerCase();
  1042. return catOK && (!q || text.includes(q));
  1043. });
  1044. document.getElementById('exerciseBank').innerHTML = filtered.map(([id,ex]) => `
  1045. <article class="bank-card">
  1046. <h3>${esc(ex.name)}</h3>
  1047. <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>
  1048. <p class="small-text">${esc(ex.cue)}</p>
  1049. <button class="btn small" data-demo="${esc(id)}">${demoButtonLabel(ex)}</button>
  1050. </article>
  1051. `).join('') || `<div class="empty-state">No exercises match your search.</div>`;
  1052. }
  1053.  
  1054. function openDemo(exId){
  1055. const ex = EXERCISES[exId];
  1056. if(!ex) return;
  1057. const demo = ex.demo || {type:'search', search:ex.name + ' tutorial'};
  1058. const search = demo.search || `${ex.name} exercise tutorial proper form`;
  1059. const modal = document.getElementById('demoModal');
  1060. document.getElementById('modalTitle').textContent = `${ex.name} demo`;
  1061. let body = '';
  1062. if(hasEmbeddableDemo(demo)){
  1063. if(isFileMode()){
  1064. 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>`;
  1065. }else{
  1066. const src = youtubeEmbedUrl(demo.id);
  1067. 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>`;
  1068. }
  1069. 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>`;
  1070. }else if(demo.type === 'url' && demo.url){
  1071. 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>`;
  1072. 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>`;
  1073. }else{
  1074. 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>`;
  1075. body += `<div class="link-grid"><a class="btn small primary" target="_blank" rel="noopener" href="${esc(youtubeSearchUrl(search))}">Search YouTube videos</a></div>`;
  1076. }
  1077. body += `<div class="quote-box"><strong>Form cue:</strong> ${esc(ex.cue)}</div>`;
  1078. document.getElementById('modalBody').innerHTML = body;
  1079. modal.classList.add('open');
  1080. modal.setAttribute('aria-hidden','false');
  1081. }
  1082.  
  1083. function closeModal(){
  1084. const modal = document.getElementById('demoModal');
  1085. modal.classList.remove('open');
  1086. modal.setAttribute('aria-hidden','true');
  1087. document.getElementById('modalBody').innerHTML = '';
  1088. }
  1089.  
  1090. function toast(msg){
  1091. const el = document.getElementById('toast');
  1092. el.textContent = msg;
  1093. el.classList.add('show');
  1094. clearTimeout(toast._t);
  1095. toast._t = setTimeout(()=>el.classList.remove('show'), 2400);
  1096. }
  1097.  
  1098. function setActiveTab(tab){
  1099. state.activeTab = tab;
  1100. document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tab));
  1101. document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('hidden', panel.id !== `tab-${tab}`));
  1102. saveState();
  1103. }
  1104.  
  1105. function updateSettingsFromForm(){
  1106. const s = getSettings();
  1107. s.name = document.getElementById('nameInput').value.trim();
  1108. s.kg = document.getElementById('kgInput').value;
  1109. s.cm = document.getElementById('cmInput').value;
  1110. s.age = document.getElementById('ageInput').value;
  1111. s.level = clamp(document.getElementById('levelSelect').value,1,5);
  1112. s.week = clamp(document.getElementById('weekInput').value,1,12);
  1113. s.pushupTest = document.getElementById('pushupTest').value;
  1114. s.hollowTest = document.getElementById('hollowTest').value;
  1115. s.noBar = document.getElementById('noBarToggle').checked;
  1116. s.wall = document.getElementById('wallToggle').checked;
  1117. s.elevated = document.getElementById('elevatedToggle').checked;
  1118. s.backpack = document.getElementById('backpackToggle').checked;
  1119. s.hardMode = document.getElementById('hardModeToggle').checked;
  1120. s.deload = document.getElementById('deloadToggle').checked;
  1121. saveState();
  1122. }
  1123.  
  1124. function autoPickLevel(){
  1125. const s = getSettings();
  1126. const push = Number(document.getElementById('pushupTest').value || s.pushupTest || 0);
  1127. const hollow = Number(document.getElementById('hollowTest').value || s.hollowTest || 0);
  1128. let score = 1;
  1129. if(push >= 10 || hollow >= 20) score = 2;
  1130. if(push >= 20 && hollow >= 30) score = 3;
  1131. if(push >= 35 && hollow >= 45) score = 4;
  1132. if(push >= 50 && hollow >= 60) score = 5;
  1133. s.level = score;
  1134. s.pushupTest = document.getElementById('pushupTest').value;
  1135. s.hollowTest = document.getElementById('hollowTest').value;
  1136. render();
  1137. toast(`Auto-set to Level ${score}: ${LEVELS[score].name}`);
  1138. }
  1139.  
  1140. function exportData(){
  1141. const payload = {
  1142. app:'Ninja Military Bodyweight Web App',
  1143. storageKey:STORAGE_KEY,
  1144. exportedAt:new Date().toISOString(),
  1145. state:structuredClone(state)
  1146. };
  1147. const blob = new Blob([JSON.stringify(payload,null,2)], {type:'application/json'});
  1148. const url = URL.createObjectURL(blob);
  1149. const a = document.createElement('a');
  1150. a.href = url;
  1151. a.download = `ninja-bodyweight-data-week-${getSettings().week}.json`;
  1152. document.body.appendChild(a);
  1153. a.click();
  1154. a.remove();
  1155. URL.revokeObjectURL(url);
  1156. }
  1157.  
  1158. function importData(file){
  1159. if(!file) return;
  1160. const reader = new FileReader();
  1161. reader.onload = () => {
  1162. try{
  1163. const imported = JSON.parse(reader.result);
  1164. const importedState = imported && imported.state ? imported.state : imported;
  1165. if(!importedState || typeof importedState !== 'object' || !importedState.settings){
  1166. throw new Error('Missing app state');
  1167. }
  1168. state = deepMerge(structuredClone(DEFAULT_STATE), importedState);
  1169. saveState();
  1170. render();
  1171. toast('Imported data.');
  1172. }catch(e){
  1173. alert('That file was not valid JSON for this app.');
  1174. }
  1175. };
  1176. reader.readAsText(file);
  1177. }
  1178.  
  1179. function bindEvents(){
  1180. document.querySelectorAll('.tab-btn').forEach(btn => btn.addEventListener('click', () => setActiveTab(btn.dataset.tab)));
  1181. document.querySelectorAll('[data-tab-jump]').forEach(btn => btn.addEventListener('click', () => {
  1182. if(btn.hasAttribute('data-set-today')) state.selectedDay = browserDay();
  1183. setActiveTab(btn.dataset.tabJump);
  1184. render();
  1185. }));
  1186. document.getElementById('printBtn').addEventListener('click', () => window.print());
  1187.  
  1188. document.getElementById('weekSelect').addEventListener('change', e => {getSettings().week = clamp(e.target.value,1,12); render();});
  1189. document.getElementById('daySelect').addEventListener('change', e => {state.selectedDay = e.target.value; render();});
  1190. document.getElementById('todayDayBtn').addEventListener('click', () => {state.selectedDay = browserDay(); render(); toast(`Selected ${state.selectedDay}.`);});
  1191. document.getElementById('quickLevelSelect').addEventListener('change', e => {getSettings().level = clamp(e.target.value,1,5); render();});
  1192. document.getElementById('dayNotes').addEventListener('input', e => {state.notes[noteKey(getSettings().week, state.selectedDay)] = e.target.value; saveState();});
  1193.  
  1194. document.getElementById('markAllBtn').addEventListener('click', () => {
  1195. const w = getSettings().week, d = state.selectedDay;
  1196. dayItems(d,w).forEach(it => setItemComplete(w,d,it,true));
  1197. render(); toast('Day marked complete.');
  1198. });
  1199. document.getElementById('clearDayBtn').addEventListener('click', () => {
  1200. const w = getSettings().week, d = state.selectedDay;
  1201. dayItems(d,w).forEach(it => setItemComplete(w,d,it,false));
  1202. render(); toast('Day cleared.');
  1203. });
  1204.  
  1205. ['nameInput','kgInput','cmInput','ageInput','levelSelect','weekInput','pushupTest','hollowTest','noBarToggle','wallToggle','elevatedToggle','backpackToggle','hardModeToggle','deloadToggle'].forEach(id => {
  1206. document.getElementById(id).addEventListener('change', () => { updateSettingsFromForm(); render(); toast('Settings saved.'); });
  1207. });
  1208. document.getElementById('saveSettingsBtn').addEventListener('click', () => {updateSettingsFromForm(); render(); toast('Settings saved.');});
  1209. document.getElementById('autoLevelBtn').addEventListener('click', autoPickLevel);
  1210. document.getElementById('exportBtn').addEventListener('click', exportData);
  1211. document.getElementById('importInput').addEventListener('change', e => importData(e.target.files[0]));
  1212. document.getElementById('resetBtn').addEventListener('click', () => {
  1213. if(confirm('Reset all completion marks and notes? Settings will stay.')){
  1214. state.completions = {}; state.notes = {}; saveState(); render(); toast('Progress reset.');
  1215. }
  1216. });
  1217. document.getElementById('bankSearch').addEventListener('input', renderBank);
  1218. document.getElementById('bankCategory').addEventListener('change', renderBank);
  1219.  
  1220. document.addEventListener('click', e => {
  1221. const demoBtn = e.target.closest('[data-demo]');
  1222. if(demoBtn){ openDemo(demoBtn.dataset.demo); return; }
  1223. const close = e.target.closest('[data-close-modal]');
  1224. if(close){ closeModal(); return; }
  1225. const doneBtn = e.target.closest('[data-toggle-check]');
  1226. if(doneBtn){
  1227. const w=getSettings().week, d=state.selectedDay;
  1228. const slot=doneBtn.dataset.toggleCheck, exId=doneBtn.dataset.exid;
  1229. const it = dayItems(d,w).find(x => x.slot===slot && x.exId===exId);
  1230. if(it) setItemComplete(w,d,it,!isItemComplete(w,d,it));
  1231. render();
  1232. return;
  1233. }
  1234. const openDay = e.target.closest('[data-open-day]');
  1235. if(openDay){ state.selectedDay = openDay.dataset.openDay; setActiveTab('today'); render(); return; }
  1236. const setLevel = e.target.closest('[data-set-level]');
  1237. if(setLevel){ getSettings().level = clamp(setLevel.dataset.setLevel,1,5); render(); toast(`Level ${getSettings().level} selected.`); return; }
  1238. const grid = e.target.closest('[data-grid-week]');
  1239. if(grid){ getSettings().week = clamp(grid.dataset.gridWeek,1,12); state.selectedDay = grid.dataset.gridDay; setActiveTab('today'); render(); return; }
  1240. });
  1241.  
  1242. document.addEventListener('change', e => {
  1243. const cb = e.target.closest('[data-check]');
  1244. if(cb){
  1245. const w = getSettings().week, d = state.selectedDay;
  1246. const it = dayItems(d,w).find(x => x.slot === cb.dataset.check && x.exId === cb.dataset.exid);
  1247. if(it) setItemComplete(w,d,it,cb.checked);
  1248. render();
  1249. }
  1250. });
  1251.  
  1252. document.addEventListener('keydown', e => { if(e.key === 'Escape') closeModal(); });
  1253. }
  1254.  
  1255. bindEvents();
  1256. renderSelectors();
  1257. setActiveTab(state.activeTab || 'today');
  1258. render();
  1259. })();
  1260. </script>
  1261. </body>
  1262. </html>
  1263.  
Advertisement
Add Comment
Please, Sign In to add comment