feat: implement 6 pillars, dynamic badge fctory and security

This commit is contained in:
Marc Lasserre 2026-05-28 23:23:00 +02:00
parent 0a13344a2e
commit a917500aac
Signed by: M7s
GPG Key ID: F74837410959661B
4 changed files with 517 additions and 403 deletions

View File

@ -7,6 +7,18 @@ et ce projet respecte le [Versionnement Sémantique](https://semver.org/lang/fr/
--- ---
## [3.1.3] - 2026-05-28
### Ajouté
- **Cockpit Holistique à 6 Piliers :** Extension du tableau de bord de 4 à 6 indicateurs circulaires SVG autonomes représentant l'intégralité des dimensions de l'athlète (Entraînement, Récupération, Nutrition, Hydratation, Soins, Matériel).
- **Usine à Badges Dynamique :** Implémentation d'un générateur d'activités et de tâches de maintenance à la volée via un formulaire JavaScript natif interconnecté au moteur de planification.
- **Sécurisation de la Supply Chain (SRI) :** Intégration des verrous d'intégrité cryptographique `integrity` (SHA-384) sur les liaisons CDN externes pour se conformer aux exigences de sécurité strictes de SonarQube.
### Modifié
- **Recentrage Produit (MVP) :** Nettoyage architectural complet de l'interface pour éliminer les briques tierces obsolètes (Espace Cohorte, Administrateur, MCO Infra) et recentrer l'application sur le profil unique de l'athlète d'endurance.
- **Moteur de Rendu Temporel :** Refonte de la logique événementielle dans `app.js` pour détruire et réassigner dynamiquement les écouteurs de glisser-déposer (*Drag & Drop*) sur les nouveaux composants injectés en cours de session.
- **Feuille de Style Souveraine :** Élimination des conflits d'opacité liés aux classes de transition natives de Bootstrap au profit d'un contrôle d'affichage exclusif et performant en CSS pur.
## [3.1.0] - 2026-05-27 ## [3.1.0] - 2026-05-27
### Ajouté ### Ajouté
- **Moteur Drag & Drop Natif :** Implémentation d'un système de planification agile (Kanban) dans `app.js` permettant de glisser-déposer les séances d'entraînement à la souris sans aucune dépendance logicielle externe. - **Moteur Drag & Drop Natif :** Implémentation d'un système de planification agile (Kanban) dans `app.js` permettant de glisser-déposer les séances d'entraînement à la souris sans aucune dépendance logicielle externe.

183
app.js
View File

@ -1,83 +1,148 @@
document.addEventListener("DOMContentLoaded", () => { /**
// --- NAV SWITCHER DES ONGLETS LATÉRAUX --- * ====================================================================
const navLinks = document.querySelectorAll(".nav-link"); * RYM HORIZON ENGINE APPLICATION (v3.1.3)
const tabContents = document.querySelectorAll(".tab-content"); * ====================================================================
* Philosophie : Code découplé, souveraineté et performance pure.
* Architecture : Gestionnaire de vues + Moteur Drag & Drop + Usine à badges.
* Conformité : Sans dépendances tierces intrusives.
*/
navLinks.forEach(link => { document.addEventListener('DOMContentLoaded', () => {
link.addEventListener("click", (e) => {
e.preventDefault(); // --- INTÉGRATION DE LA CHRONOLOGIE (SESSION LOG) ---
navLinks.forEach(l => l.classList.remove("active")); console.log("RYM Horizon Engine v3.1.3 : Initialisation du système...");
tabContents.forEach(tc => tc.classList.remove("active"));
link.classList.add("active"); // ==========================================
const tabId = link.getAttribute("data-tab"); // MODULE 1 : MANAGER DE NAVIGATION SOUVERAIN
const targetSection = document.getElementById(tabId); // ==========================================
if (targetSection) { const tabButtons = document.querySelectorAll('.nav-view-switcher .nav-link');
targetSection.classList.add("active"); const tabPanes = document.querySelectorAll('.tab-content .tab-pane');
}
tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
const targetTabId = button.getAttribute('data-tab');
// 1. Mise à jour de l'état actif sur les boutons du menu
tabButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// 2. Commutation stricte des conteneurs de vues (Géré via CSS display !important)
tabPanes.forEach(pane => {
if (pane.id === targetTabId) {
pane.classList.add('active');
} else {
pane.classList.remove('active');
}
});
console.log(`[Navigation] Passage sur la vue : ${targetTabId}`);
}); });
}); });
// --- DRAG & DROP ENGINE INTELLIGENT ---
const draggables = document.querySelectorAll(".rym-draggable-workout");
const dropZones = document.querySelectorAll(".drop-zone");
function recalculerChargeHebdo() { // ==========================================
let totalSéances = 0; // MODULE 2 : USINE DE CRÉATION DE BADGES (À LA VOLÉE)
// ==========================================
const generatorForm = document.getElementById('rym-coach-generator');
const productionZone = document.getElementById('badge-production-zone');
// Dictionnaire de configuration des composants (Couleurs CSS natifs et Icônes)
const typeDictionary = {
run: { cssClass: 'run', icon: 'bi-lightning-fill' },
bike: { cssClass: 'bike', icon: 'bi-bicycle' },
swim: { cssClass: 'swim', icon: 'bi-water' },
matos: { cssClass: 'run', icon: 'bi-gear-wide-connected', customStyle: 'background: #fef2f2; color: #991b1b; border: 1px dashed #f87171;' },
soins: { cssClass: 'swim', icon: 'bi-heart-pulse-fill', customStyle: 'background: #faf5ff; color: #6b21a8; border: 1px dashed #c084fc;' }
};
generatorForm.addEventListener('submit', (e) => {
e.preventDefault(); // Bloque le rechargement de la page pour préserver le cache local
const type = document.getElementById('gen-type').value;
const title = document.getElementById('gen-title').value;
const config = typeDictionary[type];
const uniqueId = `w-generated-${Date.now()}`; // Identifiant unique basé sur le timestamp
// Nettoyage de la zone de production
productionZone.innerHTML = '';
// Création du nœud DOM du badge
const badge = document.createElement('div');
badge.id = uniqueId;
badge.className = `workout ${config.cssClass} rym-draggable-workout`;
badge.setAttribute('draggable', 'true');
dropZones.forEach(zone => { if (config.customStyle) {
totalSéances += zone.querySelectorAll(".rym-draggable-workout").length; badge.setAttribute('style', config.customStyle);
}
// Injection du contenu structuré
badge.innerHTML = `<i class="bi ${config.icon} me-1"></i> ${title}`;
// Activation immédiate de la mécanique Drag sur le nouveau composant
attachDragEventsToElement(badge);
// Rendu final dans l'interface
productionZone.appendChild(badge);
generatorForm.reset();
console.log(`[Engine] Nouveau badge instancié avec succès ID: ${uniqueId}`);
});
// ==========================================
// MODULE 3 : MOTEUR DRAG & DROP MULTI-CIBLES
// ==========================================
let draggedElement = null;
/**
* Attache les écouteurs d'événements Drag à un élément unique
* @param {HTMLElement} element
*/
function attachDragEventsToElement(element) {
element.addEventListener('dragstart', (e) => {
draggedElement = element;
e.dataTransfer.setData('text/plain', element.id);
element.style.opacity = '0.4';
console.log(`[Drag] Début du déplacement de l'élément : ${element.id}`);
}); });
// Calcul dynamique : 25% de charge par bloc actif, plafonné à 100% element.addEventListener('dragend', () => {
const nouvelleCharge = Math.min(totalSéances * 25, 100); element.style.opacity = '1';
draggedElement = null;
// Cibler le deuxième spinner du dashboard (Charge Hebdo) });
const spinnerCharge = document.querySelectorAll(".rym-spinner-wrapper")[1];
if (spinnerCharge) {
const cercleStroke = spinnerCharge.querySelector(".circle-stroke");
const texteValeur = spinnerCharge.querySelector(".rym-spinner-text span");
texteValeur.textContent = nouvelleCharge;
cercleStroke.setAttribute("stroke-dasharray", `${nouvelleCharge}, 100`);
}
} }
draggables.forEach(draggable => { // Initialisation du moteur Drag sur les cartes pré-existantes au boot
draggable.addEventListener("dragstart", (e) => { document.querySelectorAll('.rym-draggable-workout').forEach(attachDragEventsToElement);
e.dataTransfer.setData("text/plain", draggable.id);
setTimeout(() => { draggable.style.display = "none"; }, 0);
});
draggable.addEventListener("dragend", () => { // Configuration des zones de réception (Les jours de la semaine)
draggable.style.display = "block"; const dropZones = document.querySelectorAll('.drop-zone');
});
});
dropZones.forEach(zone => { dropZones.forEach(zone => {
zone.addEventListener("dragover", (e) => { zone.addEventListener('dragover', (e) => {
e.preventDefault(); e.preventDefault(); // Indispensable pour autoriser le drop dans le navigateur
zone.classList.add("drag-over"); zone.classList.add('drag-over');
}); });
zone.addEventListener("dragleave", () => { zone.addEventListener('dragleave', () => {
zone.classList.remove("drag-over"); zone.classList.remove('drag-over');
}); });
zone.addEventListener("drop", (e) => { zone.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
zone.classList.remove("drag-over"); zone.classList.remove('drag-over');
const id = e.dataTransfer.getData("text/plain"); if (draggedElement) {
const draggableElement = document.getElementById(id); // Déplacement physique du nœud dans le conteneur du jour ciblé
zone.appendChild(draggedElement);
if (draggableElement) {
zone.appendChild(draggableElement); // Si le badge provient de la zone de production, on remet le texte d'attente
recalculerChargeHebdo(); if (productionZone.children.length === 0) {
productionZone.innerHTML = 'Aucun badge généré';
}
console.log(`[Drop] Élément réassigné au jour : ${zone.getAttribute('data-day')}`);
} }
}); });
}); });
// Calcul initial
recalculerChargeHebdo();
}); });

View File

@ -3,244 +3,261 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RYM Horizon | Écosystème Souverain</title> <title>RYM Horizon — Cockpit Intégré v3.1.3</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
rel="stylesheet"
integrity="sha384-4LISF5TTv3qMT719A6O7faWdgANoF94m85M84IfTWM5cKWAYjHL0ive8SkWAAAAA"
crossorigin="anonymous"
>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body> <body>
<div class="container-fluid"> <header class="bg-white border-bottom py-3 sticky-top">
<div class="row"> <div class="container-fluid px-4 d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<nav class="col-md-2 d-none d-md-block sidebar p-4 sticky-top"> <span class="fw-bold fs-4 me-4">Rym<span class="text-primary">Horizon</span></span>
<h4 class="fw-bold text-center mb-5">RYM <span class="text-info">HORIZON</span></h4> <div class="nav nav-view-switcher" role="tablist">
<button class="nav-link" data-tab="view-day">Jour</button>
<div class="sidebar-heading">Admin</div> <button class="nav-link active" data-tab="view-week">Semaine</button>
<ul class="nav flex-column"> <button class="nav-link" data-tab="view-month">Mois</button>
<li class="nav-item"> <button class="nav-link" data-tab="view-macro">Macro</button>
<a class="nav-link active" data-tab="dashboard"><i class="bi bi-shield-check me-2"></i> MCO Dashboard</a> </div>
</li> </div>
</ul> <div class="d-flex align-items-center">
<div class="text-end me-3">
<div class="sidebar-heading">Finance</div> <small class="text-muted d-block" style="font-size: 0.7rem;">RYM BANK</small>
<ul class="nav flex-column"> <span class="fw-bold">19 420,00 €</span>
<li class="nav-item"> </div>
<a class="nav-link" data-tab="bank"><i class="bi bi-bank me-2"></i> RYM Bank</a> <i class="bi bi-wallet2 fs-4 text-primary"></i>
</li> </div>
</ul>
<div class="sidebar-heading">Utilisateur</div>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" data-tab="activity"><i class="bi bi-heart-pulse me-2"></i> RYM Coach</a>
</li>
</ul>
<div class="sidebar-heading">Expert</div>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" data-tab="clients"><i class="bi bi-people me-2"></i> Suivi Cohorte</a>
</li>
</ul>
</nav>
<main class="col-md-10 ms-sm-auto px-md-5 py-4">
<section id="dashboard" class="tab-content active">
<h2 class="mb-4">MCO Dashboard</h2>
<div class="card card-rym p-3 w-25">
<h6 class="text-muted">Status Infra</h6>
<p class="text-success mb-0">● Online - France (OVH)</p>
</div>
</section>
<section id="bank" class="tab-content">
<h2 class="mb-4">RYM Bank</h2>
<div class="card card-rym p-4 bg-primary text-white w-50 shadow-lg">
<h6>Solde Courant</h6>
<h2 class="fw-bold">12 450,80 €</h2>
</div>
</section>
<section id="activity" class="tab-content">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>RYM Coach <small class="text-muted fs-6">| Mon Planning</small></h2>
<span class="badge bg-primary">Saison : Triathlon</span>
</div>
<div class="card card-rym p-4 shadow-sm mb-4">
<h5 class="mb-3 text-primary"><i class="bi bi-activity me-2"></i>Indicateurs de Performance & Souveraineté</h5>
<div class="row row-cols-2 row-cols-sm-2 row-cols-md-4 g-3 text-center justify-content-center">
<div class="col">
<div class="rym-spinner-wrapper">
<svg class="rym-spinner-svg" viewBox="0 0 36 36">
<path class="circle-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<path class="circle-stroke text-success" stroke-dasharray="82, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
</svg>
<div class="rym-spinner-text">
<span class="fw-bold fs-5">82</span><small>%</small>
</div>
</div>
<div class="small fw-semibold mt-2">Fraîcheur VRC</div>
</div>
<div class="col">
<div class="rym-spinner-wrapper">
<svg class="rym-spinner-svg" viewBox="0 0 36 36">
<path class="circle-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<path class="circle-stroke text-warning" stroke-dasharray="65, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
</svg>
<div class="rym-spinner-text">
<span class="fw-bold fs-5">65</span><small>%</small>
</div>
</div>
<div class="small fw-semibold mt-2">Charge Hebdo</div>
</div>
<div class="col">
<div class="rym-spinner-wrapper">
<svg class="rym-spinner-svg" viewBox="0 0 36 36">
<path class="circle-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<path class="circle-stroke text-info" stroke-dasharray="45, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
</svg>
<div class="rym-spinner-text">
<span class="fw-bold fs-5">45</span><small>%</small>
</div>
</div>
<div class="small fw-semibold mt-2">Chaîne Vélo</div>
</div>
<div class="col">
<div class="rym-spinner-wrapper">
<svg class="rym-spinner-svg" viewBox="0 0 36 36">
<path class="circle-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<path class="circle-stroke text-danger" stroke-dasharray="90, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
</svg>
<div class="rym-spinner-text">
<span class="fw-bold fs-5">90</span><small>%</small>
</div>
</div>
<div class="small fw-semibold mt-2">Paires Carbone</div>
</div>
</div>
</div>
<div class="card card-rym p-4 shadow-sm mb-5">
<h5 class="mb-3 text-primary"><i class="bi bi-calendar-week me-2"></i>Vue Hebdomadaire Dynamique</h5>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 row-cols-lg-7 g-2" id="planning-kanban">
<div class="col">
<div class="bg-light p-2 rounded border h-100 drop-zone" data-day="Lun">
<div class="fw-bold text-center border-bottom pb-1 mb-2 bg-dark text-white rounded-top small">Lundi</div>
<div class="rym-draggable-workout bg-primary text-white p-2 rounded mb-2 shadow-sm small" draggable="true" id="w-swim">
<i class="bi bi-water me-1"></i> Natation : 2500m
</div>
</div>
</div>
<div class="col">
<div class="bg-light p-2 rounded border h-100 drop-zone" data-day="Mar">
<div class="fw-bold text-center border-bottom pb-1 mb-2 bg-dark text-white rounded-top small">Mardi</div>
<div class="rym-draggable-workout bg-warning text-dark p-2 rounded mb-2 shadow-sm small" draggable="true" id="w-vma">
<i class="bi bi-lightning me-1"></i> VMA Balayage
</div>
</div>
</div>
<div class="col">
<div class="bg-light p-2 rounded border h-100 drop-zone" data-day="Mer">
<div class="fw-bold text-center border-bottom pb-1 mb-2 bg-dark text-white rounded-top small">Mercredi</div>
<div class="rym-draggable-workout bg-info text-dark p-2 rounded mb-2 shadow-sm small" draggable="true" id="w-bike">
<i class="bi bi-bicycle me-1"></i> Vélo Tempête
</div>
</div>
</div>
<div class="col">
<div class="bg-light p-2 rounded border h-100 drop-zone" data-day="Jeu">
<div class="fw-bold text-center border-bottom pb-1 mb-2 bg-dark text-white rounded-top small">Jeudi</div>
</div>
</div>
<div class="col">
<div class="bg-light p-2 rounded border h-100 drop-zone" data-day="Ven">
<div class="fw-bold text-center border-bottom pb-1 mb-2 bg-dark text-white rounded-top small">Vendredi</div>
</div>
</div>
<div class="col">
<div class="bg-light p-2 rounded border h-100 drop-zone" data-day="Sam">
<div class="fw-bold text-center border-bottom pb-1 mb-2 bg-dark text-white rounded-top small">Samedi</div>
</div>
</div>
<div class="col">
<div class="bg-light p-2 rounded border h-100 drop-zone" data-day="Dim">
<div class="fw-bold text-center border-bottom pb-1 mb-2 bg-dark text-white rounded-top small">Dimanche</div>
</div>
</div>
</div>
</div>
<div class="card card-rym p-4 premium-zone shadow-lg mb-5">
<h5 class="fw-bold text-dark"><i class="bi bi-clock me-2"></i>Agenda Précision Premium</h5>
<p class="text-muted small mb-0">Zone réservée aux flux haute précision asynchrones.</p>
</div>
</section>
<section id="clients" class="tab-content">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Espace Expert <small class="text-muted fs-6">| Dashboard Cohorte</small></h2>
<div class="btn-group shadow-sm">
<button class="btn btn-white border btn-sm"><i class="bi bi-file-earmark-pdf"></i> PDF</button>
<button class="btn btn-white border btn-sm"><i class="bi bi-file-earmark-excel"></i> XLS</button>
</div>
</div>
<div class="row g-4">
<div class="col-md-7">
<div class="card card-rym p-0 overflow-hidden">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light"><tr><th class="p-3">Athlète</th><th>Santé</th><th>Sync API</th><th>Action</th></tr></thead>
<tbody>
<tr>
<td class="p-3"><strong>Jean Dupont</strong><br><small>Ironman Nice 2026</small></td>
<td><span class="badge bg-success">Optimal</span></td>
<td><i class="bi bi-strava text-orange"></i> <i class="bi bi-check-circle-fill text-primary"></i></td>
<td><button class="btn btn-sm btn-outline-dark">Ouvrir Dossier</button></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-5">
<div class="card card-rym p-4 border-start border-primary border-5">
<h5 class="mb-3">Bilan Anthropométrique (Cible)</h5>
<div class="row g-2 small">
<div class="col-6">Poids: <strong>74.5 kg</strong></div><div class="col-6 text-end">Âge: <strong>32</strong></div>
<hr>
<div class="col-6">Cou: 39cm</div><div class="col-6 text-end">Poitrine: 104cm</div>
<div class="col-6">Hanches: 92cm</div><div class="col-6 text-end">Taille: 80cm</div>
<div class="col-6">Cuisse (D/G): 58/58</div><div class="col-6 text-primary text-end">Mollet (D/G): 39/39</div>
</div>
<div class="mt-4 p-2 bg-light border rounded text-center x-small">
<i class="bi bi-shield-lock me-1"></i> Données Chiffrées (Souveraineté RYM)
</div>
</div>
</div>
</div>
</section>
</main>
</div> </div>
</div> </header>
<main class="container-fluid py-4 px-4">
<section class="row g-4 mb-5">
<div class="col-12">
<div class="bg-white rounded-4 p-3 border d-flex justify-content-around flex-wrap gap-4">
<div class="d-flex align-items-center gap-3">
<div class="spinner-container">
<svg width="60" height="60"><circle cx="30" cy="30" r="26" fill="none" stroke="#e2e8f0" stroke-width="4"/><circle cx="30" cy="30" r="26" fill="none" stroke="#0d9488" stroke-width="4" stroke-dasharray="163" stroke-dashoffset="80"/></svg>
<div class="spinner-value" style="color:#0d9488" id="val-charge">12h</div>
</div>
<div><small class="text-muted d-block">Entraînement</small><span class="fw-bold">Charge Hebdo</span></div>
</div>
<div class="d-flex align-items-center gap-3">
<div class="spinner-container">
<svg width="60" height="60"><circle cx="30" cy="30" r="26" fill="none" stroke="#e2e8f0" stroke-width="4"/><circle cx="30" cy="30" r="26" fill="none" stroke="#16a34a" stroke-width="4" stroke-dasharray="163" stroke-dashoffset="30"/></svg>
<div class="spinner-value text-success" id="val-recup">82%</div>
</div>
<div><small class="text-muted d-block">Récupération</small><span class="fw-bold">VRC / Sommeil</span></div>
</div>
<div class="d-flex align-items-center gap-3">
<div class="spinner-container">
<svg width="60" height="60"><circle cx="30" cy="30" r="26" fill="none" stroke="#e2e8f0" stroke-width="4"/><circle cx="30" cy="30" r="26" fill="none" stroke="#2563eb" stroke-width="4" stroke-dasharray="163" stroke-dashoffset="40"/></svg>
<div class="spinner-value text-primary" id="val-nutrition">75%</div>
</div>
<div><small class="text-muted d-block">Nutrition</small><span class="fw-bold">Macro Cibles</span></div>
</div>
<div class="d-flex align-items-center gap-3">
<div class="spinner-container">
<svg width="60" height="60"><circle cx="30" cy="30" r="26" fill="none" stroke="#e2e8f0" stroke-width="4"/><circle cx="30" cy="30" r="26" fill="none" stroke="#0284c7" stroke-width="4" stroke-dasharray="163" stroke-dashoffset="50"/></svg>
<div class="spinner-value" style="color:#0284c7" id="val-hydra">2.5L</div>
</div>
<div><small class="text-muted d-block">Hydratation</small><span class="fw-bold">Eau / Électro</span></div>
</div>
<div class="d-flex align-items-center gap-3">
<div class="spinner-container">
<svg width="60" height="60"><circle cx="30" cy="30" r="26" fill="none" stroke="#e2e8f0" stroke-width="4"/><circle cx="30" cy="30" r="26" fill="none" stroke="#f59e0b" stroke-width="4" stroke-dasharray="163" stroke-dashoffset="120"/></svg>
<div class="spinner-value text-warning" id="val-soins">Kiné</div>
</div>
<div><small class="text-muted d-block">Soins / Santé</small><span class="fw-bold">Fatigue Musc.</span></div>
</div>
<div class="d-flex align-items-center gap-3">
<div class="spinner-container">
<svg width="60" height="60"><circle cx="30" cy="30" r="26" fill="none" stroke="#e2e8f0" stroke-width="4"/><circle cx="30" cy="30" r="26" fill="none" stroke="#ea580c" stroke-width="4" stroke-dasharray="163" stroke-dashoffset="130"/></svg>
<div class="spinner-value text-danger" id="val-matos">22%</div>
</div>
<div><small class="text-muted d-block">Matériel</small><span class="fw-bold text-danger">MCO Critique</span></div>
</div>
</div>
</div>
</section>
<div class="tab-content">
<div class="tab-pane" id="view-day">
<div class="row">
<div class="col-lg-6 mx-auto">
<h2 class="fw-bold h4 mb-4">Aujourd'hui, Samedi 23 mai</h2>
<div class="day-part-card">
<span class="text-muted small fw-bold">MATIN</span>
<div class="workout swim"><i class="bi bi-water me-2"></i><strong>Natation</strong> - 1h15 Endurance (2500m)</div>
</div>
<div class="day-part-card day-part-active">
<span class="text-primary small fw-bold">APRÈS-MIDI</span>
<p class="text-muted small mt-2">Repos actif / Préparation vélo</p>
</div>
<div class="day-part-card">
<span class="text-muted small fw-bold">SOIR</span>
<div class="workout run"><i class="bi bi-speedometer2 me-2"></i><strong>Course</strong> - 45' Footing souple</div>
</div>
</div>
</div>
</div>
<div class="tab-pane active" id="view-week">
<div class="row g-4">
<div class="col-xl-3">
<div class="bg-white border rounded-4 p-4 sticky-md-top" style="top: 90px; z-index: 10;">
<h5 class="fw-bold mb-3"><i class="bi bi-plus-circle-fill text-primary me-2"></i>RYM Coach Engine</h5>
<p class="text-muted small">Créez une activité ou une tâche de maintenance et planifiez-la par Drag & Drop.</p>
<hr class="text-muted opacity-25">
<form id="rym-coach-generator" autocomplete="off">
<div class="mb-3">
<label class="form-label small fw-bold text-muted">TYPE DE COMPOSANT</label>
<select class="form-select form-select-sm" id="gen-type">
<option value="run">🏃 Course à pied</option>
<option value="bike">🚴 Vélo / Home-Trainer</option>
<option value="swim">🏊 Natation</option>
<option value="matos">🔧 Maintenance Matériel</option>
<option value="soins">🧘 Soins / Récupération</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">TITRE / DESCRIPTION</label>
<input type="text" class="form-control form-control-sm" id="gen-title" placeholder="ex: 6x800m VMA ou Purge freins" required>
</div>
<button type="submit" class="btn btn-primary btn-sm w-100 fw-bold">Générer le Badge</button>
</form>
<div class="mt-4">
<label class="form-label small fw-bold text-muted d-block mb-2">BADGE PRÊT À GLISSER :</label>
<div id="badge-production-zone" class="p-3 border border-dashed rounded-3 bg-light text-center small text-muted">
Aucun badge généré
</div>
</div>
</div>
</div>
<div class="col-xl-9">
<div class="d-flex justify-content-between align-items-end mb-3">
<h2 class="fw-bold h4 mb-0">Timeline Hebdomadaire</h2>
<span class="text-muted small">Semaine 21 — Bloc Intensité</span>
</div>
<div class="timeline-scroll">
<div class="day-card drop-zone" data-day="Lun">
<span class="d-block fw-bold">Lundi 18</span>
<small class="text-muted">Repos</small>
<div class="mt-4 text-center py-3 border rounded-3 border-dashed">😴 Assimilation</div>
</div>
<div class="day-card drop-zone" data-day="Mar">
<span class="d-block fw-bold">Mardi 19</span>
<small class="text-muted">Haute Intensité</small>
<div class="workout run rym-draggable-workout" draggable="true" id="w-vma">
<i class="bi bi-lightning-fill me-1"></i> 6x800m VMA
</div>
</div>
<div class="day-card today drop-zone" data-day="Mer">
<span class="d-block fw-bold text-primary">Mercredi 20 (Aujourd'hui)</span>
<small class="text-primary fw-bold">Bi-quotidien</small>
<div class="workout swim rym-draggable-workout" draggable="true" id="w-swim">
<i class="bi bi-water me-1"></i> Swim 2500m
</div>
<div class="workout bike rym-draggable-workout" draggable="true" id="w-bike">
<i class="bi bi-bicycle"></i> H.Trainer 1h
</div>
</div>
<div class="day-card drop-zone" data-day="Jeu">
<span class="d-block fw-bold">Jeudi 21</span>
<small class="text-muted">Récupération</small>
<div class="workout run rym-draggable-workout" draggable="true" id="w-footing">
<i class="bi bi-speedometer2 me-1"></i> Footing 40'
</div>
</div>
<div class="day-card drop-zone" data-day="Ven">
<span class="d-block fw-bold">Vendredi 22</span>
<small class="text-muted">Repos</small>
<div class="mt-4 text-center py-3 border rounded-3 border-dashed">🧘 Flexibilité</div>
</div>
<div class="day-card drop-zone" data-day="Sam">
<span class="d-block fw-bold">Samedi 23</span>
<small class="text-muted">Sortie Longue</small>
</div>
<div class="day-card drop-zone" data-day="Dim">
<span class="d-block fw-bold">Dimanche 24</span>
<small class="text-muted">Enchaînement</small>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="view-month">
<div class="row g-3">
<div class="col-md-3">
<div class="bg-white border rounded-4 p-3">
<h6 class="fw-bold">Semaine 1</h6>
<div class="progress mb-2" style="height: 6px;"><div class="progress-bar bg-primary" style="width: 70%"></div></div>
<small class="text-muted">Focus : Volume Foncier</small>
</div>
</div>
<div class="col-md-3">
<div class="bg-white border rounded-4 p-3">
<h6 class="fw-bold">Semaine 2</h6>
<div class="progress mb-2" style="height: 6px;"><div class="progress-bar bg-primary" style="width: 85%"></div></div>
<small class="text-muted">Focus : Charge Progressive</small>
</div>
</div>
<div class="col-md-3">
<div class="bg-white border rounded-4 p-3">
<h6 class="fw-bold">Semaine 3</h6>
<div class="progress mb-2" style="height: 6px;"><div class="progress-bar bg-primary" style="width: 40%"></div></div>
<small class="text-muted">Focus : Assimilation / Repos</small>
</div>
</div>
<div class="col-md-3">
<div class="bg-white border rounded-4 p-3">
<h6 class="fw-bold">Semaine 4</h6>
<div class="progress mb-2" style="height: 6px;"><div class="progress-bar bg-primary" style="width: 90%"></div></div>
<small class="text-muted">Focus : Intensité Cible</small>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="view-macro">
<div class="macro-card">
<div class="row">
<div class="col-md-8">
<h2 class="fw-bold mb-3">Saison 2026 : Road to Ironman Vitoria</h2>
<p class="opacity-75">Cycle de 24 semaines — Actuellement en Phase de Spécificité 1.</p>
<div class="d-flex gap-2 mt-4">
<span class="badge bg-primary">Prep Foncière</span>
<span class="badge bg-primary">Prep Spécifique</span>
<span class="badge bg-outline-light border text-white opacity-50">Affûtage</span>
</div>
</div>
<div class="col-md-4 text-end">
<div class="h1 fw-bold">J - 142</div>
<small class="opacity-75">Avant l'objectif A</small>
</div>
</div>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="app.js" defer></script> <script src="app.js" defer></script>
</body> </body>
</html> </html>

244
style.css
View File

@ -1,150 +1,170 @@
/* Variables de Design System RYM */ :root {
:root { --rym-bg: #f8fafc;
--rym-main: #004a99; --rym-surface: #ffffff;
--rym-dark: #121416; --rym-accent: #2563eb;
--rym-accent: #00d1b2; --rym-text: #0f172a;
--rym-premium: #ffd700; --rym-muted: #64748b;
--rym-bg: #f0f2f5; --rym-stroke: #e2e8f0;
} }
/* Base App Layout */ body {
body { background-color: var(--rym-bg);
background-color: var(--rym-bg); color: var(--rym-text);
font-family: 'Inter', sans-serif; font-family: 'Inter', -apple-system, sans-serif;
font-size: 0.9rem; letter-spacing: -0.01em;
} }
/* Navigation & Sidebar UI */ /* Nav & Tabs Switcheur du haut */
.sidebar { .nav-view-switcher {
min-height: 100vh; background: #e2e8f0;
background: var(--rym-dark); padding: 4px;
color: white; border-radius: 12px;
display: inline-flex;
} }
.sidebar-heading { .nav-view-switcher .nav-link {
font-size: 0.7rem; border-radius: 10px;
font-weight: 700; color: var(--rym-muted);
text-transform: uppercase; font-weight: 600;
color: #495057; font-size: 0.85rem;
margin-top: 1.5rem; padding: 8px 16px;
padding-bottom: 5px; border: none;
border-bottom: 1px solid #343a40; background: none;
transition: all 0.2s ease;
} }
.nav-link { .nav-view-switcher .nav-link.active {
color: #8a8d91; background: var(--rym-surface);
border-radius: 10px; color: var(--rym-text);
padding: 10px 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);
margin-bottom: 4px;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
} }
.nav-link:hover { /* Cockpit Spinners Originaux */
color: white; .spinner-container {
background: rgba(255, 255, 255, 0.05); width: 60px;
} height: 60px;
.nav-link.active {
background: var(--rym-main);
color: white;
}
/* UI Components */
.card-rym {
border: none;
border-radius: 18px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
background: white;
}
.premium-zone {
border: 2px solid var(--rym-premium);
position: relative;
}
/* Agenda Components */
.agenda-scroll {
max-height: 250px;
overflow-y: auto;
}
.hour-row {
border-bottom: 1px solid #eee;
min-height: 40px;
display: flex;
align-items: center;
}
/* Router / Navigation Tabs States */
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s forwards;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
/* --- CONFIGURATION DES SPINNERS (OPTION 2) --- */
.rym-spinner-wrapper {
position: relative; position: relative;
width: 90px;
height: 90px;
margin: 0 auto;
} }
.rym-spinner-svg { .spinner-container svg {
width: 100%;
height: 100%;
transform: rotate(-90deg); transform: rotate(-90deg);
} }
.circle-bg { .spinner-container circle {
fill: none; transition: stroke-dashoffset 0.3s ease;
stroke: #e6e6e6;
stroke-width: 2.8;
} }
.circle-stroke { .spinner-value {
fill: none;
stroke-width: 2.8;
stroke-linecap: round;
transition: stroke-dasharray 0.5s ease;
}
.rym-spinner-text {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
text-align: center; font-size: 0.75rem;
font-weight: 700;
} }
/* --- DRAG AND DROP KANBAN (OPTION 1) --- */ /* Timeline Hebdo Défilante */
.drop-zone { .timeline-scroll {
min-height: 140px; display: flex;
gap: 1.25rem;
overflow-x: auto;
padding: 10px 0 20px;
scroll-snap-type: x mandatory;
}
.day-card {
min-width: 280px;
background: var(--rym-surface);
border: 1px solid var(--rym-stroke);
border-radius: 20px;
padding: 1.5rem;
scroll-snap-align: start;
transition: background-color 0.2s ease, border-color 0.2s ease; transition: background-color 0.2s ease, border-color 0.2s ease;
} }
.day-card.today {
border-color: var(--rym-accent);
border-width: 2px;
}
/* Drag & Drop Zones visual feedback */
.drop-zone.drag-over { .drop-zone.drag-over {
background-color: #e2e8f0 !important; background-color: #f1f5f9 !important;
border: 2px dashed #0dcaf0 !important; border: 2px dashed var(--rym-accent) !important;
} }
.rym-draggable-workout { .rym-draggable-workout {
cursor: grab; cursor: grab;
transition: transform 0.1s ease, opacity 0.1s ease;
user-select: none; user-select: none;
} }
.rym-draggable-workout:active { .rym-draggable-workout:active {
cursor: grabbing; cursor: grabbing;
opacity: 0.5; opacity: 0.4;
}
/* Vue Journalière (Matin/Midi/Soir) */
.day-part-card {
border-left: 3px solid var(--rym-stroke);
padding-left: 15px;
margin-bottom: 20px;
position: relative;
}
.day-part-card::before {
content: '';
position: absolute;
left: -6px;
top: 0;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--rym-stroke);
}
.day-part-active {
border-left-color: var(--rym-accent);
}
.day-part-active::before {
background: var(--rym-accent);
}
/* Badges de Séances d'endurance */
.workout {
border-radius: 12px;
padding: 10px;
font-size: 0.9rem;
margin-top: 10px;
font-weight: 500;
}
.run { background: #fff7ed; color: #9a3412; }
.bike { background: #f0f9ff; color: #075985; }
.swim { background: #f0fdfa; color: #115e59; }
/* Carte Macro Annu */
.macro-card {
background: linear-gradient(135deg, #1e293b, #0f172a);
color: white;
border-radius: 20px;
padding: 2rem;
}
/* --- MOTEUR DE NAVIGATION SOUVERAIN (FIX DE L'AFFICHAGE) --- */
.tab-pane {
display: none !important;
opacity: 0;
}
/* Force l'affichage et l'opacité à 1 quand l'onglet est actif */
.tab-pane.active {
display: block !important;
opacity: 1 !important;
animation: rymFadeIn 0.25s ease-out forwards;
}
@keyframes rymFadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
} }