Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/personal-trainerProgramme musculationPlain Text
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Mon programme muscu</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@600;700;800&family=Barlow:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
<style>
:root{
--bg:#eef3f7; --surface:#ffffff; --surface2:#f4f7fa; --ink:#14283a; --muted:#5b6b7a;
--line:#d6dfe7; --red:#d62828; --redbg:#fde9e9; --field:#f0f4f8;
--font-d:"Barlow Condensed","Arial Narrow",Arial,sans-serif;
--font-b:"Barlow","Segoe UI",Helvetica,Arial,sans-serif;
color-scheme:light;
box-sizing:border-box;
padding-top:env(safe-area-inset-top,0px);
padding-bottom:env(safe-area-inset-bottom,0px);
}
@media (prefers-color-scheme:dark){
:root:not([data-theme="light"]){
--bg:#0f1a24; --surface:#172532; --surface2:#1d2d3c; --ink:#e8eef4; --muted:#9db0c1;
--line:#2a3d4f; --red:#ff5a5a; --redbg:#3a1c20; --field:#223446; color-scheme:dark;
}
}
:root[data-theme="dark"]{
--bg:#0f1a24; --surface:#172532; --surface2:#1d2d3c; --ink:#e8eef4; --muted:#9db0c1;
--line:#2a3d4f; --red:#ff5a5a; --redbg:#3a1c20; --field:#223446; color-scheme:dark;
}
html{scroll-padding-top:env(safe-area-inset-top,0px)}
*,*::before,*::after{box-sizing:inherit}
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font-b);font-size:16px;line-height:1.45;-webkit-font-smoothing:antialiased}
button,input,select,textarea{font:inherit;color:inherit}
button{cursor:pointer}
:focus-visible{outline:3px solid var(--c,#2f6fdd);outline-offset:2px}
.wrap{max-width:760px;margin:0 auto;padding:16px 14px 120px}
header.top{padding:8px 2px 14px}
.title{font-family:var(--font-d);font-weight:800;font-size:clamp(2rem,8vw,2.9rem);line-height:1;letter-spacing:-.01em;border:0;background:none;width:100%;padding:0;margin:0}
.subtitle{border:0;background:none;width:100%;padding:4px 0 0;color:var(--muted);font-weight:500}
.title:hover,.subtitle:hover,.name:hover{background:var(--field)}
.tabs{display:flex;gap:8px;overflow-x:auto;padding:4px 2px 12px;scrollbar-width:none}
.tabs::-webkit-scrollbar{display:none}
.tab{flex:0 0 auto;border:2px solid var(--line);background:var(--surface);border-radius:999px;padding:7px 14px;font-weight:600;white-space:nowrap}
.tab .dot{display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--tc);margin-right:7px;vertical-align:1px}
.tab[aria-selected="true"]{background:var(--tc);border-color:var(--tc);color:#fff}
.tab[aria-selected="true"] .dot{background:#fff}
.tab.add{border-style:dashed;color:var(--muted)}
.dayhead{background:var(--c);color:#fff;border-radius:18px;padding:16px 16px 14px;margin-bottom:14px;position:relative;overflow:hidden}
.dayhead::after{content:"";position:absolute;right:-30px;top:-40px;width:160px;height:160px;border-radius:50%;background:rgba(255,255,255,.12)}
.dayhead input{background:transparent;border:0;color:#fff;width:100%;padding:0;position:relative;z-index:1}
.dayhead .dname{font-family:var(--font-d);font-size:2rem;font-weight:800;line-height:1.05}
.dayhead .dfocus{font-weight:600;opacity:.92}
.dayhead input:hover{background:rgba(255,255,255,.14)}
.daytools{display:flex;gap:8px;align-items:center;margin-top:10px;flex-wrap:wrap;position:relative;z-index:1}
.swatch{width:26px;height:26px;border-radius:50%;border:2px solid rgba(255,255,255,.7);background:var(--s);padding:0}
.swatch[aria-pressed="true"]{border:3px solid #fff;box-shadow:0 0 0 2px rgba(0,0,0,.25)}
.ghost{background:rgba(255,255,255,.18);border:0;color:#fff;border-radius:999px;padding:5px 12px;font-weight:600;font-size:.88rem}
.daystats{margin-left:auto;font-weight:600;font-size:.9rem;opacity:.95}
.ex{background:var(--surface);border-radius:16px;padding:14px;margin-bottom:12px;display:grid;grid-template-columns:84px 1fr;gap:12px;border-left:6px solid var(--c);position:relative}
.pic{width:84px;height:66px;border-radius:12px;background:var(--surface2);color:var(--ink);display:grid;place-items:center}
.pic svg{width:80px;height:60px}
.pic .acc{stroke:var(--c)}
.pic .plate{fill:var(--c);stroke:none}
.name{font-family:var(--font-d);font-weight:700;font-size:1.35rem;line-height:1.1;border:0;background:none;width:100%;padding:2px 0;border-radius:6px}
.tipline{display:flex;gap:6px;align-items:flex-start;margin-top:4px}
.tipline b{color:var(--red);font-size:.8rem;line-height:1.6;flex:0 0 auto}
.tip{border:0;background:none;width:100%;color:var(--red);font-weight:600;font-size:.9rem;padding:0;resize:none;line-height:1.35;border-radius:6px;min-height:1.3em;overflow:hidden}
.tip:hover{background:var(--redbg)}
.grid{grid-column:1 / -1;display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:2px}
.f{background:var(--field);border-radius:12px;padding:6px 6px 7px;text-align:center}
.f label{display:block;font-size:.72rem;font-weight:600;color:var(--muted)}
.f .row{display:flex;align-items:center;justify-content:center;gap:2px}
.f input{width:100%;min-width:0;border:0;background:none;text-align:center;font-family:var(--font-d);font-weight:700;font-size:1.35rem;padding:0;-moz-appearance:textfield}
.f input::-webkit-outer-spin-button,.f input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.st{flex:0 0 26px;height:26px;border-radius:50%;border:0;background:var(--surface);font-weight:700;line-height:1;padding:0}
.unit{font-size:.75rem;color:var(--muted);font-weight:600}
.sets{grid-column:1 / -1;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.sets span{font-size:.82rem;color:var(--muted);font-weight:600;margin-right:2px}
.set{width:38px;height:38px;border-radius:50%;border:2px solid var(--c);background:transparent;font-weight:700}
.set.on{background:var(--c);color:#fff}
.more{position:absolute;top:8px;right:8px}
.more summary{list-style:none;width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-weight:800;color:var(--muted);cursor:pointer}
.more summary::-webkit-details-marker{display:none}
.more[open] summary{background:var(--field)}
.menu{position:absolute;right:0;top:36px;z-index:5;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:8px;width:220px;box-shadow:0 10px 30px rgba(10,30,50,.18);display:grid;gap:6px}
.menu label{font-size:.8rem;color:var(--muted);font-weight:600}
.menu select{width:100%;padding:7px;border-radius:9px;border:1px solid var(--line);background:var(--field)}
.menu button{border:0;background:var(--field);border-radius:9px;padding:8px;text-align:left;font-weight:600}
.menu .danger{color:var(--red)}
.nameline{padding-right:30px}
.addex{width:100%;border:2px dashed var(--line);background:transparent;border-radius:16px;padding:14px;font-weight:700;color:var(--muted)}
.rules{background:var(--surface);border-radius:16px;padding:14px 16px;margin-top:22px}
.rules h2,.export h2{font-family:var(--font-d);font-size:1.5rem;margin:0 0 6px}
.rules li{margin:6px 0}
.rules .k{color:var(--red);font-weight:700}
.export{margin-top:16px;background:var(--surface);border-radius:16px;padding:14px 16px}
.btns{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
.btn{border:0;border-radius:12px;padding:11px 14px;font-weight:700;background:var(--field)}
.btn.primary{background:var(--ink);color:var(--bg)}
.note{font-size:.85rem;color:var(--muted);margin:8px 0 0}
.status{font-size:.82rem;color:var(--muted);text-align:right;min-height:1.2em;margin-top:6px}
.timer{position:fixed;left:0;right:0;bottom:0;padding:12px 16px calc(12px + env(safe-area-inset-bottom,0px));background:var(--ink);color:var(--bg);display:none;align-items:center;gap:12px;z-index:20}
.timer.show{display:flex}
.timer .t{font-family:var(--font-d);font-size:2.2rem;font-weight:800;min-width:96px;font-variant-numeric:tabular-nums}
.timer .lbl{flex:1;font-weight:600;font-size:.9rem;line-height:1.2}
.timer button{border:0;border-radius:10px;padding:8px 11px;font-weight:700;background:rgba(127,140,155,.3);color:inherit}
.timer.end{background:var(--red);color:#fff}
.bar{position:absolute;left:0;top:0;height:4px;background:var(--red)}
@media (max-width:420px){
.ex{grid-template-columns:70px 1fr;padding:12px}
.pic{width:70px;height:58px}.pic svg{width:66px;height:52px}
.grid{gap:6px}.f input{font-size:1.2rem}.st{flex-basis:22px;height:22px}
}
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
</head>
<body>
<div class="wrap">
<header class="top">
<input class="title" id="title" aria-label="Titre du programme">
<input class="subtitle" id="subtitle" aria-label="Objectif">
</header>
<nav class="tabs" id="tabs" role="tablist" aria-label="Séances"></nav>
<main id="day"></main>
<section class="rules">
<h2>Règles qui font progresser</h2>
<ul>
<li><span class="k">Progression :</span> quand tu fais le haut de la fourchette de reps sur toutes les séries, augmente la charge.</li>
<li><span class="k">Intensité :</span> arrête chaque série à 1 ou 2 reps de l’échec, mouvement propre.</li>
<li><span class="k">Repos :</span> 2 à 3 min sur les gros exercices, 1 à 2 min sur l’isolation.</li>
<li><span class="k">Objectif poids :</span> environ +0,5 à 0,7 kg par mois, protéines autour de 110-120 g par jour, 8 h de sommeil.</li>
</ul>
</section>
<section class="export">
<h2>Télécharger et sauvegarder</h2>
<div class="btns">
<button class="btn primary" id="pdf">Télécharger en PDF</button>
<button class="btn" id="json">Sauvegarder une copie (.json)</button>
<button class="btn" id="importBtn">Importer une copie</button>
<button class="btn" id="reset">Réinitialiser</button>
</div>
<input type="file" id="importFile" accept=".json,application/json" hidden>
<p class="note" id="dlnote">Tes modifications sont enregistrées automatiquement dans ce navigateur. Pour les retrouver sur un autre appareil, sauvegarde une copie puis importe-la là-bas.</p>
<div class="status" id="status"></div>
</section>
</div>
<div class="timer" id="timer" role="timer" aria-live="polite">
<div class="bar" id="tbar"></div>
<div class="t" id="tt">0:00</div>
<div class="lbl" id="tl">Repos</div>
<button id="tplus">+15 s</button>
<button id="tstop">Arrêter</button>
</div>
<script>
// ---------- Illustrations ----------
// c x y r = tête · l x y x y… = membres · b x y x y = barre/banc · p x y = disque
const PICS = {
squat:["c60 15 7","l58 23 52 48","l52 48 74 54 68 80","l52 48 46 56 40 80","b34 26 86 26","p34 26","p86 26","l57 28 66 26"],
souleve:["c38 28 7","l44 34 70 48","l70 48 66 66 68 84","l50 38 52 66","b28 68 80 68","p28 68","p80 68"],
couche:["b16 64 104 64","c26 54 7","l34 56 80 56","l80 56 92 70 92 86","l46 55 46 30","b24 28 70 28","p24 28","p70 28"],
militaire:["c60 26 7","l60 34 60 62","l60 62 52 86","l60 62 68 86","l60 38 48 12","l60 38 72 12","b32 10 88 10","p32 10","p88 10"],
rowing:["c34 32 7","l40 38 70 48","l70 48 66 66 70 84","l46 40 54 52 52 60","b30 60 78 60","p30 60","p78 60"],
traction:["b16 8 104 8","l48 8 54 32","l72 8 66 32","c60 26 6","l60 33 60 62","l60 62 54 74 62 86","l60 62 66 74 72 84"],
tirage:["b60 2 60 20","b38 20 82 20","l60 42 46 22","l60 42 74 22","c60 34 6","l60 40 60 66","b42 68 82 68","l60 66 80 66 80 86"],
curl:["c58 14 7","l58 22 58 54","l58 54 52 84","l58 54 64 84","l58 28 64 46 80 32","b78 26 82 38"],
triceps:["b84 0 84 30","c56 14 7","l56 22 56 54","l56 54 50 84","l56 54 62 84","l56 28 62 46 80 46","b80 40 80 52"],
laterales:["c60 14 7","l60 22 60 54","l60 54 54 84","l60 54 66 84","l60 28 26 34","l60 28 94 34","b24 28 24 40","b96 28 96 40"],
fente:["b84 62 112 62","c56 16 7","l56 24 56 52","l56 52 38 62 38 84","l56 52 68 74 90 62"],
presse:["c22 48 7","l28 54 52 66","l52 66 70 46 92 56","b96 32 96 80","b14 70 54 70"],
legcurl:["b26 64 76 64","c40 18 7","l40 26 40 62","l40 62 70 62 64 84","p66 80"],
mollets:["b44 86 80 86","c60 13 7","l60 21 60 52","l60 52 56 78 60 86","l60 52 66 78 70 86","b36 22 84 22","p36 22","p84 22"],
ecarte:["c60 20 7","l60 28 60 60","l60 60 54 84","l60 60 66 84","l60 32 30 36","l60 32 90 36","b28 28 28 44","b92 28 92 44"],
dips:["b24 44 96 44","c60 16 7","l60 24 60 56","l60 56 68 70 58 82","l60 28 52 44","l60 28 68 44"],
abdos:["b24 6 96 6","l52 6 56 28","l68 6 64 28","c60 24 6","l60 31 60 58","l60 58 96 58"],
gainage:["b8 74 112 74","c22 50 6","l28 54 92 64","l32 56 34 70 46 70","l92 64 96 72"],
hipthrust:["b10 52 36 52","c16 42 6","l22 48 56 46","l56 46 76 46 78 80","p56 38","b36 38 76 38"],
facepull:["b112 20 70 20","c50 18 7","l50 26 50 56","l50 56 44 84","l50 56 56 84","l50 30 38 22 66 20"],
saut:["b74 58 112 58","b74 58 74 86","b112 58 112 86","c40 18 7","l42 26 46 50","l46 50 60 56 52 72","l46 50 40 62 32 70","l42 30 28 20","l42 30 56 22"]
};
const PIC_NAMES = {squat:"Squat",souleve:"Soulevé de terre",couche:"Développé couché",militaire:"Développé militaire",rowing:"Rowing",traction:"Traction",tirage:"Tirage poulie",curl:"Curl",triceps:"Triceps",laterales:"Élévations latérales",fente:"Fente",presse:"Presse",legcurl:"Leg curl",mollets:"Mollets",ecarte:"Écarté",dips:"Dips",abdos:"Abdos suspendu",gainage:"Gainage",hipthrust:"Hip thrust",facepull:"Face pull",saut:"Plyométrie"};
function picSVG(key){
const els=(PICS[key]||PICS.curl).map(t=>{
const k=t[0], n=t.slice(1).split(" ").map(Number);
if(k==="c") return `<circle cx="${n[0]}" cy="${n[1]}" r="${n[2]}" fill="currentColor"/>`;
if(k==="p") return `<circle class="plate" cx="${n[0]}" cy="${n[1]}" r="7"/>`;
if(k==="b") return `<line class="acc" x1="${n[0]}" y1="${n[1]}" x2="${n[2]}" y2="${n[3]}" stroke-width="5"/>`;
const pts=[];for(let i=0;i<n.length;i+=2)pts.push(n[i]+","+n[i+1]);
return `<polyline points="${pts.join(" ")}" fill="none" stroke="currentColor" stroke-width="5"/>`;
}).join("");
return `<svg viewBox="0 0 120 90" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">${els}</svg>`;
}
// ---------- Programme par défaut ----------
const COLORS=["#0e9f6e","#2f6fdd","#7b3fe4","#f08a00","#0891b2","#db2777","#4b5563"];
let uid=0;const nid=()=>Date.now().toString(36)+(uid++).toString(36);
const ex=(name,pic,sets,reps,load,rest,tip)=>({id:nid(),name,pic,sets,reps,load,rest,tip});
const ABS=()=>[
ex("Relevés de jambes suspendu","abdos",3,"10-15","",60,"Monte le bassin, sans balancer."),
ex("Crunch à la poulie","abdos",3,"12-15","",60,"Enroule le dos, les hanches ne bougent pas."),
ex("Gainage","gainage",3,"45 s","",45,"Fessiers serrés, dos plat.")
];
const SEED={"title": "Mon programme muscu", "subtitle": "Objectif 65 kg de muscle pour fin juin", "days": [{"name": "Lundi", "focus": "Jambes", "color": "#0e9f6e", "exercises": [{"name": "Squat barre", "pic": "squat", "sets": 4, "reps": "6-8", "load": "", "rest": 180, "tip": "Descends au moins à la parallèle, genoux dans l’axe des pieds."}, {"name": "Soulevé de terre roumain", "pic": "souleve", "sets": 3, "reps": "8-10", "load": "", "rest": 150, "tip": "Dos plat, la barre frôle les cuisses."}, {"name": "Presse à cuisses", "pic": "presse", "sets": 3, "reps": "10-12", "load": "", "rest": 120, "tip": "Ne verrouille pas les genoux en haut."}, {"name": "Fentes bulgares", "pic": "fente", "sets": 3, "reps": "8-10 / jambe", "load": "", "rest": 90, "tip": "Buste droit, le genou avant reste stable."}, {"name": "Leg curl assis", "pic": "legcurl", "sets": 3, "reps": "10-12", "load": "", "rest": 90, "tip": "Descente lente, 2 à 3 secondes."}, {"name": "Mollets debout", "pic": "mollets", "sets": 4, "reps": "10-15", "load": "", "rest": 60, "tip": "Pause d’1 s en bas, étirement complet."}, {"name": "Relevés de jambes suspendu", "pic": "abdos", "sets": 3, "reps": "10-15", "load": "", "rest": 60, "tip": "Monte le bassin, sans balancer."}, {"name": "Crunch à la poulie", "pic": "abdos", "sets": 3, "reps": "12-15", "load": "", "rest": 60, "tip": "Enroule le dos, les hanches ne bougent pas."}, {"name": "Gainage", "pic": "gainage", "sets": 3, "reps": "45 s", "load": "", "rest": 45, "tip": "Fessiers serrés, dos plat."}]}, {"name": "Mardi", "focus": "Pecs, épaules, triceps", "color": "#2f6fdd", "exercises": [{"name": "Développé couché barre", "pic": "couche", "sets": 2, "reps": "10", "load": 40, "rest": 180, "tip": "Omoplates serrées, pieds ancrés au sol."}, {"name": "Developer coucher haltères", "pic": "curl", "sets": 3, "reps": "8", "load": 24, "rest": 90, "tip": ""}, {"name": "Développé incliné barre", "pic": "couche", "sets": 3, "reps": "8-10", "load": "", "rest": 120, "tip": "Banc à 30°, descends bas."}, {"name": "Développé militaire", "pic": "militaire", "sets": 3, "reps": "6-10", "load": 18, "rest": 150, "tip": "Abdos gainés, ne cambre pas."}, {"name": "Pec deck ou écarté poulie", "pic": "ecarte", "sets": 3, "reps": "12-15", "load": "", "rest": 75, "tip": "Bras légèrement fléchis, serre en fin de mouvement."}, {"name": "Élévations latérales", "pic": "laterales", "sets": 4, "reps": "12-20", "load": 9, "rest": 60, "tip": "Monte jusqu’aux épaules, sans élan."}, {"name": "Dips", "pic": "dips", "sets": 3, "reps": "8-12", "load": "", "rest": 120, "tip": "Buste penché en avant, coudes contrôlés."}, {"name": "Extension triceps poulie au-dessus de la tête", "pic": "triceps", "sets": 3, "reps": "10-15", "load": "", "rest": 75, "tip": "Coudes fixes, étirement complet."}, {"name": "Relevés de jambes suspendu", "pic": "abdos", "sets": 3, "reps": "10-15", "load": "", "rest": 60, "tip": "Monte le bassin, sans balancer."}, {"name": "Crunch à la poulie", "pic": "abdos", "sets": 3, "reps": "12-15", "load": "", "rest": 60, "tip": "Enroule le dos, les hanches ne bougent pas."}, {"name": "Gainage", "pic": "gainage", "sets": 3, "reps": "45 s", "load": "", "rest": 45, "tip": "Fessiers serrés, dos plat."}]}, {"name": "Mercredi", "focus": "Dos, biceps", "color": "#7b3fe4", "exercises": [{"name": "Tractions", "pic": "traction", "sets": 4, "reps": "6-10", "load": "", "rest": 150, "tip": "Menton au-dessus de la barre, descente complète."}, {"name": "Rowing barre", "pic": "rowing", "sets": 4, "reps": "8-10", "load": "", "rest": 150, "tip": "Tire vers le nombril, dos plat."}, {"name": "Tirage vertical prise serrée", "pic": "tirage", "sets": 3, "reps": "10-12", "load": "", "rest": 90, "tip": "Poitrine sortie, coudes vers les hanches."}, {"name": "Tirage horizontal", "pic": "tirage", "sets": 3, "reps": "10-12", "load": "", "rest": 90, "tip": "Serre les omoplates 1 s."}, {"name": "Face pull", "pic": "facepull", "sets": 3, "reps": "15", "load": "", "rest": 60, "tip": "Tire la corde vers le front, coudes hauts."}, {"name": "Curl barre EZ", "pic": "curl", "sets": 3, "reps": "8-10", "load": "", "rest": 90, "tip": "Coudes collés, pas de balancier."}, {"name": "Curl incliné haltères", "pic": "curl", "sets": 3, "reps": "10-12", "load": "", "rest": 75, "tip": "Bras derrière le buste, étirement max."}, {"name": "Relevés de jambes suspendu", "pic": "abdos", "sets": 3, "reps": "10-15", "load": "", "rest": 60, "tip": "Monte le bassin, sans balancer."}, {"name": "Crunch à la poulie", "pic": "abdos", "sets": 3, "reps": "12-15", "load": "", "rest": 60, "tip": "Enroule le dos, les hanches ne bougent pas."}, {"name": "Gainage", "pic": "gainage", "sets": 3, "reps": "45 s", "load": "", "rest": 45, "tip": "Fessiers serrés, dos plat."}]}, {"name": "Vendredi", "focus": "Full body", "color": "#f08a00", "exercises": [{"name": "Plyométrie : sauts sur box", "pic": "saut", "sets": 4, "reps": "5", "load": "", "rest": 90, "tip": "Une semaine sur deux. Réception souple et silencieuse."}, {"name": "Hack squat", "pic": "squat", "sets": 3, "reps": "8-10", "load": "", "rest": 150, "tip": "Amplitude complète, contrôle la descente."}, {"name": "Développé incliné machine", "pic": "couche", "sets": 3, "reps": "8-10", "load": "", "rest": 120, "tip": "Pousse sans décoller les épaules."}, {"name": "Rowing un bras", "pic": "rowing", "sets": 3, "reps": "10 / bras", "load": "", "rest": 90, "tip": "Coude près du corps, pas de rotation."}, {"name": "Hip thrust", "pic": "hipthrust", "sets": 3, "reps": "10", "load": "", "rest": 120, "tip": "Contracte les fessiers 1 s en haut."}, {"name": "Élévations latérales", "pic": "laterales", "sets": 3, "reps": "15", "load": "", "rest": 60, "tip": "Charge légère, contrôle total."}, {"name": "Superset curl + triceps poulie", "pic": "curl", "sets": 3, "reps": "12", "load": "", "rest": 60, "tip": "Enchaîne les deux sans pause."}, {"name": "Relevés de jambes suspendu", "pic": "abdos", "sets": 3, "reps": "10-15", "load": "", "rest": 60, "tip": "Monte le bassin, sans balancer."}, {"name": "Crunch à la poulie", "pic": "abdos", "sets": 3, "reps": "12-15", "load": "", "rest": 60, "tip": "Enroule le dos, les hanches ne bougent pas."}, {"name": "Gainage", "pic": "gainage", "sets": 3, "reps": "45 s", "load": "", "rest": 45, "tip": "Fessiers serrés, dos plat."}]}]};
function defaults(){const s=JSON.parse(JSON.stringify(SEED));s.days.forEach(d=>{d.id=nid();d.exercises.forEach(e=>e.id=nid());});return s;}
// ---------- État ----------
const LS="fiche-muscu-autonome-v1";
let state, active=0, done={}, saveTimer=null;
function loadLocal(){try{const r=localStorage.getItem(LS);if(r){const s=JSON.parse(r);if(s&&Array.isArray(s.days)&&s.days.length)return s;}}catch(e){}return null;}
state=loadLocal()||defaults();
const $=s=>document.querySelector(s);
const esc=s=>String(s??"").replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<").replace(/>/g,">");
const day=()=>state.days[Math.min(active,state.days.length-1)];
const findEx=id=>day().exercises.find(e=>e.id===id);
function status(t){$("#status").textContent=t;}
function save(){
state.updatedAt=Date.now();
clearTimeout(saveTimer);
saveTimer=setTimeout(()=>{try{localStorage.setItem(LS,JSON.stringify(state));status("Enregistré");}catch(e){status("Enregistrement impossible dans ce navigateur (navigation privée ?)");}},300);
}
// ---------- Rendu ----------
function fmtRest(s){s=+s||0;const m=Math.floor(s/60),r=s%60;return m?`${m}:${String(r).padStart(2,"0")}`:`${r} s`;}
function renderTabs(){
$("#tabs").innerHTML=state.days.map((d,i)=>`<button class="tab" role="tab" style="--tc:${d.color}" aria-selected="${i===active}" data-act="tab" data-i="${i}"><span class="dot"></span>${esc(d.name)}</button>`).join("")+
`<button class="tab add" data-act="addday">+ Séance</button>`;
}
function stepper(e,field,label,step,unit){
const v=e[field];
return `<div class="f"><label for="${field}-${e.id}">${label}</label><div class="row">
<button class="st" data-act="step" data-ex="${e.id}" data-f="${field}" data-s="-${step}" aria-label="Moins">−</button>
<input id="${field}-${e.id}" type="number" inputmode="decimal" step="${step}" min="0" value="${esc(v)}" data-ex="${e.id}" data-f="${field}" placeholder="–">
<button class="st" data-act="step" data-ex="${e.id}" data-f="${field}" data-s="${step}" aria-label="Plus">+</button>
</div>${unit?`<span class="unit">${unit}</span>`:""}</div>`;
}
function card(e,i,n){
const d=done[e.id]||[];
const sets=Array.from({length:Math.max(0,Math.min(12,+e.sets||0))},(_,k)=>`<button class="set ${d[k]?"on":""}" data-act="set" data-ex="${e.id}" data-k="${k}" aria-pressed="${!!d[k]}" aria-label="Série ${k+1}">${k+1}</button>`).join("");
const opts=Object.keys(PICS).map(k=>`<option value="${k}" ${k===e.pic?"selected":""}>${PIC_NAMES[k]}</option>`).join("");
return `<article class="ex">
<div class="pic">${picSVG(e.pic)}</div>
<div class="nameline">
<input class="name" value="${esc(e.name)}" data-ex="${e.id}" data-f="name" aria-label="Nom de l’exercice">
<div class="tipline"><b>●</b><textarea class="tip" rows="1" data-ex="${e.id}" data-f="tip" aria-label="Point clé" placeholder="Ajoute un point clé">${esc(e.tip)}</textarea></div>
</div>
<div class="grid">
${stepper(e,"sets","Séries",1,"")}
<div class="f"><label for="reps-${e.id}">Reps</label><div class="row"><input id="reps-${e.id}" value="${esc(e.reps)}" data-ex="${e.id}" data-f="reps"></div></div>
${stepper(e,"load","Charge",2.5,"kg")}
${stepper(e,"rest","Repos",15,fmtRest(e.rest))}
</div>
<div class="sets">${sets?`<span>Séries faites</span>${sets}`:""}</div>
<details class="more"><summary aria-label="Options de l’exercice">⋯</summary>
<div class="menu">
<label for="pic-${e.id}">Illustration</label>
<select id="pic-${e.id}" data-ex="${e.id}" data-f="pic">${opts}</select>
${i>0?`<button data-act="up" data-ex="${e.id}">Monter</button>`:""}
${i<n-1?`<button data-act="down" data-ex="${e.id}">Descendre</button>`:""}
<button data-act="dup" data-ex="${e.id}">Dupliquer</button>
<button class="danger" data-act="del" data-ex="${e.id}">Supprimer</button>
</div>
</details>
</article>`;
}
function totals(d){
const s=d.exercises.reduce((a,e)=>a+(+e.sets||0),0);
const sec=d.exercises.reduce((a,e)=>a+(+e.sets||0)*((+e.rest||0)+45),0);
return `${d.exercises.length} exos · ${s} séries · ~${Math.round(sec/60)} min`;
}
function render(){
if(active>=state.days.length)active=state.days.length-1;
const d=day();
$("#title").value=state.title||"";$("#subtitle").value=state.subtitle||"";
renderTabs();
const sw=COLORS.map(c=>`<button class="swatch" style="--s:${c}" aria-pressed="${c===d.color}" aria-label="Couleur" data-act="color" data-c="${c}"></button>`).join("");
$("#day").innerHTML=`<div style="--c:${d.color}">
<section class="dayhead">
<input class="dname" value="${esc(d.name)}" data-day="name" aria-label="Nom de la séance">
<input class="dfocus" value="${esc(d.focus)}" data-day="focus" aria-label="Muscles travaillés">
<div class="daytools">${sw}
<button class="ghost" data-act="clearsets">Nouvelle séance</button>
${state.days.length>1?`<button class="ghost" data-act="delday">Supprimer</button>`:""}
<span class="daystats" id="stats">${totals(d)}</span>
</div>
</section>
${d.exercises.map((e,i,a)=>card(e,i,a.length)).join("")}
<button class="addex" data-act="addex">+ Ajouter un exercice</button>
</div>`;
document.querySelectorAll(".tip").forEach(autosize);
}
function autosize(t){t.style.height="auto";t.style.height=t.scrollHeight+"px";}
// ---------- Interactions ----------
document.addEventListener("input",ev=>{
const t=ev.target;
if(t.id==="title"){state.title=t.value;save();return;}
if(t.id==="subtitle"){state.subtitle=t.value;save();return;}
if(t.dataset.day){day()[t.dataset.day]=t.value;if(t.dataset.day==="name")renderTabs();save();return;}
const id=t.dataset.ex,f=t.dataset.f;if(!id||!f)return;
const e=findEx(id);if(!e)return;
e[f]=(["sets","load","rest"].includes(f))?(t.value===""?"":+t.value):t.value;
if(t.classList.contains("tip"))autosize(t);
save();
if(f==="pic"){render();return;}
if(f==="sets"||f==="rest")softRefresh(e,t);
$("#stats").textContent=totals(day());
});
function softRefresh(e,input){
// met à jour bulles de séries / unité de repos sans perdre le focus
const art=input.closest(".ex");if(!art)return;
const tmp=document.createElement("div");tmp.innerHTML=card(e,0,1);
art.querySelector(".sets").replaceWith(tmp.querySelector(".sets"));
if(input.dataset.f==="rest"){const u=input.closest(".f").querySelector(".unit");if(u)u.textContent=fmtRest(e.rest);}
}
document.addEventListener("click",ev=>{
const b=ev.target.closest("[data-act]");if(!b)return;
const a=b.dataset.act, d=day(), list=d.exercises;
const idx=b.dataset.ex?list.findIndex(e=>e.id===b.dataset.ex):-1;
switch(a){
case "tab":active=+b.dataset.i;render();window.scrollTo({top:0});return;
case "addday":state.days.push({id:nid(),name:"Nouvelle séance",focus:"Muscles travaillés",color:COLORS[state.days.length%COLORS.length],exercises:[]});active=state.days.length-1;break;
case "delday":
if(b.dataset.confirm!=="1"){b.dataset.confirm="1";b.textContent="Confirmer la suppression";return;}
state.days.splice(active,1);active=Math.max(0,active-1);break;
case "color":d.color=b.dataset.c;break;
case "clearsets":list.forEach(e=>delete done[e.id]);render();return;
case "addex":list.push(ex("Nouvel exercice","curl",3,"10","",90,""));save();render();
setTimeout(()=>{const n=document.querySelectorAll(".ex .name");const el=n[n.length-1];if(el){el.focus();el.select();}},0);return;
case "up":if(idx>0)[list[idx-1],list[idx]]=[list[idx],list[idx-1]];break;
case "down":if(idx>=0&&idx<list.length-1)[list[idx+1],list[idx]]=[list[idx],list[idx+1]];break;
case "dup":if(idx>=0)list.splice(idx+1,0,{...list[idx],id:nid()});break;
case "del":if(idx>=0)list.splice(idx,1);break;
case "step":{
const e=list[idx],f=b.dataset.f,s=+b.dataset.s;
let v=(+e[f]||0)+s;v=Math.max(0,Math.round(v*10)/10);if(f==="sets")v=Math.min(12,v);
e[f]=v;save();
const inp=document.getElementById(f+"-"+e.id);if(inp)inp.value=v;
if(f==="sets"||f==="rest")softRefresh(e,inp);
$("#stats").textContent=totals(d);return;}
case "set":{
const e=list[idx],k=+b.dataset.k;done[e.id]=done[e.id]||[];
done[e.id][k]=!done[e.id][k];
b.classList.toggle("on",done[e.id][k]);b.setAttribute("aria-pressed",String(done[e.id][k]));
if(done[e.id][k]&&(+e.rest>0))startTimer(+e.rest,`${e.name} · série ${k+1} faite`);
return;}
default:return;
}
save();render();
});
document.addEventListener("click",ev=>{ // fermer les menus ouverts
document.querySelectorAll("details.more[open]").forEach(dt=>{if(!dt.contains(ev.target))dt.open=false;});
},true);
// ---------- Minuteur de repos ----------
let tEnd=0,tTotal=0,tInt=null;
function startTimer(sec,label){
tTotal=sec;tEnd=Date.now()+sec*1000;$("#tl").textContent=label;
$("#timer").classList.add("show");$("#timer").classList.remove("end");
clearInterval(tInt);tick();tInt=setInterval(tick,250);
}
function tick(){
const left=Math.max(0,Math.ceil((tEnd-Date.now())/1000));
$("#tt").textContent=Math.floor(left/60)+":"+String(left%60).padStart(2,"0");
$("#tbar").style.width=(tTotal?(1-left/tTotal)*100:100)+"%";
if(left<=0){clearInterval(tInt);$("#timer").classList.add("end");$("#tl").textContent="Repos terminé, série suivante !";
try{navigator.vibrate&&navigator.vibrate([200,100,200]);}catch(e){}}
}
$("#tplus").onclick=()=>{tEnd=Math.max(tEnd,Date.now())+15000;tTotal+=15;$("#timer").classList.remove("end");clearInterval(tInt);tInt=setInterval(tick,250);tick();};
$("#tstop").onclick=()=>{clearInterval(tInt);$("#timer").classList.remove("show");};
// ---------- PDF ----------
const pdfTxt=s=>String(s??"").replace(/[’‘]/g,"'").replace(/[“”]/g,'"').replace(/·/g,"-");
const rgb=h=>[parseInt(h.slice(1,3),16),parseInt(h.slice(3,5),16),parseInt(h.slice(5,7),16)];
function buildPDF(){
const {jsPDF}=window.jspdf;const doc=new jsPDF({unit:"mm",format:"a4"});
const W=doc.internal.pageSize.getWidth(),H=doc.internal.pageSize.getHeight(),M=14;
doc.setFillColor(20,40,58);doc.rect(0,0,W,30,"F");
doc.setTextColor(255,255,255);doc.setFont("helvetica","bold");doc.setFontSize(22);
doc.text(pdfTxt(state.title||"Mon programme"),M,15);
doc.setFont("helvetica","normal");doc.setFontSize(11);doc.text(pdfTxt(state.subtitle||""),M,23);
let y=40;
state.days.forEach(d=>{
if(y>H-50){doc.addPage();y=18;}
const c=rgb(d.color);
doc.setFillColor(...c);doc.roundedRect(M,y-6,W-2*M,12,2,2,"F");
doc.setTextColor(255,255,255);doc.setFont("helvetica","bold");doc.setFontSize(13);
doc.text(pdfTxt(d.name),M+4,y+1.8);
doc.setFont("helvetica","normal");doc.setFontSize(10);
doc.text(pdfTxt(d.focus),M+4+doc.getTextWidth(pdfTxt(d.name))*1.35+4,y+1.6);
doc.autoTable({
startY:y+8,margin:{left:M,right:M},
head:[["Exercice","Séries","Reps","Charge","Repos","Point clé"]],
body:d.exercises.map(e=>[pdfTxt(e.name),String(e.sets??""),pdfTxt(e.reps),e.load!==""&&e.load!=null?e.load+" kg":"",pdfTxt(fmtRest(e.rest)),pdfTxt(e.tip)]),
styles:{fontSize:9,cellPadding:2.2,valign:"middle",textColor:[20,40,58],lineColor:[214,223,231],lineWidth:.2},
headStyles:{fillColor:[238,243,247],textColor:c,fontStyle:"bold"},
columnStyles:{0:{fontStyle:"bold",cellWidth:46},1:{halign:"center",cellWidth:15},2:{halign:"center",cellWidth:22},3:{halign:"center",cellWidth:18},4:{halign:"center",cellWidth:15},5:{textColor:[214,40,40],fontStyle:"bold"}},
alternateRowStyles:{fillColor:[248,250,252]}
});
y=doc.lastAutoTable.finalY+14;
});
if(y>H-50){doc.addPage();y=18;}
doc.setTextColor(214,40,40);doc.setFont("helvetica","bold");doc.setFontSize(13);doc.text("Règles qui font progresser",M,y);y+=7;
doc.setFont("helvetica","normal");doc.setFontSize(10);doc.setTextColor(20,40,58);
["Progression : quand tu fais le haut de la fourchette sur toutes les séries, augmente la charge.",
"Intensité : arrête chaque série à 1 ou 2 reps de l'échec, mouvement propre.",
"Repos : 2 à 3 min sur les gros exercices, 1 à 2 min sur l'isolation.",
"Objectif poids : environ +0,5 à 0,7 kg par mois, 110-120 g de protéines par jour, 8 h de sommeil."
].forEach(l=>{const lines=doc.splitTextToSize("- "+l,W-2*M);doc.text(lines,M,y);y+=lines.length*5+1.5;});
return doc.output("blob");
}
let dlNS;const dlReady=(async()=>{try{dlNS=(window.claude&&claude.use)?await claude.use("downloads"):null;}catch(e){dlNS=null;}})();
async function offer(filename,data){
await dlReady;
if(dlNS){
try{await dlNS.save({filename,data});status("Fichier prêt");}
catch(e){status(e&&e.code==="declined"?"Téléchargement annulé":"Téléchargement impossible pour le moment");}
return;
}
try{
const blob=data instanceof Blob?data:new Blob([data],{type:"application/json"});
const url=URL.createObjectURL(blob);const a=document.createElement("a");
a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove();
setTimeout(()=>URL.revokeObjectURL(url),4000);status("Fichier téléchargé");
}catch(e){status("Téléchargement impossible");}
}
$("#pdf").onclick=()=>{
if(!window.jspdf){status("Le PDF demande une connexion internet. Connecte-toi puis recharge la page.");return;}
try{offer("programme-muscu.pdf",buildPDF());}catch(e){status("Erreur pendant la création du PDF");}
};
$("#json").onclick=()=>offer("programme-muscu.json",JSON.stringify(state,null,2));
$("#importBtn").onclick=()=>$("#importFile").click();
$("#importFile").onchange=async ev=>{
const f=ev.target.files[0];if(!f)return;
try{const s=JSON.parse(await f.text());if(!s||!Array.isArray(s.days))throw 0;state=s;active=0;done={};save();render();status("Programme importé");}
catch(e){status("Ce fichier n’est pas une copie de programme valide.");}
ev.target.value="";
};
$("#reset").onclick=ev=>{
const b=ev.currentTarget;
if(b.dataset.confirm!=="1"){b.dataset.confirm="1";b.textContent="Confirmer : tout remettre par défaut";return;}
state=defaults();active=0;done={};b.dataset.confirm="";b.textContent="Réinitialiser";save();render();
};
render();
</script>
</body>
</html>
35,395 chars · codepastes.com