blognews-elementor-demo-four-one-ad-banner
Off-Canvas Header
Please select a template

Je vais te livrer un bloc UI complet en HTML/CSS/JS : 6 jauges, un score calculé, une “priorité hebdo” automatique et un texte dynamique, avec des données d’exemple faciles à brancher sur tes valeurs Supabase.

Voici une UI “exacte” (copier-coller) du bloc Écart Objectif ↔ Réalité : 6 jauges + 1 score + 1 priorité hebdo + texte dynamique.
Tout est en HTML/CSS/JS vanilla, conçu pour s’intégrer dans ton dashboard (WordPress/Elementor, page HTML, etc.). Tu n’as qu’à remplacer l’objet data par tes valeurs réelles (Supabase).

Écart Objectif ↔ Réalité

On ne juge pas. On ajuste. Un seul focus à la fois.

Recomp Score
--
Calcul en cours…
Calories
--
Protéines
--
Activité (NEAT)
--
Entraînement
--
Sommeil / Récup
--
Qualité (micro)
--

Priorité hebdo

Diagnostic (texte dynamique)

Astuce : on optimise le prochain pas, pas l’ego.

Comment le brancher à tes vraies données (Supabase)

Tu remplaces data.targets et data.actuals par :

  • targets : depuis macro_targets (kcal/protéines), metabolism_settings (steps), goals (mode/phase), et ton “micro_score cible”

  • actuals : moyenne des 7 derniers jours (nutrition_logs, checkins, workouts)

Bon pattern : calcule tes moyennes côté app (JS) ou via une view SQL.


Bonus (pro) : les 6 jauges et leurs sources

  • Calories → nutrition_logs (kcal) vs macro_targets.kcal_target

  • Protéines → nutrition_logs.protein_g vs macro_targets.protein_g

  • NEAT → metabolism_settings.steps_per_day vs “steps réels” (si tu les logges) sinon proxy

  • Entraînement → workout_sessions count/sem vs cible

  • Sommeil → checkins.sleep_hours vs cible

  • Qualité micro → score calculé (à partir d’un mini questionnaire ou d’un vrai tracking)

Oui — dans WeWeb, le plus propre c’est : HTML/CSS dans un “Custom Code / HTML Embed” + JS dans un Workflow “On page load” via “Custom JavaScript action” (c’est la méthode recommandée pour exécuter du JS de manière fiable).

Pourquoi pas juste coller <script> dans le bloc HTML ?

Ça peut marcher, mais c’est plus fragile (timing de chargement, editor vs publié). WeWeb pousse justement le pattern Workflow → Custom JavaScript.


Intégration BODYNUT “Gap Block” dans WeWeb (pas à pas)

1) Récupérer tes données Supabase dans WeWeb

  1. Active le plugin Supabase dans WeWeb (data source).

  2. Crée des Collections (requêtes) :

    • targets (ex: macro_targets actif + metabolism_settings + goals actif)

    • actuals (moyenne 7 jours : nutrition_logs, checkins, workout_sessions)

💡 Astuce pro : fais une VIEW côté Supabase (ex: dashboard_gap_view) qui te renvoie déjà targets + actuals + context en une requête → WeWeb adore ça (moins de logique dispersée).


2) Créer une variable “gapData” dans WeWeb

  • Dans WeWeb → Variables → crée une variable Object : gapData

  • Elle contiendra exactement ce que ton JS attend :

     
    { targets: {...}, actuals: {...}, context: {...} }

Ensuite :

  • Workflow “On page load” :

    • Action : Fetch ta collection (ou ta view)

    • Action : Set variable gapData = result


3) Mettre le HTML/CSS du bloc dans la page

Dans l’éditeur WeWeb :

  • Ajoute un élément Custom Code (ou HTML Embed)

  • Colle uniquement :

    • le <section …>...</section>

    • le <style>...</style>

  • ⚠️ Ne colle pas le gros <script> ici (on va le mettre dans Workflow).

Important :

  • garde un id stable sur le root : id="bodynut-gap"


4) Mettre le JS dans un Workflow WeWeb

WeWeb → Page settings → Workflows → On page load :

  • Ajoute une action Custom JavaScript

  • Colle une version “fonction” du script (ci-dessous).

💡 Option bonus : crée aussi un Workflow “On variable change (gapData)” si tu veux que ça se mette à jour quand tu changes de date / profil.


Le JS “WeWeb-friendly” (à coller dans Custom JavaScript action)

👉 L’idée : on lit la variable gapData et on appelle renderGap(data).

 
// 1) Récupérer la variable WeWeb (remplace "gapData" par l'ID/nom exact si besoin) const data = wwLib.wwVariable.getValue("gapData"); // 2) Sécurité if (!data || !data.targets || !data.actuals) { console.warn("BODYNUT Gap: data manquante", data); return; } // 3) Render renderBodynutGap(data, "bodynut-gap"); // ---- RENDERER (version compacte) ---- function renderBodynutGap(data, rootId){ const root = document.getElementById(rootId); if(!root){ console.warn("BODYNUT Gap: root introuvable"); return; } const clamp = (n,a,b)=>Math.max(a,Math.min(b,n)); function adherenceScore(actual,target,tolPct){ if(target==null||actual==null) return 50; const tol = Math.abs(target)*(tolPct/100); const diff = Math.abs(actual-target); const raw = 100 - (diff/(tol||1))*100; return clamp(raw,0,100); } const pillFromScore = s => (s>=85?"OK":s>=70?"Bien":s>=55?"À corriger":"Priorité"); const formatDelta = (a,t,u="")=>{ const d=a-t; const sign=d>0?"+":""; return `${sign}${Math.round(d)}${u}`; }; const scores = { kcal: adherenceScore(data.actuals.kcal, data.targets.kcal, 7), protein: adherenceScore(data.actuals.protein_g, data.targets.protein_g, 8), neat: adherenceScore(data.actuals.steps, data.targets.steps, 12), training: adherenceScore(data.actuals.training_sessions_per_week, data.targets.training_sessions_per_week, 15), sleep: adherenceScore(data.actuals.sleep_hours, data.targets.sleep_hours, 10), quality: clamp(data.actuals.micro_score ?? 50, 0, 100), }; const weights = { kcal:0.22, protein:0.22, neat:0.18, training:0.16, sleep:0.12, quality:0.10 }; const recompScore = Math.round(Object.keys(weights).reduce((acc,k)=>acc+scores[k]*weights[k],0)); const order = ["protein","kcal","neat","training","sleep","quality"]; const priorityKey = order.reduce((w,k)=>scores[k]<scores[w]?k:w, order[0]); const labels = { kcal:"Calories", protein:"Protéines", neat:"NEAT", training:"Entraînement", sleep:"Sommeil", quality:"Qualité micro" }; // Helpers DOM const setTxt = (id, v)=>{ const el=document.getElementById(id); if(el) el.textContent=v; }; const setFill = (id, v)=>{ const el=document.getElementById(id); if(el) el.style.width=`${clamp(v,0,100)}%`; }; // Score box setTxt("recompScore", recompScore); // Gauges setFill("fill-kcal", scores.kcal); setTxt("pill-kcal", pillFromScore(scores.kcal)); setTxt("meta-kcal", `Cible ${data.targets.kcal} kcal · Réel ${data.actuals.kcal} kcal (${formatDelta(data.actuals.kcal,data.targets.kcal," kcal")})`); setFill("fill-protein", scores.protein); setTxt("pill-protein", pillFromScore(scores.protein)); setTxt("meta-protein", `Cible ${data.targets.protein_g} g · Réel ${data.actuals.protein_g} g (${formatDelta(data.actuals.protein_g,data.targets.protein_g," g")})`); setFill("fill-neat", scores.neat); setTxt("pill-neat", pillFromScore(scores.neat)); setTxt("meta-neat", `Cible ${data.targets.steps} pas · Réel ${data.actuals.steps} pas (${formatDelta(data.actuals.steps,data.targets.steps," pas")})`); setFill("fill-training", scores.training); setTxt("pill-training", pillFromScore(scores.training)); setTxt("meta-training", `Cible ${data.targets.training_sessions_per_week}/sem · Réel ${data.actuals.training_sessions_per_week}/sem`); setFill("fill-sleep", scores.sleep); setTxt("pill-sleep", pillFromScore(scores.sleep)); setTxt("meta-sleep", `Cible ${data.targets.sleep_hours} h · Réel ${data.actuals.sleep_hours} h`); setFill("fill-quality", scores.quality); setTxt("pill-quality", pillFromScore(scores.quality)); setTxt("meta-quality", `Score micro: ${data.actuals.micro_score}/100 · Cible: ${data.targets.micro_score}/100`); // Priorité hebdo setTxt("prioTag", labels[priorityKey]); const prioMain = document.getElementById("prioMain"); const prioActions = document.getElementById("prioActions"); if(prioActions) prioActions.innerHTML = ""; const addChip = (t)=>{ if(!prioActions) return; const d=document.createElement("div"); d.className="chip"; d.textContent=t; prioActions.appendChild(d); }; if(prioMain){ if(priorityKey==="protein"){ const need = data.targets.protein_g - data.actuals.protein_g; prioMain.textContent = `Augmente tes protéines : +${Math.max(15, Math.round(need))} g/j pendant 7 jours.`; addChip("1 portion protéine/jour"); addChip("25–40 g / repas"); addChip("Petit-déj + post-training"); } else if(priorityKey==="kcal"){ const delta = data.actuals.kcal - data.targets.kcal; const dir = delta>0 ? "réduis" : "augmente"; prioMain.textContent = `Calories : ${dir} ~${Math.min(200, Math.max(80, Math.abs(Math.round(delta))))} kcal/j (sans toucher aux protéines).`; addChip("Protéines constantes"); addChip("Ajuste G/L"); addChip("Re-test 7 jours"); } else if(priorityKey==="neat"){ const need = data.targets.steps - data.actuals.steps; prioMain.textContent = `NEAT : +${Math.max(1500, Math.round(need))} pas/j pendant 7 jours.`; addChip("1 marche 10–15 min"); addChip("2 mini-marches"); addChip("Régularité"); } else if(priorityKey==="sleep"){ const need = data.targets.sleep_hours - data.actuals.sleep_hours; prioMain.textContent = `Sommeil : +${Math.max(0.5, Math.round(need*10)/10)} h au lit, 5j/7.`; addChip("Coucher fixe"); addChip("Écran off 30 min"); addChip("Caféine plus tôt"); } else if(priorityKey==="training"){ prioMain.textContent = `Entraînement : verrouille ${data.targets.training_sessions_per_week} séances/semaine avant d’optimiser le reste.`; addChip("Séances courtes OK"); addChip("+1 rep ou +1 kg"); addChip("Régularité"); } else { prioMain.textContent = "Qualité micro : 2 superfoods/jour + fibres progressives, cette semaine."; addChip("1 fruit + 1 légume"); addChip("Oméga-3 / jour"); addChip("Fermenté 3×/sem"); } } // Texte dynamique (simple) const insight = document.getElementById("insightText"); if(insight){ const t = data.context || {}; const trend = t.weight_trend_7d ?? 0; const mode = t.goal_mode || "recomp"; const phase = t.phase || "cut"; const modeTxt = {cut:"perte de gras",build:"prise de muscle",recomp:"recomposition",maintain:"maintien"}[mode] || "objectif"; let p = `<p><b>Objectif :</b> ${modeTxt}. <b>Phase :</b> ${phase}.</p>`; if(trend < -0.1) p += `<p>Tendance poids OK (${trend} kg/sem). On réduit l’écart via <b>${labels[priorityKey]}</b>.</p>`; else if(trend < 0.1) p += `<p>Tendance plate (${trend} kg/sem). Focus <b>${labels[priorityKey]}</b> → 7 jours → réévaluation.</p>`; else p += `<p>Tendance en hausse (${trend} kg/sem). On sécurise l’exécution (kcal/protéines/NEAT) avant de corriger plus fort.</p>`; insight.innerHTML = p; } }

📌 Ce code utilise wwLib.wwVariable.getValue(...) pour lire une variable WeWeb (c’est le pont standard entre ton app no-code et ton JS).
Et l’exécution via Workflow “Custom JavaScript action” est la voie officielle.


La meilleure pratique WeWeb (pour éviter les bugs)

  • HTML/CSS : dans le composant “Custom Code / HTML Embed”

  • JS : dans Workflow “On page load” (et éventuellement “On variable change”)

  • Data : dans une variable gapData remplie via tes Collections Supabase

C’est le trio “stable”.


Si tu veux le mode “ultra propre”

Tu peux en faire un Custom Component (Vue) réutilisable partout, mais c’est un cran plus technique. WeWeb a des ressources pour développer des composants custom si tu veux industrialiser.


Si tu me dis comment tu récupères tes “actuals” (tu logges les pas/sommeil dans tes tables, ou tu les estimes), je te fournis la VIEW SQL Supabase dashboard_gap_view qui renvoie directement l’objet complet (targets/actuals/context) — comme ça WeWeb ne fait qu’une requête et ton bloc s’alimente tout seul.

 
 
What’s your Reaction?
Love
Love
0
Smile
Smile
0
Haha
Haha
0
Sad
Sad
0
Star
Star
0
Weary
Weary
0

Adhésion libre, accès complet : rejoignez iki707, explorez tout le contenu et soutenez le webzine sans obligation.

X
Retour en haut
0
Vous avez apprécié cet article ? Merci de laisser un commentaire.x