Spaces:
Running
Running
<html lang="fr"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Cocktail Fitness - Suivi</title> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js"></script> | |
<!-- SDK Firebase (v8) --> | |
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script> | |
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-auth.js"></script> | |
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-firestore.js"></script> | |
<!-- Google Fonts --> | |
<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=Oswald:wght@700&family=Roboto:wght@400;700&display=swap" rel="stylesheet"> | |
<style> | |
/* Variables & Styles Généraux (inchangés) */ | |
:root { | |
--bg-dark: #101010; --bg-card: #1e1e1e; --text-light: #f0f0f0; --text-secondary: #aaaaaa; --accent: #FFC107; --accent-dark: #E0A800; --text-on-accent: #101010; --neutral-white: #FFFFFF; --danger: #f44336; --info: #6c757d; --success: #4CAF50; --border-color: #333333; | |
--font-primary: 'Roboto', sans-serif; --font-headings: 'Oswald', sans-serif; | |
} | |
* { margin: 0; padding: 0; box-sizing: border-box; font-family: var(--font-primary); } | |
body { background-color: var(--bg-dark); color: var(--text-light); min-height: 100vh; padding-bottom: 80px; } | |
.container { width: 100%; max-width: 800px; margin: 0 auto; padding: 1rem; } | |
header { padding: 1rem 0; text-align: center; border-bottom: 1px solid var(--border-color); margin-bottom: 1rem; display: flex; flex-direction: column; align-items: center; } | |
#logo-link { display: inline-block; cursor: pointer; text-decoration: none; margin-bottom: 0.5rem; } | |
header img#app-logo { max-width: 250px; height: auto; display: block; } | |
header p { color: var(--text-secondary); font-size: 0.9rem; } | |
h1, h2, h3, h4 { font-family: var(--font-headings); font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: 0.5px; } | |
.btn { background-color: var(--accent); color: var(--text-on-accent); border: none; padding: 0.6rem 1.2rem; border-radius: 4px; cursor: pointer; font-weight: bold; font-family: var(--font-headings); text-transform: uppercase; transition: background-color 0.2s, color 0.2s; } | |
.btn:hover { background-color: var(--accent-dark); color: var(--text-on-accent); } | |
.btn:disabled { background-color: #555; color: #999; cursor: not-allowed; } | |
.btn-outline { background-color: transparent; color: var(--accent); border: 1px solid var(--accent); } | |
.btn-outline:hover { background-color: rgba(255, 193, 7, 0.1); color: var(--accent); } | |
.btn-outline:disabled { color: #555; border-color: #555; background-color: transparent; } | |
.btn-danger { background-color: var(--danger); color: white; border-color: var(--danger); } | |
.btn-danger:hover { background-color: #d32f2f; color: white; } | |
.btn-info { background-color: var(--info); color: var(--text-light); border-color: var(--info); } | |
.btn-info:hover { background-color: #5a6268; color: var(--text-light); } | |
input, select, textarea { width: 100%; padding: 0.7rem; margin-bottom: 1rem; background-color: #2a2a2a; border: 1px solid #444; border-radius: 4px; color: var(--text-light); font-size: 1rem; } | |
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(255, 193, 7, 0.3); } | |
input[type="checkbox"] { width: auto; margin-right: 0.5rem; vertical-align: middle; accent-color: var(--accent); } | |
label { display: block; margin-bottom: 0.4rem; color: var(--text-secondary); font-size: 0.9rem; font-weight: bold; } | |
.form-group { margin-bottom: 1rem; } | |
.form-row { display: flex; gap: 1rem; margin-bottom: 0.5rem; flex-wrap: wrap; } | |
.form-row > * { flex: 1; margin-bottom: 0; min-width: 100px; } | |
.card { background-color: var(--bg-card); border-radius: 8px; padding: 1.2rem; margin-bottom: 1rem; box-shadow: 0 4px 8px rgba(0,0,0,0.3); } | |
.exercise { border-left: 4px solid var(--accent); padding-left: 1rem; margin-bottom: 1.5rem; display: none; background-color: rgba(0,0,0,0.1); padding: 1rem; border-radius: 0 4px 4px 0; } | |
.exercise.active-exercise { display: block; animation: fadeIn 0.3s ease-in-out; } | |
.exercise-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; gap: 0.5rem;} | |
.exercise-header input { margin-bottom: 0; } | |
.series-container { margin-left: 0rem; margin-top: 1rem; } | |
.series { background-color: #282828; padding: 0.8rem; border-radius: 4px; margin-bottom: 0.8rem; border: 1px solid #333; } | |
.nav-bottom { position: fixed; bottom: 0; left: 0; width: 100%; background-color: #1a1a1a; display: flex; justify-content: space-around; padding: 0.7rem 0; box-shadow: 0 -3px 10px rgba(0,0,0,0.4); z-index: 10; } | |
.nav-item { text-align: center; color: var(--text-secondary); text-decoration: none; font-size: 0.85rem; transition: color 0.2s; padding: 0 0.5rem; } | |
.nav-item.active { color: var(--accent); font-weight: bold; } | |
.nav-icon { font-size: 1.5rem; margin-bottom: 0.2rem; } | |
.workout-card { border-left: 4px solid var(--accent); cursor: pointer; transition: transform 0.2s ease-out, box-shadow 0.2s ease-out; } | |
.workout-card:hover { transform: translateY(-3px); box-shadow: 0 6px 12px rgba(0,0,0,0.4); } | |
.workout-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; } | |
.workout-header h3 { color: var(--text-light); font-family: var(--font-primary); text-transform: none; letter-spacing: normal; font-weight: bold; flex-grow: 1; min-width: 0; position: static; margin-bottom: 0; } | |
.workout-header > .badge { background-color: #2a2a2a; color: var(--text-secondary); padding: 0.3rem 0.6rem; border-radius: 4px; font-size: 0.8rem; font-weight: normal; white-space: nowrap; flex-shrink: 0; margin-left: auto; position: static; } | |
.workout-details { font-size: 0.9rem; color: var(--text-secondary); margin-top: 0.8rem; } | |
.workout-details span { margin-right: 0.8rem; } | |
.workout-header h3 .badge.type-badge { background-color: var(--neutral-white); color: var(--bg-dark); padding: 0.3rem 0.6rem; border-radius: 12px; font-size: 0.8rem; font-weight: bold; white-space: nowrap; margin-left: 5px; position: static; flex-shrink: initial; margin-left: 5px; } | |
.stat-card { text-align: center; padding: 1rem; } | |
.stat-value { font-size: 2rem; color: var(--accent); font-weight: bold; font-family: var(--font-headings); margin-bottom: 0.3rem; } | |
.stat-card div:last-child { color: var(--text-secondary); font-size: 0.9rem; } | |
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; } | |
.hidden { display: none ; } | |
#app-container > div:not(.active) { display: none ; } | |
.flex-between { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; } | |
.badge { background-color: var(--accent); color: var(--text-on-accent); padding: 0.3rem 0.6rem; border-radius: 12px; font-size: 0.8rem; font-weight: bold; white-space: nowrap; margin-left: 5px; } | |
.spinner { border: 4px solid rgba(255, 255, 255, 0.1); width: 36px; height: 36px; border-radius: 50%; border-left-color: var(--accent); animation: spin 1s linear infinite; margin: 2rem auto; display: none; } | |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
.exercise-summary { margin: 0.3rem 0; padding: 0.5rem 0; border-bottom: 1px solid var(--border-color); font-size: 0.9rem; } | |
.exercise-summary:last-child { border-bottom: none; } | |
.series-summary-container .badge { font-size: 0.7rem; padding: 0.2rem 0.4rem; background-color: var(--info); color: var(--text-light); } | |
.satisfaction { display: flex; align-items: center; justify-content: center; flex-direction: column; margin-top: 1rem; } | |
input[type="range"] { accent-color: var(--accent); cursor: pointer;} | |
.satisfaction-value { font-size: 2.2rem; color: var(--accent); margin-top: 0.5rem; font-weight: bold; font-family: var(--font-headings); } | |
.exercise-navigation { display: flex; justify-content: space-between; margin-top: 1.5rem; margin-bottom: 1.5rem; align-items: center;} | |
.exercise-navigation span { color: var(--text-secondary); font-style: italic; } | |
#login-page .card { max-width: 400px; margin: 3rem auto; } | |
#login-page h2 { text-align: center; margin-bottom: 1.5rem; color: var(--accent); } | |
#login-message { text-align: center; margin-top: 1rem; font-size: 0.9rem; min-height: 1.2em; font-weight: bold; display: none; } | |
#login-message.error { color: var(--danger); } | |
#login-message.success { color: var(--success); } | |
#login-form button { margin-top: 1rem; } | |
.auth-options { margin-top: 1.5rem; font-size: 0.9rem; text-align: center; } | |
.auth-switch { margin-bottom: 0.5rem; color: var(--text-secondary); } | |
.auth-options a { color: var(--accent); cursor: pointer; text-decoration: underline; font-weight: bold; } | |
#forgot-password-link { display: block; } | |
.user-info { text-align: right; margin-bottom: 1rem; font-size: 0.9rem; color: var(--text-secondary);} | |
.user-info strong { color: var(--text-light); margin: 0 5px; } | |
.user-info button { margin-left: 0.8rem; padding: 0.3rem 0.7rem; font-size: 0.8rem; } | |
#workout-types-list li { display: flex; justify-content: space-between; align-items: center; background-color: #2a2a2a; padding: 0.7rem 1rem; margin-bottom: 0.5rem; border-radius: 4px; border-left: 4px solid var(--neutral-white); color: var(--text-light); font-weight: bold; } | |
#add-type-form { display: flex; gap: 0.5rem; align-items: flex-end;} | |
#add-type-form input { margin-bottom: 0; flex-grow: 1;} | |
#add-type-form button { flex-shrink: 0; } | |
#add-type-error { color: var(--danger); font-size: 0.9rem; margin-top: 0.5rem; font-weight: bold; display: none; } | |
#select-workout-type-page .card { text-align: center; } | |
#select-workout-type-page .btn { margin-top: 1rem; width: 80%; } | |
#select-workout-type { margin-bottom: 1rem;} | |
.type-trend-card { margin-bottom: 1.5rem; } | |
.type-trend-card h4 { color: var(--neutral-white); border-bottom: 1px solid var(--border-color); padding-bottom: 0.5rem; margin-bottom: 1rem; text-transform: uppercase; } | |
.type-trend-card ul { list-style: none; padding: 0; font-size: 0.95rem; } | |
.type-trend-card li { margin-bottom: 0.6rem; color: var(--text-light); display: flex; justify-content: space-between; border-bottom: 1px dashed #333; padding-bottom: 0.4rem;} | |
.type-trend-card li:last-child { border-bottom: none; } | |
.type-trend-card li span:first-child { color: var(--text-secondary); font-size: 0.85rem; margin-right: 1rem; } | |
.type-trend-card li strong { color: var(--accent); } | |
/* === NOUVEAUX STYLES === */ | |
/* Style page d'accueil */ | |
#accueil-page .card { | |
border: 2px solid var(--accent); /* Ajout bordure jaune */ | |
background-color: #282828; /* Fond légèrement différent pour se démarquer */ | |
} | |
#accueil-page .card p { line-height: 1.6; color: var(--text-light); } | |
#accueil-page .card ul { list-style: none; /* Retrait puces */ padding-left: 0; margin-top: 1rem; color: var(--text-light); } | |
#accueil-page .card li { margin-bottom: 0.6rem; padding-left: 1.5rem; position: relative; } | |
/* Ajout d'une puce personnalisée jaune */ | |
#accueil-page .card li::before { | |
content: '✓'; /* Ou un autre symbole comme •, → */ | |
color: var(--accent); | |
font-weight: bold; | |
position: absolute; | |
left: 0; | |
top: 0; | |
} | |
/* Style flèche guide */ | |
.guide-arrow { | |
text-align: center; | |
margin-top: 1.5rem; | |
font-size: 2.5rem; /* Taille de la flèche */ | |
color: var(--accent); | |
animation: bounce 2s infinite; /* Ajoute une petite animation */ | |
} | |
/* Animation pour la flèche */ | |
@keyframes bounce { | |
0%, 20%, 50%, 80%, 100% { transform: translateY(0); } | |
40% { transform: translateY(-10px); } | |
60% { transform: translateY(-5px); } | |
} | |
/* === FIN NOUVEAUX STYLES === */ | |
/* Media Queries */ | |
@media (max-width: 600px) { | |
.container { padding: 0.8rem; } | |
h1,h2 { font-size: 1.6rem; } | |
.form-row { flex-direction: column; gap: 0.8rem; } | |
.flex-between { flex-direction: column; align-items: stretch; gap: 0.8rem; } | |
.flex-between > div { width: 100%; display: flex; justify-content: center; margin-top: 0.5rem;} | |
.workout-header { flex-wrap: wrap; } | |
.workout-header h3 { margin-bottom: 0.3rem; } | |
.workout-header > .badge { margin-left: 0; } | |
.user-info { text-align: center; margin-bottom: 0.8rem;} | |
.user-info button { display: block; margin: 0.5rem auto 0;} | |
.exercise-navigation button { padding: 0.6rem; font-size: 0.9rem;} | |
#add-type-form { flex-direction: column; align-items: stretch;} | |
header img#app-logo { max-width: 200px; } | |
} | |
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<!-- Header avec logo cliquable --> | |
<header> | |
<a href="#" id="logo-link"> | |
<img src="input_file_0.png" alt="Cocktail Fitness Logo" id="app-logo"> | |
</a> | |
<p>Votre suivi de séances personnalisé</p> | |
</header> | |
<!-- Login Page --> | |
<div id="login-page"> | |
<div class="card"> | |
<h2 id="auth-title">Connexion</h2> | |
<form id="login-form" novalidate> | |
<div class="form-group"> <label for="auth-email">Email</label> <input type="email" id="auth-email" placeholder="[email protected]" required> </div> | |
<div class="form-group"> <label for="auth-password">Mot de passe</label> <input type="password" id="auth-password" placeholder="********" required> </div> | |
<p id="login-message" style="display: none;"></p> | |
<button type="submit" id="auth-action-btn" class="btn" style="width: 100%;">Se Connecter</button> | |
</form> | |
<div class="auth-options"> | |
<div class="auth-switch"> | |
<span id="auth-switch-text">Pas encore de compte ?</span> | |
<a id="auth-switch-link">Inscrivez-vous ici</a> | |
</div> | |
<a href="#" id="forgot-password-link">Mot de passe oublié ?</a> | |
</div> | |
</div> | |
</div> | |
<!-- Main App Content --> | |
<div id="main-app-content" style="display: none;"> | |
<div class="user-info"> | |
Connecté: <strong id="current-user-display"></strong> | |
<button id="logout-btn" class="btn btn-outline">Déconnexion</button> | |
</div> | |
<div id="app-container"> | |
<!-- Accueil Page MISE A JOUR --> | |
<div id="accueil-page"> | |
<h2>Bienvenue !</h2> | |
<div class="card"> <!-- La bordure jaune sera appliquée par le CSS --> | |
<p> | |
Bienvenue sur l'application de suivi de <strong>Cocktail Fitness</strong> ! | |
</p> | |
<p style="margin-top: 1rem;"> | |
Utilisez cette application pour enregistrer vos séances d'entraînement, | |
suivre votre progression et analyser vos performances au fil du temps. | |
</p> | |
<ul> | |
<li>Cliquez sur "Séances" pour voir vos entraînements passés ou en créer un nouveau.</li> | |
<li>Cliquez sur "Stats" pour visualiser vos statistiques globales.</li> | |
<li>Gérez vos types de séances personnalisés (Push, Pull, Legs...).</li> | |
</ul> | |
<p style="margin-top: 1.5rem; text-align: center;"> | |
Bon entraînement ! 💪 | |
</p> | |
<!-- Flèche guide ajoutée ici --> | |
<div class="guide-arrow">↓</div> | |
</div> | |
</div> | |
<!-- Séances Page --> | |
<div id="seances-page"> | |
<div class="flex-between"> | |
<h2>Mes séances</h2> | |
<div> | |
<button id="manage-types-btn" class="btn btn-outline" style="margin-right: 0.5rem;">Gérer Types</button> | |
<button id="new-workout-btn" class="btn">Nouvelle séance</button> | |
</div> | |
</div> | |
<div id="workouts-list" class="workout-list" style="margin-top: 1.5rem;"> | |
<div class="spinner hidden"></div> | |
<p id="empty-workout-message" class="hidden" style="text-align: center; margin-top: 2rem; color: var(--text-secondary);"> Aucune séance enregistrée. </p> | |
</div> | |
</div> | |
<!-- Manage Types Page --> | |
<div id="manage-types-page"> | |
<div class="flex-between"> | |
<h2>Gérer les Types</h2> | |
<button class="btn btn-outline back-to-seances-btn">Retour</button> | |
</div> | |
<div class="card"> | |
<h3>Types Existants</h3> | |
<ul id="workout-types-list" style="list-style: none; padding: 0; margin-top: 1rem;"> | |
<li class="hidden" id="empty-types-message" style="color: var(--text-secondary); background: none; border: none; padding-left: 0;">Aucun type défini.</li> | |
</ul> | |
<div class="spinner hidden" id="types-spinner"></div> | |
</div> | |
<div class="card"> | |
<h3>Ajouter un Type</h3> | |
<form id="add-type-form" novalidate> | |
<input type="text" id="new-type-name" placeholder="Nom (ex: Push, Pull, Legs)" required> | |
<button type="submit" class="btn">Ajouter</button> | |
</form> | |
<p id="add-type-error" style="display: none;"></p> | |
</div> | |
</div> | |
<!-- Select Workout Type Page --> | |
<div id="select-workout-type-page"> | |
<div class="flex-between"> | |
<h2>Démarrer Séance</h2> | |
<button class="btn btn-outline back-to-seances-btn">Annuler</button> | |
</div> | |
<div class="card"> | |
<h3>Séance Structurée</h3> | |
<select id="select-workout-type"> | |
<option value="">-- Sélectionner Type --</option> | |
</select> | |
<button id="start-structured-workout-btn" class="btn" disabled>Démarrer</button> | |
</div> | |
<div class="card"> | |
<h3>Séance Libre</h3> | |
<button id="start-free-workout-btn" class="btn btn-info">Démarrer Libre</button> | |
</div> | |
</div> | |
<!-- New Workout Page --> | |
<div id="new-workout-page"> | |
<div class="flex-between"> | |
<h2>Nouvelle séance <span id="workout-type-indicator" class="badge type-badge hidden"></span> <span id="current-exercise-indicator" style="font-size: 0.9rem; color: var(--text-secondary); text-transform: none; font-family: var(--font-primary);"></span></h2> | |
<div> | |
<button id="cancel-new-workout-btn" class="btn btn-outline" style="margin-right: 0.5rem;">Annuler</button> | |
<button id="save-workout-btn" class="btn">Enregistrer</button> | |
</div> | |
</div> | |
<div class="card"> | |
<div class="form-group"> <label for="workout-name">Nom Séance (optionnel)</label> <input type="text" id="workout-name" placeholder="Ex: Séance Haut du Corps..."> </div> | |
<div class="form-row"> | |
<div class="form-group"> <label for="workout-date">Date</label> <input type="date" id="workout-date"> </div> | |
<div class="form-group"> <label for="workout-duration">Durée (min)</label> <input type="number" id="workout-duration" min="1" placeholder="60"> </div> | |
</div> | |
</div> | |
<h3 style="margin: 1.5rem 0;">Exercices</h3> | |
<div class="exercise-navigation card"> | |
<button id="prev-exercise-btn" class="btn btn-outline">< Précédent</button> | |
<span style="align-self: center; color: var(--text-secondary);">Exercice Actif</span> | |
<button id="next-exercise-btn" class="btn btn-outline">Suivant ></button> | |
</div> | |
<div id="exercises-container"> <!-- Exercices injected here --> </div> | |
<button id="add-exercise-btn" class="btn btn-outline" style="width: 100%; margin-top: 1rem;">+ Ajouter Exercice</button> | |
<div class="card" style="margin-top: 2rem;"> | |
<div class="form-group"> | |
<label for="satisfaction">Satisfaction (1-100%)</label> | |
<input type="range" id="satisfaction" min="1" max="100" value="75"> | |
<div class="satisfaction"> <div class="satisfaction-value">75%</div> </div> | |
</div> | |
</div> | |
</div> | |
<!-- Workout Details Page --> | |
<div id="workout-details-page"> | |
<div class="flex-between"> | |
<h2>Détail Séance <span id="detail-workout-type" class="badge type-badge hidden"></span></h2> | |
<button class="btn btn-outline back-to-seances-btn">Retour</button> | |
</div> | |
<div class="card"> | |
<div class="workout-details-info"> | |
<p><strong>Nom:</strong> <span id="detail-workout-name" style="color: var(--text-light); font-weight: normal;"></span></p> | |
<div class="form-row" style="margin-top: 0.5rem;"> | |
<p><strong>Date:</strong> <span id="detail-date" style="color: var(--text-light); font-weight: normal;"></span></p> | |
<p><strong>Durée:</strong> <span id="detail-duration" style="color: var(--text-light); font-weight: normal;"></span> min</p> | |
</div> | |
</div> | |
</div> | |
<div class="stats-grid" style="margin-top: 1.5rem;"> | |
<div class="card stat-card"> <div class="stat-value" id="detail-tonnage">0</div> <div>Tonnage (kg)</div> </div> | |
<div class="card stat-card"> <div class="stat-value" id="detail-satisfaction">0%</div> <div>Satisfaction</div> </div> | |
<div class="card stat-card"> <div class="stat-value" id="detail-exercises-count">0</div> <div>Exercices</div> </div> | |
</div> | |
<h3 style="margin: 1.5rem 0 1rem;">Exercices Réalisés</h3> | |
<div id="detail-exercises-container"> <!-- Details injected here --> </div> | |
<button id="delete-workout-btn" class="btn btn-danger" style="width: 100%; margin-top: 2rem;">Supprimer Séance</button> | |
</div> | |
<!-- Stats Page --> | |
<div id="stats-page"> | |
<h2>Statistiques Générales</h2> | |
<div class="stats-grid"> | |
<div class="card stat-card"> <div class="stat-value" id="stats-workout-count">0</div> <div>Séances Totales</div> </div> | |
<div class="card stat-card"> <div class="stat-value" id="stats-avg-tonnage">0</div> <div>Tonnage Moyen / Séance</div> </div> | |
<div class="card stat-card"> <div class="stat-value" id="stats-avg-satisfaction">0%</div> <div>Satisfaction Moyenne</div> </div> | |
</div> | |
<h3 style="margin: 2rem 0 1rem;">Tendances par Type</h3> | |
<div id="type-trends-container"> | |
<p id="no-trends-message" style="text-align: center; color: var(--text-secondary);" class="hidden"> Pas assez de données pour afficher les tendances par type. </p> | |
<div class="spinner hidden" id="trends-spinner"></div> | |
<!-- Trends injected here --> | |
</div> | |
</div> | |
</div> <!-- Fin #app-container --> | |
<!-- Bottom Navigation --> | |
<nav class="nav-bottom"> | |
<a href="#" class="nav-item" data-page="accueil-page"> <div class="nav-icon">🏠</div> <div>Accueil</div> </a> | |
<a href="#" class="nav-item" data-page="seances-page"> <div class="nav-icon">📋</div> <div>Séances</div> </a> | |
<a href="#" class="nav-item" data-page="stats-page"> <div class="nav-icon">📊</div> <div>Stats</div> </a> | |
</nav> | |
</div> <!-- Fin #main-app-content --> | |
</div> <!-- Fin .container --> | |
<!-- ================================================== --> | |
<!-- ================= JAVASCRIPT =================== --> | |
<!-- ================================================== --> | |
<script> | |
// Firebase Init | |
const firebaseConfig = { apiKey: "AIzaSyAkWvrRyXgrC7zbTtoh_GppsHMrz2rF7WM", authDomain: "lifttrackapp.firebaseapp.com", projectId: "lifttrackapp", storageBucket: "lifttrackapp.appspot.com", messagingSenderId: "594426771796", appId: "1:594426771796:web:789bef037ca0016c54b0c1", measurementId: "G-MXLFK0H160" }; | |
let app, auth, db; | |
try { app = firebase.initializeApp(firebaseConfig); auth = firebase.auth(); db = firebase.firestore(); console.log("Firebase Initialisé !"); } | |
catch (e) { console.error("Erreur Init Firebase:", e); alert("Erreur critique: Impossible d'initialiser Firebase."); document.body.innerHTML = '<div style="color: red; padding: 20px;">Erreur critique: Impossible d\'initialiser Firebase. Vérifiez la configuration et la connexion.</div>'; } | |
// --- Variables d'État --- | |
let workouts = []; let currentWorkoutId = null; let currentExerciseIndex = 0; let workoutExercisesForm = []; let currentFirebaseUser = null; let userWorkoutTypes = []; let selectedWorkoutTypeName = null; let isLoginMode = true; | |
// --- Éléments DOM --- | |
const loginPage = document.getElementById('login-page'); | |
const mainAppContent = document.getElementById('main-app-content'); | |
const authEmailInput = document.getElementById('auth-email'); | |
const authPasswordInput = document.getElementById('auth-password'); | |
const loginForm = document.getElementById('login-form'); | |
const authActionButton = document.getElementById('auth-action-btn'); | |
const authSwitchLink = document.getElementById('auth-switch-link'); | |
const forgotPasswordLink = document.getElementById('forgot-password-link'); | |
const logoLink = document.getElementById('logo-link'); | |
const authTitle = document.getElementById('auth-title'); | |
const authSwitchText = document.getElementById('auth-switch-text'); | |
const loginMessage = document.getElementById('login-message'); | |
const currentUserDisplay = document.getElementById('current-user-display'); | |
const logoutBtn = document.getElementById('logout-btn'); | |
const spinner = document.querySelector('#workouts-list .spinner'); | |
const appContainer = document.getElementById('app-container'); | |
const navItems = document.querySelectorAll('.nav-item'); | |
const newWorkoutBtn = document.getElementById('new-workout-btn'); | |
const saveWorkoutBtn = document.getElementById('save-workout-btn'); | |
const cancelNewWorkoutBtn = document.getElementById('cancel-new-workout-btn'); | |
const addExerciseBtn = document.getElementById('add-exercise-btn'); | |
const exercisesContainer = document.getElementById('exercises-container'); | |
const workoutsList = document.getElementById('workouts-list'); | |
const backToSeancesBtns = document.querySelectorAll('.back-to-seances-btn'); | |
const deleteWorkoutBtn = document.getElementById('delete-workout-btn'); | |
const satisfactionRange = document.getElementById('satisfaction'); | |
const satisfactionValue = document.querySelector('.satisfaction-value'); | |
const emptyWorkoutMessage = document.getElementById('empty-workout-message'); | |
const workoutDateInput = document.getElementById('workout-date'); | |
const prevExerciseBtn = document.getElementById('prev-exercise-btn'); | |
const nextExerciseBtn = document.getElementById('next-exercise-btn'); | |
const currentExerciseIndicator = document.getElementById('current-exercise-indicator'); | |
const workoutTypeIndicator = document.getElementById('workout-type-indicator'); | |
const detailWorkoutType = document.getElementById('detail-workout-type'); | |
const manageTypesBtn = document.getElementById('manage-types-btn'); | |
const manageTypesPage = document.getElementById('manage-types-page'); | |
const workoutTypesList = document.getElementById('workout-types-list'); | |
const emptyTypesMessage = document.getElementById('empty-types-message'); | |
const typesSpinner = document.getElementById('types-spinner'); | |
const addTypeForm = document.getElementById('add-type-form'); | |
const newTypeNameInput = document.getElementById('new-type-name'); | |
const addTypeError = document.getElementById('add-type-error'); | |
const selectWorkoutTypePage = document.getElementById('select-workout-type-page'); | |
const selectWorkoutTypeDropdown = document.getElementById('select-workout-type'); | |
const startStructuredWorkoutBtn = document.getElementById('start-structured-workout-btn'); | |
const startFreeWorkoutBtn = document.getElementById('start-free-workout-btn'); | |
const statsWorkoutCountEl = document.getElementById('stats-workout-count'); | |
const statsAvgTonnageEl = document.getElementById('stats-avg-tonnage'); | |
const statsAvgSatisfactionEl = document.getElementById('stats-avg-satisfaction'); | |
const typeTrendsContainer = document.getElementById('type-trends-container'); | |
const noTrendsMessage = document.getElementById('no-trends-message'); | |
const trendsSpinner = document.getElementById('trends-spinner'); | |
const accueilPage = document.getElementById('accueil-page'); | |
const seancesPage = document.getElementById('seances-page'); | |
// =========================================== | |
// --- DÉFINITION DES FONCTIONS --- | |
// =========================================== | |
// (Les définitions des fonctions JavaScript sont ici - identiques à la version précédente, sauf mention contraire) | |
function showMessage(message, isError = true) { if (loginMessage) { loginMessage.textContent = message; loginMessage.className = isError ? 'error' : 'success'; loginMessage.style.display = 'block'; } } | |
function clearMessage() { if (loginMessage) { loginMessage.textContent = ''; loginMessage.style.display = 'none'; } } | |
function initAuthListener() { if (!auth) return; auth.onAuthStateChanged(user => { console.log("Auth state changed:", user ? user.uid : 'null'); currentFirebaseUser = user; if (user) { showApp(); } else { workouts = []; userWorkoutTypes = []; showLoginPage(); } }); } | |
function showLoginPage() { if(loginPage) loginPage.style.display = 'block'; if(mainAppContent) mainAppContent.style.display = 'none'; isLoginMode = true; authSwitchMode(); } | |
function showApp() { if (!currentFirebaseUser || !mainAppContent) { showLoginPage(); return; } console.log("Affichage App for user:", currentFirebaseUser.email); if(loginPage) loginPage.style.display = 'none'; mainAppContent.style.display = 'block'; if(currentUserDisplay) currentUserDisplay.textContent = currentFirebaseUser.email; setTodayDate(); loadWorkouts(); loadWorkoutTypes(); showPage('accueil-page'); } // Démarre sur accueil | |
function handleAuthAction(event) { if (!auth || !authEmailInput || !authPasswordInput || !authActionButton) return; event.preventDefault(); const email = authEmailInput.value; const password = authPasswordInput.value; clearMessage(); if (!email || !password) { showMessage('Email et Mot de passe requis.'); return; } authActionButton.disabled = true; authActionButton.textContent = 'Chargement...'; if (isLoginMode) { auth.signInWithEmailAndPassword(email, password).catch(handleAuthError).finally(() => { authActionButton.disabled = false; authActionButton.textContent = 'Se Connecter'; }); } else { auth.createUserWithEmailAndPassword(email, password).catch(handleAuthError).finally(() => { authActionButton.disabled = false; authActionButton.textContent = 'S\'inscrire'; }); } } // Ne pas switcher après inscription réussie | |
function handlePasswordReset(event) { event.preventDefault(); if (!auth) { alert("Service d'authentification non disponible."); return; } clearMessage(); const email = prompt("Veuillez entrer votre adresse email pour recevoir le lien de réinitialisation :"); if (!email) { showMessage("Saisie de l'email annulée.", true); return; } console.log("Tentative d'envoi de l'email de réinitialisation à :", email); auth.sendPasswordResetEmail(email).then(() => { console.log("Email de réinitialisation envoyé."); showMessage("Si un compte existe pour " + email + ", un email de réinitialisation a été envoyé.", false); }).catch((error) => { console.error("Erreur sendPasswordResetEmail:", error); handleAuthError(error); }); } | |
function handleLogout() { if (!auth) return; console.log("Déconnexion..."); auth.signOut().catch(error => { console.error("Logout Error:", error); alert("Erreur lors de la déconnexion."); }); } | |
function authSwitchMode() { isLoginMode = !isLoginMode; clearMessage(); if (authEmailInput) authEmailInput.value = ''; if (authPasswordInput) authPasswordInput.value = ''; if (authTitle) authTitle.textContent = isLoginMode ? 'Connexion' : 'Inscription'; if (authActionButton) authActionButton.textContent = isLoginMode ? 'Se Connecter' : 'S\'inscrire'; if (authSwitchText) authSwitchText.textContent = isLoginMode ? 'Pas encore de compte ?' : 'Déjà un compte ?'; if (authSwitchLink) authSwitchLink.textContent = isLoginMode ? 'Inscrivez-vous ici' : 'Connectez-vous ici'; if (authEmailInput) authEmailInput.focus(); } | |
function handleAuthError(error) { console.error("Erreur Auth Firebase:", error.code, error.message); showMessage(getAuthErrorMessage(error), true); } | |
function getAuthErrorMessage(error) { switch (error.code) { case 'auth/invalid-email': return 'Format d\'email invalide.'; case 'auth/user-disabled': return 'Ce compte utilisateur a été désactivé.'; case 'auth/user-not-found': return 'Aucun utilisateur trouvé avec cet email.'; case 'auth/wrong-password': return 'Mot de passe incorrect.'; case 'auth/email-already-in-use': return 'Cet email est déjà utilisé.'; case 'auth/weak-password': return 'Le mot de passe doit faire au moins 6 caractères.'; case 'auth/operation-not-allowed': return 'Méthode de connexion non activée.'; case 'auth/missing-password': return 'Mot de passe manquant.'; case 'auth/missing-email': return 'Adresse email manquante.'; default: console.error("Unhandled Auth Error Code:", error.code); return `Erreur (${error.code}). Veuillez réessayer.`; } } | |
function loadWorkoutTypes() { /* ... inchangé ... */ if (!currentFirebaseUser || !db || !typesSpinner) return; const userId = currentFirebaseUser.uid; console.log("Chargement types pour", userId); userWorkoutTypes = []; typesSpinner.classList.remove('hidden'); db.collection('workoutTypes').where('userId', '==', userId).orderBy('name').get().then(snapshot => { userWorkoutTypes = []; snapshot.forEach(doc => { userWorkoutTypes.push({ id: doc.id, ...doc.data() }); }); console.log("Types chargés:", userWorkoutTypes); renderWorkoutTypesList(); populateWorkoutTypeSelector(); }).catch(handleFirestoreError).finally(() => { typesSpinner.classList.add('hidden'); }); } | |
function renderWorkoutTypesList() { /* ... inchangé ... */ if (!workoutTypesList || !emptyTypesMessage) return; workoutTypesList.innerHTML = ''; if (userWorkoutTypes.length === 0) { emptyTypesMessage.classList.remove('hidden'); } else { emptyTypesMessage.classList.add('hidden'); userWorkoutTypes.forEach(type => { const li = document.createElement('li'); li.textContent = type.name; workoutTypesList.appendChild(li); }); } } | |
function handleAddWorkoutType(event) { /* ... inchangé ... */ event.preventDefault(); if (!currentFirebaseUser || !newTypeNameInput || !db || !addTypeError || !addTypeForm) return; const typeName = newTypeNameInput.value.trim(); addTypeError.textContent = ''; addTypeError.style.display = 'none'; if (!typeName) { addTypeError.textContent = "Le nom du type ne peut pas être vide."; addTypeError.style.display = 'block'; return; } if (userWorkoutTypes.some(t => t.name.toLowerCase() === typeName.toLowerCase())) { addTypeError.textContent = "Ce type de séance existe déjà."; addTypeError.style.display = 'block'; return; } console.log("Ajout type:", typeName); const typeData = { userId: currentFirebaseUser.uid, name: typeName, createdAt: firebase.firestore.FieldValue.serverTimestamp() }; const addBtn = addTypeForm.querySelector('button'); if (addBtn) { addBtn.disabled = true; addBtn.textContent = '...'; } db.collection('workoutTypes').add(typeData).then(() => { console.log("Type ajouté avec succès"); newTypeNameInput.value = ''; loadWorkoutTypes(); }).catch(handleFirestoreError).finally(() => { if (addBtn) { addBtn.disabled = false; addBtn.textContent = 'Ajouter'; } }); } | |
function populateWorkoutTypeSelector() { /* ... inchangé ... */ if (!selectWorkoutTypeDropdown) return; selectWorkoutTypeDropdown.innerHTML = '<option value="">-- Sélectionner Type --</option>'; userWorkoutTypes.forEach(type => { const option = document.createElement('option'); option.value = type.name; option.textContent = type.name; selectWorkoutTypeDropdown.appendChild(option); }); updateStartStructuredBtnState(); } | |
function updateStartStructuredBtnState() { /* ... inchangé ... */ if (!selectWorkoutTypeDropdown || !startStructuredWorkoutBtn) return; startStructuredWorkoutBtn.disabled = !selectWorkoutTypeDropdown.value; } | |
function loadWorkouts() { /* ... inchangé ... */ if (!currentFirebaseUser || !db || !spinner || !emptyWorkoutMessage || !workoutsList) { console.warn("Cannot load workouts"); if(workoutsList) workoutsList.innerHTML = ''; if(emptyWorkoutMessage) emptyWorkoutMessage.classList.remove('hidden'); workouts = []; updateStats(); return; } const userId = currentFirebaseUser.uid; console.log(`Chargement séances pour ${userId}...`); spinner.classList.remove('hidden'); emptyWorkoutMessage.classList.add('hidden'); workoutsList.innerHTML = ''; db.collection('workouts').where('userId', '==', userId).orderBy('date', 'desc').get().then((querySnapshot) => { workouts = []; console.log(`${querySnapshot.size} séances trouvées.`); querySnapshot.forEach((doc) => { const workoutData = doc.data(); workoutData.id = doc.id; workouts.push(workoutData); }); renderWorkoutsList(); updateStats(); }).catch(handleFirestoreError).finally(() => { spinner.classList.add('hidden'); if (workouts.length === 0) { emptyWorkoutMessage.classList.remove('hidden'); } }); } | |
function saveWorkout() { /* ... inchangé - mais redirection vers seances-page ... */ if (!currentFirebaseUser || !db || !exercisesContainer || !saveWorkoutBtn) { alert("Erreur: Impossible de sauvegarder."); return; } const userId = currentFirebaseUser.uid; workoutExercisesForm = Array.from(exercisesContainer.querySelectorAll('.exercise')); const workoutNameInput = document.getElementById('workout-name'); const workoutName = workoutNameInput ? workoutNameInput.value.trim() : ''; const workoutDate = workoutDateInput ? workoutDateInput.value : ''; const workoutDurationInput = document.getElementById('workout-duration'); const workoutDuration = workoutDurationInput ? parseInt(workoutDurationInput.value) || 0 : 0; const satisfaction = satisfactionRange ? parseInt(satisfactionRange.value) : 75; if (!selectedWorkoutTypeName) { alert("Erreur interne: Type de séance manquant."); showPage('seances-page'); return; } if (!workoutDate) { alert("Veuillez sélectionner une date."); if(workoutDateInput) workoutDateInput.focus(); return; } if (workoutDuration <= 0) { alert("Veuillez entrer une durée valide."); if(workoutDurationInput) workoutDurationInput.focus(); return; } if (workoutExercisesForm.length === 0) { alert("Veuillez ajouter au moins un exercice."); return; } const exercises = []; let validationError = null; workoutExercisesForm.forEach((exerciseEl, index) => { if (validationError) return; const nameInput = exerciseEl.querySelector('.exercise-name'); const exerciseName = nameInput ? nameInput.value.trim() : ''; const unilateralCheckbox = exerciseEl.querySelector('.unilateral-checkbox'); const isUnilateral = unilateralCheckbox ? unilateralCheckbox.checked : false; if (!exerciseName) { validationError = `Nommez l'exercice #${index + 1}.`; if(nameInput) nameInput.focus(); return; } const series = []; const seriesElements = exerciseEl.querySelectorAll('.series'); if (seriesElements.length === 0) { validationError = `L'exercice "${exerciseName}" n'a pas de série.`; return; } seriesElements.forEach((seriesEl, sIndex) => { if (validationError) return; const repsInput = seriesEl.querySelector('.reps'); const weightInput = seriesEl.querySelector('.weight'); const degressiveCheckbox = seriesEl.querySelector('.degressive-checkbox'); const reps = repsInput ? parseInt(repsInput.value) || 0 : 0; const weight = weightInput ? parseFloat(weightInput.value) || 0 : 0; const isDegressive = degressiveCheckbox ? degressiveCheckbox.checked : false; if (reps <= 0) { validationError = `Reps invalides pour série ${sIndex + 1} (${exerciseName}).`; if(repsInput) repsInput.focus(); return; } if (weight < 0) { validationError = `Charge invalide pour série ${sIndex + 1} (${exerciseName}).`; if(weightInput) weightInput.focus(); return; } series.push({ reps, weight, isDegressive }); }); if (!validationError) { exercises.push({ name: exerciseName, isUnilateral, series }); } }); if (validationError) { alert(validationError); return; } let totalTonnage = 0; exercises.forEach(ex => { (ex.series || []).forEach(s => { totalTonnage += (s.reps || 0) * (s.weight || 0) * (ex.isUnilateral ? 2 : 1); }); }); const finalWorkoutName = workoutName || selectedWorkoutTypeName; const workoutData = { userId: userId, workoutTypeName: selectedWorkoutTypeName, name: finalWorkoutName, date: workoutDate, duration: workoutDuration, exercises: exercises, totalTonnage: totalTonnage, satisfaction: satisfaction, lastUpdated: firebase.firestore.FieldValue.serverTimestamp() }; let savePromise; if (currentWorkoutId) { workoutData.createdAt = workouts.find(w => w.id === currentWorkoutId)?.createdAt || firebase.firestore.FieldValue.serverTimestamp(); savePromise = db.collection('workouts').doc(currentWorkoutId).set(workoutData); } else { workoutData.createdAt = firebase.firestore.FieldValue.serverTimestamp(); savePromise = db.collection('workouts').add(workoutData); } saveWorkoutBtn.disabled = true; saveWorkoutBtn.textContent = 'Sauvegarde...'; savePromise.then((docRefOrVoid) => { const savedId = currentWorkoutId || (docRefOrVoid ? docRefOrVoid.id : 'unknown'); console.log("Séance sauvegardée:", savedId); currentWorkoutId = null; try { if (typeof confetti === 'function') confetti({ particleCount: 150, spread: 90, origin: { y: 0.6 } }); } catch(e) { console.warn("Confetti error:", e)} showPage('seances-page'); loadWorkouts(); }).catch(handleFirestoreError).finally(() => { saveWorkoutBtn.disabled = false; saveWorkoutBtn.textContent = 'Enregistrer Séance'; }); } | |
function deleteWorkout() { /* ... inchangé - mais redirection vers seances-page ... */ if (!currentFirebaseUser || !currentWorkoutId || !db || !deleteWorkoutBtn) return; const workoutToDelete = workouts.find(w => w && w.id === currentWorkoutId); if (!workoutToDelete) { console.error("Workout to delete not found locally."); return; } const confirmMsg = `Supprimer la séance "${workoutToDelete.name || 'Sans nom'}" du ${new Date(workoutToDelete.date).toLocaleDateString('fr-FR')} ?`; if (!confirm(confirmMsg)) return; console.log("Suppression séance:", currentWorkoutId); deleteWorkoutBtn.disabled = true; deleteWorkoutBtn.textContent = 'Suppression...'; db.collection('workouts').doc(currentWorkoutId).delete().then(() => { console.log("Séance supprimée avec succès:", currentWorkoutId); currentWorkoutId = null; showPage('seances-page'); loadWorkouts(); }).catch(handleFirestoreError).finally(() => { deleteWorkoutBtn.disabled = false; deleteWorkoutBtn.textContent = 'Supprimer Séance'; }); } | |
function setTodayDate() { /* ... inchangé ... */ if (workoutDateInput) { try { const today = new Date(); const offset = today.getTimezoneOffset(); const localDate = new Date(today.getTime() - (offset*60*1000)); workoutDateInput.value = localDate.toISOString().split('T')[0]; } catch (e) { console.error("Erreur setTodayDate:", e); try { workoutDateInput.value = new Date().toISOString().split('T')[0]; } catch {} } } } | |
function showPage(pageId) { // **MODIFIÉ** pour gérer la nav d'accueil | |
if (!appContainer) { console.error("App container not found!"); return; } | |
console.log(`Affichage page: ${pageId}`); | |
const pages = appContainer.querySelectorAll(':scope > div[id$="-page"]'); | |
pages.forEach(page => page.classList.remove('active')); | |
const pageToShow = document.getElementById(pageId); | |
if (pageToShow) { | |
pageToShow.classList.add('active'); | |
window.scrollTo(0, 0); | |
// Actions spécifiques | |
if (pageId === 'new-workout-page') { if (!selectedWorkoutTypeName) { alert("Sélectionnez un type."); showPage('select-workout-type-page'); return; } renderActiveExerciseForm(); updateWorkoutTypeIndicator(); } | |
else if (pageId === 'manage-types-page') loadWorkoutTypes(); | |
else if (pageId === 'select-workout-type-page') populateWorkoutTypeSelector(); | |
else if (pageId === 'stats-page') updateStats(); | |
else if (pageId === 'workout-details-page' && !currentWorkoutId) { alert("Séance non spécifiée."); showPage('seances-page'); return; } | |
} else { | |
console.error(`Page ID "${pageId}" non trouvée. Affichage accueil.`); | |
if (accueilPage) accueilPage.classList.add('active'); // Fallback sur accueil | |
pageId = 'accueil-page'; | |
} | |
// Mise à jour Nav Bottom | |
navItems.forEach(item => item.classList.remove('active')); | |
// Active le bouton correspondant directement à la pageId si trouvé | |
const activeNavItem = document.querySelector(`.nav-item[data-page="${pageId}"]`); | |
if (activeNavItem) { | |
activeNavItem.classList.add('active'); | |
} else { | |
// Si pas de bouton direct (ex: page détails), active "Séances" par défaut | |
const seancesNavItem = document.querySelector('.nav-item[data-page="seances-page"]'); | |
if(seancesNavItem) seancesNavItem.classList.add('active'); | |
} | |
} | |
function handleAddNewExercise() { /* ... inchangé ... */ addExercise(true); } | |
function addExercise(navigateToNew = false) { /* ... inchangé ... */ if (!currentFirebaseUser || !exercisesContainer) { console.error("Cannot add exercise"); return; } const exerciseId = `exercise-${Date.now()}`; const exerciseDiv = document.createElement('div'); exerciseDiv.className = 'card exercise'; exerciseDiv.setAttribute('data-exercise-id', exerciseId); exerciseDiv.innerHTML = `<div class="exercise-header"> <input type="text" placeholder="Nom Exercice" class="exercise-name" required> <button class="btn btn-danger remove-exercise" style="padding: 0.3rem 0.6rem; flex-shrink: 0;" aria-label="Supprimer exercice">×</button> </div> <div class="form-group" style="margin: 0.5rem 0;"> <label style="display: inline-flex; align-items: center; color: var(--text-secondary); font-size: 0.9rem; font-weight: normal;"> <input type="checkbox" class="unilateral-checkbox"> Unilatéral ? </label> </div> <div class="series-container"></div> <button class="btn btn-outline add-series" style="width: 100%; margin-top: 1rem; padding: 0.4rem;">+ Ajouter Série</button>`; exercisesContainer.appendChild(exerciseDiv); workoutExercisesForm = Array.from(exercisesContainer.querySelectorAll('.exercise')); const removeBtn = exerciseDiv.querySelector('.remove-exercise'); removeBtn.addEventListener('click', () => { const indexToRemove = workoutExercisesForm.findIndex(el => el === exerciseDiv); if (indexToRemove > -1) { if (confirm("Supprimer cet exercice et toutes ses séries ?")) { exerciseDiv.remove(); workoutExercisesForm.splice(indexToRemove, 1); if (currentExerciseIndex >= indexToRemove && currentExerciseIndex > 0) { currentExerciseIndex--; } if (currentExerciseIndex >= workoutExercisesForm.length) { currentExerciseIndex = Math.max(0, workoutExercisesForm.length - 1); } renderActiveExerciseForm(); } } }); const addSeriesBtn = exerciseDiv.querySelector('.add-series'); const seriesContainer = exerciseDiv.querySelector('.series-container'); addSeriesBtn.addEventListener('click', () => addSeries(seriesContainer)); addSeries(seriesContainer); if (navigateToNew) { currentExerciseIndex = workoutExercisesForm.length - 1; renderActiveExerciseForm(); const nameInput = exerciseDiv.querySelector('.exercise-name'); if (nameInput) nameInput.focus(); } else { updateExerciseNavButtons(); } } | |
function addSeries(container) { /* ... inchangé ... */ if (!currentFirebaseUser || !container) { console.error("Cannot add series"); return; } const seriesId = `series-${Date.now()}`; const seriesDiv = document.createElement('div'); seriesDiv.className = 'series'; seriesDiv.setAttribute('data-series-id', seriesId); seriesDiv.innerHTML = `<div class="form-row" style="align-items: flex-end; gap: 0.8rem;"> <div class="form-group" style="flex: 1.2;"> <label style="font-weight: normal; font-size: 0.8rem;">Reps</label> <input type="number" class="reps" min="1" placeholder="10" style="padding: 0.5rem;" required> </div> <div class="form-group" style="flex: 1.2;"> <label style="font-weight: normal; font-size: 0.8rem;">Charge (kg)</label> <input type="number" class="weight" min="0" step="any" placeholder="20" style="padding: 0.5rem;" required> </div> <div class="form-group" style="flex: 1; display: flex; align-items: center; padding-bottom: 0.8rem; min-width: 100px;"> <label style="display: inline-flex; align-items: center; color: var(--text-secondary); font-size: 0.8rem; margin-bottom: 0; white-space: nowrap; font-weight: normal;"> <input type="checkbox" class="degressive-checkbox" style="margin-right: 0.3rem;"> Dégressive </label> </div> <button class="btn btn-danger remove-series" style="padding: 0.3rem 0.6rem; margin-bottom: 0.8rem; flex-basis: 30px; flex-grow: 0; align-self: center;" aria-label="Supprimer série">×</button> </div>`; container.appendChild(seriesDiv); const removeBtn = seriesDiv.querySelector('.remove-series'); removeBtn.addEventListener('click', () => { seriesDiv.remove(); }); } | |
function navigateExerciseForm(direction) { /* ... inchangé ... */ if (!exercisesContainer) return; workoutExercisesForm = Array.from(exercisesContainer.querySelectorAll('.exercise')); const newIndex = currentExerciseIndex + direction; if (newIndex >= 0 && newIndex < workoutExercisesForm.length) { currentExerciseIndex = newIndex; renderActiveExerciseForm(); } } | |
function renderActiveExerciseForm() { /* ... inchangé ... */ if (!exercisesContainer) return; workoutExercisesForm = Array.from(exercisesContainer.querySelectorAll('.exercise')); workoutExercisesForm.forEach((ex, index) => { if (index === currentExerciseIndex) { ex.classList.add('active-exercise'); ex.style.display = 'block'; } else { ex.classList.remove('active-exercise'); ex.style.display = 'none'; } }); updateExerciseNavButtons(); } | |
function updateExerciseNavButtons() { /* ... inchangé ... */ if (!currentExerciseIndicator || !prevExerciseBtn || !nextExerciseBtn) { console.warn("Exercise nav elements missing"); return; } const totalExercises = workoutExercisesForm.length; if (totalExercises === 0) { currentExerciseIndicator.textContent = "(Aucun exercice)"; prevExerciseBtn.disabled = true; nextExerciseBtn.disabled = true; } else { currentExerciseIndicator.textContent = `(${currentExerciseIndex + 1} / ${totalExercises})`; prevExerciseBtn.disabled = (currentExerciseIndex === 0); nextExerciseBtn.disabled = (currentExerciseIndex === totalExercises - 1); } } | |
function clearNewWorkoutForm(typeName = "Libre") { /* ... inchangé ... */ console.log("Nettoyage formulaire pour type:", typeName); if (!currentFirebaseUser) { console.error("Cannot clear form: No user"); return; } selectedWorkoutTypeName = typeName; const workoutNameInput = document.getElementById('workout-name'); if (workoutNameInput) workoutNameInput.value = (typeName === "Libre" ? "" : typeName); if (workoutDateInput) setTodayDate(); const durationInput = document.getElementById('workout-duration'); if (durationInput) durationInput.value = '60'; if (exercisesContainer) exercisesContainer.innerHTML = ''; workoutExercisesForm = []; if (satisfactionRange) satisfactionRange.value = 75; if (satisfactionValue) satisfactionValue.textContent = '75%'; currentWorkoutId = null; currentExerciseIndex = 0; addExercise(false); updateWorkoutTypeIndicator(); renderActiveExerciseForm(); } | |
function updateWorkoutTypeIndicator() { /* ... inchangé ... */ if (!workoutTypeIndicator) return; if (selectedWorkoutTypeName && selectedWorkoutTypeName !== "Libre") { workoutTypeIndicator.textContent = selectedWorkoutTypeName; workoutTypeIndicator.classList.remove('hidden'); } else { workoutTypeIndicator.classList.add('hidden'); } } | |
function renderWorkoutsList() { /* ... inchangé ... */ if (!workoutsList || !emptyWorkoutMessage) { console.warn("Cannot render workout list: DOM elements missing."); return; } workoutsList.innerHTML = ''; if (!workouts || workouts.length === 0) { emptyWorkoutMessage.classList.remove('hidden'); } else { emptyWorkoutMessage.classList.add('hidden'); const sortedWorkouts = [...workouts].sort((a, b) => new Date(b.date) - new Date(a.date)); sortedWorkouts.forEach(workout => { if (!workout || !workout.id) { console.warn("Skipping invalid workout data:", workout); return; } const workoutDate = workout.date ? new Date(workout.date).toLocaleDateString('fr-FR', { year: 'numeric', month: 'short', day: 'numeric' }) : 'Date inconnue'; const workoutDiv = document.createElement('div'); workoutDiv.className = 'card workout-card'; workoutDiv.setAttribute('data-workout-id', workout.id); const typeBadge = workout.workoutTypeName && workout.workoutTypeName !== "Libre" ? `<span class="badge type-badge" style="margin-left: 8px;">${workout.workoutTypeName}</span>` : ''; const exercisesCount = (workout.exercises || []).length; workoutDiv.innerHTML = `<div class="workout-header"><h3 style="margin-bottom: 0.5rem;">${workout.name || 'Séance sans nom'} ${typeBadge}</h3><div class="badge">${workoutDate}</div></div><div class="workout-details"><span>${workout.duration || '?'} min</span> | <span>${exercisesCount} exo${exercisesCount > 1 ? 's' : ''}</span> | <span>${(workout.totalTonnage || 0).toFixed(1)} kg</span> | <span>${workout.satisfaction || '?'}%</span></div>`; workoutDiv.addEventListener('click', () => displayWorkoutDetails(workout.id)); workoutsList.appendChild(workoutDiv); }); } } | |
function displayWorkoutDetails(workoutId) { /* ... inchangé ... */ if (!currentFirebaseUser) { console.error("User not logged in for details"); return; } const workout = workouts.find(w => w && w.id === workoutId); if (!workout) { alert("Détails de la séance introuvables."); showPage('seances-page'); return; } console.log("Affichage détails pour:", workoutId, workout); const detailNameEl = document.getElementById('detail-workout-name'); const detailDateEl = document.getElementById('detail-date'); const detailDurationEl = document.getElementById('detail-duration'); const detailTonnageEl = document.getElementById('detail-tonnage'); const detailSatisfactionEl = document.getElementById('detail-satisfaction'); const detailExercisesCountEl = document.getElementById('detail-exercises-count'); const detailExercisesContainer = document.getElementById('detail-exercises-container'); const detailWorkoutTypeBadge = document.getElementById('detail-workout-type'); if (detailWorkoutTypeBadge) { if (workout.workoutTypeName && workout.workoutTypeName !== "Libre") { detailWorkoutTypeBadge.textContent = workout.workoutTypeName; detailWorkoutTypeBadge.classList.remove('hidden'); } else { detailWorkoutTypeBadge.classList.add('hidden'); } } if (detailNameEl) detailNameEl.textContent = workout.name || 'Séance sans nom'; if (detailDateEl) detailDateEl.textContent = workout.date ? new Date(workout.date).toLocaleDateString('fr-FR') : 'Inconnue'; if (detailDurationEl) detailDurationEl.textContent = workout.duration || '?'; if (detailTonnageEl) detailTonnageEl.textContent = (workout.totalTonnage || 0).toFixed(1); if (detailSatisfactionEl) detailSatisfactionEl.textContent = `${workout.satisfaction || '?'}%`; const exercisesCount = (workout.exercises || []).length; if (detailExercisesCountEl) detailExercisesCountEl.textContent = exercisesCount; if (detailExercisesContainer) { detailExercisesContainer.innerHTML = ''; if (exercisesCount > 0) { workout.exercises.forEach((exercise) => { if (!exercise) return; const exerciseDiv = document.createElement('div'); exerciseDiv.className = 'card detail-exercise-card'; let seriesHtml = ''; (exercise.series || []).forEach((serie, sIndex) => { const degressiveLabel = serie.isDegressive ? ' <span class="badge" style="font-size: 0.7rem; background-color: var(--info); color: var(--text-light);">Dégr.</span>' : ''; const weightFactor = exercise.isUnilateral ? 2 : 1; const seriesTonnage = (serie.reps || 0) * (serie.weight || 0) * weightFactor; seriesHtml += `<div class="exercise-summary"><div class="flex-between"><span>Série ${sIndex + 1}: ${serie.reps || '?'} reps × ${serie.weight || '?'} kg${degressiveLabel}</span><span style="color: var(--text-secondary);">(${seriesTonnage.toFixed(1)} kg)</span></div></div>`; }); const unilateralLabel = exercise.isUnilateral ? ' <span class="badge" style="font-size: 0.7rem; background-color: var(--info); color: var(--text-light);">Unilat.</span>' : ''; exerciseDiv.innerHTML = `<h4 style="font-size: 1.1rem; margin-bottom: 0.8rem; color: var(--text-light); text-transform: none; font-family: var(--font-primary); letter-spacing: normal;">${exercise.name || 'Exercice sans nom'}${unilateralLabel}</h4> <div class="series-summary-container"> ${seriesHtml} </div>`; detailExercisesContainer.appendChild(exerciseDiv); }); } else { detailExercisesContainer.innerHTML = '<p style="color: var(--text-secondary); text-align: center;">Aucun exercice enregistré.</p>'; } } currentWorkoutId = workoutId; showPage('workout-details-page'); } | |
function updateStats() { /* ... inchangé ... */ console.log("MàJ stats..."); if (!currentFirebaseUser || !statsWorkoutCountEl || !statsAvgTonnageEl || !statsAvgSatisfactionEl || !typeTrendsContainer || !noTrendsMessage) { console.warn("Cannot update stats: Missing elements."); if(statsWorkoutCountEl) statsWorkoutCountEl.textContent = '0'; if(statsAvgTonnageEl) statsAvgTonnageEl.textContent = '0'; if(statsAvgSatisfactionEl) statsAvgSatisfactionEl.textContent = '0%'; if(typeTrendsContainer) typeTrendsContainer.innerHTML = ''; if(noTrendsMessage) noTrendsMessage.classList.remove('hidden'); return; } const workoutCount = workouts.length; statsWorkoutCountEl.textContent = workoutCount; if (workoutCount === 0) { statsAvgTonnageEl.textContent = '0'; statsAvgSatisfactionEl.textContent = '0%'; renderTypeTrends({}); return; } const totalTonnageAll = workouts.reduce((sum, w) => sum + (w.totalTonnage || 0), 0); statsAvgTonnageEl.textContent = workoutCount > 0 ? (totalTonnageAll / workoutCount).toFixed(1) : '0'; const totalSatisfaction = workouts.reduce((sum, w) => sum + (w.satisfaction || 0), 0); statsAvgSatisfactionEl.textContent = workoutCount > 0 ? `${Math.round(totalSatisfaction / workoutCount)}%` : '0%'; console.log("Calcul tendances..."); const trendsData = {}; const structuredWorkouts = workouts.filter(w => w && w.workoutTypeName && w.workoutTypeName !== "Libre"); const groupedByType = structuredWorkouts.reduce((acc, workout) => { const type = workout.workoutTypeName; if (!acc[type]) acc[type] = []; acc[type].push(workout); return acc; }, {}); for (const typeName in groupedByType) { const sessionsOfType = groupedByType[typeName].sort((a, b) => new Date(a.date) - new Date(b.date)); trendsData[typeName] = sessionsOfType.slice(-3).map(s => ({ date: s.date, tonnage: s.totalTonnage || 0 })); } console.log("Données tendances:", trendsData); renderTypeTrends(trendsData); } | |
function renderTypeTrends(trends) { /* ... inchangé ... */ if (!typeTrendsContainer || !noTrendsMessage || !trendsSpinner) { console.error("Cannot render trends: Missing DOM elements."); return;} trendsSpinner.classList.add('hidden'); typeTrendsContainer.innerHTML = ''; const trendTypes = Object.keys(trends); if (trendTypes.length === 0) { noTrendsMessage.classList.remove('hidden'); } else { noTrendsMessage.classList.add('hidden'); trendTypes.sort().forEach(typeName => { const typeData = trends[typeName]; if (!typeData || typeData.length === 0) return; const card = document.createElement('div'); card.className = 'card type-trend-card'; let listItems = ''; typeData.forEach(session => { const formattedDate = session.date ? new Date(session.date).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short' }) : '??'; const tonnageText = (session.tonnage !== undefined && session.tonnage !== null) ? `${session.tonnage.toFixed(1)} kg` : 'N/A'; listItems += `<li><span>${formattedDate}</span> <strong>${tonnageText}</strong></li>`; }); card.innerHTML = `<h4>${typeName} (3 Dernières)</h4><ul>${listItems}</ul>`; typeTrendsContainer.appendChild(card); }); } } | |
function handleFirestoreError(error) { /* ... inchangé ... */ console.error("Erreur Firestore:", error.code, error.message); alert(`Erreur de base de données: ${error.message}`); } | |
// --- ÉCOUTEURS D'ÉVÉNEMENTS --- | |
function initEventListeners() { | |
console.log("initEventListeners: Attachement..."); | |
// Logo | |
if (logoLink) logoLink.addEventListener('click', (e) => { e.preventDefault(); if (currentFirebaseUser) showPage('accueil-page'); }); else console.error("!logoLink"); | |
// Auth | |
if (loginForm) loginForm.addEventListener('submit', handleAuthAction); else console.error("!loginForm"); | |
if (authSwitchLink) authSwitchLink.addEventListener('click', authSwitchMode); else console.error("!authSwitchLink"); | |
if (forgotPasswordLink) forgotPasswordLink.addEventListener('click', handlePasswordReset); else console.error("!forgotPasswordLink"); | |
if (logoutBtn) logoutBtn.addEventListener('click', handleLogout); else console.error("!logoutBtn"); | |
// Nav Bottom | |
navItems.forEach((item) => { if (item) item.addEventListener('click', (e) => { e.preventDefault(); const targetPageId = item.getAttribute('data-page'); if (!currentFirebaseUser && targetPageId !== 'login-page') { showLoginPage(); return; } if (targetPageId) showPage(targetPageId); }); else console.error(`!navItem`); }); | |
// Workout List Actions | |
if (newWorkoutBtn) newWorkoutBtn.addEventListener('click', () => { if (!currentFirebaseUser) return; showPage('select-workout-type-page'); }); else console.error("!newWorkoutBtn"); | |
if (manageTypesBtn) manageTypesBtn.addEventListener('click', () => { if (!currentFirebaseUser) return; showPage('manage-types-page'); }); else console.error("!manageTypesBtn"); | |
// Type Management | |
if (addTypeForm) addTypeForm.addEventListener('submit', handleAddWorkoutType); else console.error("!addTypeForm"); | |
// Select Type Page | |
if (selectWorkoutTypeDropdown) selectWorkoutTypeDropdown.addEventListener('change', updateStartStructuredBtnState); else console.error("!selectWorkoutTypeDropdown"); | |
if (startStructuredWorkoutBtn) startStructuredWorkoutBtn.addEventListener('click', () => { if (selectWorkoutTypeDropdown) { const type = selectWorkoutTypeDropdown.value; if (type) { clearNewWorkoutForm(type); showPage('new-workout-page'); } else { alert("Veuillez sélectionner un type de séance."); } } }); else console.error("!startStructuredWorkoutBtn"); | |
if (startFreeWorkoutBtn) startFreeWorkoutBtn.addEventListener('click', () => { clearNewWorkoutForm("Libre"); showPage('new-workout-page'); }); else console.error("!startFreeWorkoutBtn"); | |
// General Nav (Retour) -> Va maintenant à seances-page | |
backToSeancesBtns.forEach(btn => { if (btn) btn.addEventListener('click', () => showPage('seances-page')); else console.error("!backToSeancesBtn missing"); }); // **RENOMMÉ** | |
// New Workout Actions | |
if (cancelNewWorkoutBtn) cancelNewWorkoutBtn.addEventListener('click', () => { if (confirm("Annuler cette séance ? Les données non enregistrées seront perdues.")) showPage('seances-page'); }); else console.error("!cancelNewWorkoutBtn"); | |
if (saveWorkoutBtn) saveWorkoutBtn.addEventListener('click', saveWorkout); else console.error("!saveWorkoutBtn"); | |
if (addExerciseBtn) addExerciseBtn.addEventListener('click', handleAddNewExercise); else console.error("!addExerciseBtn"); | |
// Exercise Nav | |
if (prevExerciseBtn) prevExerciseBtn.addEventListener('click', () => navigateExerciseForm(-1)); else console.error("!prevExerciseBtn"); | |
if (nextExerciseBtn) nextExerciseBtn.addEventListener('click', () => navigateExerciseForm(1)); else console.error("!nextExerciseBtn"); | |
// Details Page | |
if (deleteWorkoutBtn) deleteWorkoutBtn.addEventListener('click', deleteWorkout); else console.error("!deleteWorkoutBtn"); | |
// Satisfaction | |
if (satisfactionRange) satisfactionRange.addEventListener('input', () => { if (satisfactionValue) satisfactionValue.textContent = `${satisfactionRange.value}%`; }); else console.error("!satisfactionRange"); | |
console.log("initEventListeners: Fin attachement."); | |
} | |
// ================================================== | |
// --- FIN DES DÉFINITIONS DE FONCTIONS --- | |
// ================================================== | |
// --- INITIALISATION --- | |
document.addEventListener('DOMContentLoaded', () => { | |
console.log("DOM chargé. Initialisation..."); | |
if (typeof firebase !== 'undefined' && auth && db) { | |
console.log("Firebase prêt. Initialisation listeners..."); | |
initEventListeners(); | |
initAuthListener(); // Gère l'état initial | |
} else { | |
console.error("ERREUR CRITIQUE: Firebase non initialisé ou SDK manquant."); | |
document.body.innerHTML = '<div style="color: red; padding: 30px; text-align: center; font-family: sans-serif; background-color: #333;"><h1>Erreur Critique</h1><p>Impossible de charger les services nécessaires. Vérifiez la console (F12).</p></div>'; | |
} | |
}); // FIN DOMContentLoaded | |
</script> <!-- FIN SCRIPT --> | |
</body> <!-- FIN BODY --> | |
</html> <!-- FIN HTML --> |