TrackLift / tracklift
PAUTREL Johan
Create tracklift
b2d08aa verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Track Lift - Suivi de Musculation</title>
<style>
:root {
--bg-dark: #121212;
--bg-card: #1e1e1e;
--text-light: #e0e0e0;
--accent: #4CAF50;
--accent-dark: #3a8a3d;
--danger: #f44336;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: var(--bg-dark);
color: var(--text-light);
min-height: 100vh;
padding-bottom: 80px; /* Pour le menu fixe en bas */
}
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
header {
padding: 1rem 0;
text-align: center;
border-bottom: 1px solid #333;
margin-bottom: 1rem;
}
h1, h2, h3 {
color: var(--accent);
}
.btn {
background-color: var(--accent);
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s;
}
.btn:hover {
background-color: var(--accent-dark);
}
.btn-outline {
background-color: transparent;
color: var(--accent);
border: 1px solid var(--accent);
}
.btn-danger {
background-color: var(--danger);
}
input, select, textarea {
width: 100%;
padding: 0.6rem;
margin-bottom: 1rem;
background-color: #2a2a2a;
border: 1px solid #444;
border-radius: 4px;
color: var(--text-light);
}
input[type="checkbox"] {
width: auto;
margin-right: 0.5rem;
}
.card {
background-color: var(--bg-card);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.form-group {
margin-bottom: 1rem;
}
.form-row {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.form-row > * {
flex: 1;
margin-bottom: 0;
}
label {
display: block;
margin-bottom: 0.3rem;
color: #bbb;
}
.exercise {
border-left: 3px solid var(--accent);
padding-left: 1rem;
margin-bottom: 1.5rem;
}
.exercise-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.series-container {
margin-left: 0.5rem;
margin-top: 0.5rem;
}
.series {
background-color: #252525;
padding: 0.7rem;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.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 -2px 10px rgba(0, 0, 0, 0.3);
}
.nav-item {
text-align: center;
color: #888;
text-decoration: none;
font-size: 0.85rem;
transition: color 0.2s;
}
.nav-item.active {
color: var(--accent);
}
.nav-icon {
font-size: 1.4rem;
margin-bottom: 0.2rem;
}
.workout-card {
border-left: 3px solid var(--accent);
cursor: pointer;
transition: transform 0.2s;
}
.workout-card:hover {
transform: translateX(5px);
}
.workout-header {
display: flex;
justify-content: space-between;
}
.stat-card {
text-align: center;
padding: 1rem;
}
.stat-value {
font-size: 1.8rem;
color: var(--accent);
font-weight: bold;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 1rem;
}
.hidden {
display: none;
}
#app-container > div {
display: none;
}
#app-container > div.active {
display: block;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.badge {
background-color: var(--accent);
color: white;
padding: 0.2rem 0.5rem;
border-radius: 10px;
font-size: 0.8rem;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 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.3rem 0;
border-bottom: 1px solid #333;
}
.satisfaction {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-top: 1rem;
}
.satisfaction-value {
font-size: 2rem;
color: var(--accent);
margin-top: 0.5rem;
}
/* Pour les téléphones */
@media (max-width: 600px) {
.form-row {
flex-direction: column;
gap: 0;
}
.container {
padding: 0.5rem;
}
h1 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Track Lift</h1>
<p>Suivi de vos séances de musculation</p>
</header>
<div id="app-container">
<!-- Page d'accueil / Liste des séances -->
<div id="home-page" class="active">
<div class="flex-between">
<h2>Mes séances</h2>
<button id="new-workout-btn" class="btn">Nouvelle séance</button>
</div>
<div id="workouts-list" class="workout-list">
<!-- Liste des séances qui sera remplie dynamiquement -->
<div class="spinner"></div>
<p id="empty-workout-message" class="hidden" style="text-align: center; margin-top: 2rem;">
Aucune séance enregistrée. Créez votre première séance avec le bouton ci-dessus.
</p>
</div>
</div>
<!-- Page Nouvelle Séance -->
<div id="new-workout-page" class="hidden">
<div class="flex-between">
<h2>Nouvelle séance</h2>
<button id="save-workout-btn" class="btn">Enregistrer</button>
</div>
<div class="card">
<div class="form-group">
<label for="workout-name">Nom de la séance</label>
<input type="text" id="workout-name" placeholder="Ex: Push, Jambes, Full body...">
</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 (minutes)</label>
<input type="number" id="workout-duration" min="1" placeholder="60">
</div>
</div>
</div>
<h3 style="margin: 1rem 0;">Exercices</h3>
<div id="exercises-container">
<!-- Exercices ajoutés dynamiquement -->
</div>
<button id="add-exercise-btn" class="btn btn-outline" style="width: 100%; margin-top: 1rem;">
+ Ajouter un exercice
</button>
<div class="card" style="margin-top: 2rem;">
<div class="form-group">
<label for="satisfaction">Niveau de satisfaction (1-100%)</label>
<input type="range" id="satisfaction" min="1" max="100" value="75">
<div class="satisfaction">
<span>Satisfaction</span>
<div class="satisfaction-value">75%</div>
</div>
</div>
</div>
</div>
<!-- Page Détail Séance -->
<div id="workout-details-page" class="hidden">
<div class="flex-between">
<h2 id="detail-workout-name">Détail de la séance</h2>
<button id="back-to-home" class="btn btn-outline">Retour</button>
</div>
<div class="card">
<div class="workout-details-info">
<div class="form-row">
<p><strong>Date:</strong> <span id="detail-date"></span></p>
<p><strong>Durée:</strong> <span id="detail-duration"></span> min</p>
</div>
</div>
</div>
<div class="stats-grid" style="margin-top: 1rem;">
<div class="card stat-card">
<div class="stat-value" id="detail-tonnage">0</div>
<div>Tonnage total (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">
<!-- Exercices affichés dynamiquement -->
</div>
<button id="delete-workout-btn" class="btn btn-danger" style="width: 100%; margin-top: 2rem;">
Supprimer cette séance
</button>
</div>
<!-- Page Statistiques -->
<div id="stats-page" class="hidden">
<h2>Statistiques</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</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: 1.5rem 0 1rem;">Dernières tendances</h3>
<div class="card">
<p style="text-align: center; margin: 1rem 0;">
Les statistiques détaillées seront calculées après plusieurs séances enregistrées.
</p>
</div>
</div>
</div>
</div>
<!-- Menu de navigation en bas -->
<nav class="nav-bottom">
<a href="#" class="nav-item active" data-page="home-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>
<script>
// Structure de données
let workouts = [];
const STORAGE_KEY = 'workout-tracker-data';
// Éléments DOM
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 addExerciseBtn = document.getElementById('add-exercise-btn');
const exercisesContainer = document.getElementById('exercises-container');
const workoutsList = document.getElementById('workouts-list');
const backToHomeBtn = document.getElementById('back-to-home');
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');
// Variables globales
let currentWorkoutId = null;
// Initialisation
document.addEventListener('DOMContentLoaded', () => {
loadWorkouts();
renderWorkoutsList();
initEventListeners();
setTodayDate();
});
// Définir la date d'aujourd'hui par défaut
function setTodayDate() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('workout-date').value = today;
}
// Charger les données depuis le stockage local
function loadWorkouts() {
const savedData = localStorage.getItem(STORAGE_KEY);
if (savedData) {
workouts = JSON.parse(savedData);
}
}
// Sauvegarder les données dans le stockage local
function saveWorkouts() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(workouts));
}
// Initialiser les événements
function initEventListeners() {
// Navigation
navItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const targetPage = item.getAttribute('data-page');
// Changer la page active
document.querySelectorAll('#app-container > div').forEach(page => {
page.classList.remove('active');
});
document.getElementById(targetPage).classList.add('active');
// Mettre à jour la navigation
navItems.forEach(navItem => navItem.classList.remove('active'));
item.classList.add('active');
// Si on va à la page statistiques, mettre à jour les stats
if (targetPage === 'stats-page') {
updateStats();
}
});
});
// Nouvelle séance
newWorkoutBtn.addEventListener('click', () => {
showPage('new-workout-page');
clearNewWorkoutForm();
currentWorkoutId = null;
});
// Enregistrer la séance
saveWorkoutBtn.addEventListener('click', saveWorkout);
// Ajouter un exercice
addExerciseBtn.addEventListener('click', addExercise);
// Retour à l'accueil
backToHomeBtn.addEventListener('click', () => {
showPage('home-page');
});
// Supprimer une séance
deleteWorkoutBtn.addEventListener('click', deleteWorkout);
// Affichage du pourcentage de satisfaction en temps réel
satisfactionRange.addEventListener('input', () => {
satisfactionValue.textContent = `${satisfactionRange.value}%`;
});
}
// Afficher une page spécifique
function showPage(pageId) {
document.querySelectorAll('#app-container > div').forEach(page => {
page.classList.remove('active');
});
document.getElementById(pageId).classList.add('active');
// Mettre à jour la navigation
const navItem = document.querySelector(`.nav-item[data-page="${pageId}"]`);
if (navItem) {
navItems.forEach(item => item.classList.remove('active'));
navItem.classList.add('active');
}
}
// Ajouter un exercice au formulaire
function addExercise() {
const exerciseId = `exercise-${Date.now()}`;
const exerciseDiv = document.createElement('div');
exerciseDiv.className = 'exercise card';
exerciseDiv.setAttribute('data-exercise-id', exerciseId);
exerciseDiv.innerHTML = `
<div class="exercise-header">
<input type="text" placeholder="Nom de l'exercice" class="exercise-name">
<button class="btn btn-danger remove-exercise" style="padding: 0.3rem 0.6rem;">×</button>
</div>
<div class="form-row">
<div class="form-group">
<label>
<input type="checkbox" class="unilateral-checkbox"> Exercice unilatéral
</label>
</div>
</div>
<div class="series-container">
<!-- Les séries seront ajoutées ici -->
</div>
<button class="btn btn-outline add-series" style="width: 100%; margin-top: 0.5rem;">
+ Ajouter une série
</button>
`;
exercisesContainer.appendChild(exerciseDiv);
// Ajouter les écouteurs d'événements
const removeBtn = exerciseDiv.querySelector('.remove-exercise');
removeBtn.addEventListener('click', () => {
exerciseDiv.remove();
});
const addSeriesBtn = exerciseDiv.querySelector('.add-series');
addSeriesBtn.addEventListener('click', () => {
addSeries(exerciseDiv.querySelector('.series-container'));
});
// Ajouter la première série automatiquement
addSeries(exerciseDiv.querySelector('.series-container'));
}
// Ajouter une série à un exercice
function addSeries(container) {
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">
<div class="form-group">
<label>Répétitions</label>
<input type="number" class="reps" min="1" placeholder="12">
</div>
<div class="form-group">
<label>Charge (kg)</label>
<input type="number" class="weight" min="0" step="0.5" placeholder="20">
</div>
<div class="form-group" style="flex: 0.5;">
<label>
<input type="checkbox" class="degressive-checkbox"> Série dégressive
</label>
</div>
<button class="btn btn-danger remove-series" style="align-self: flex-end; padding: 0.3rem 0.6rem; flex: 0.2;">×</button>
</div>
`;
container.appendChild(seriesDiv);
// Écouteur pour supprimer une série
const removeBtn = seriesDiv.querySelector('.remove-series');
removeBtn.addEventListener('click', () => {
seriesDiv.remove();
});
}
// Sauvegarder la séance
function saveWorkout() {
// Récupérer les données de base
const workoutName = document.getElementById('workout-name').value.trim();
const workoutDate = document.getElementById('workout-date').value;
const workoutDuration = parseInt(document.getElementById('workout-duration').value) || 0;
const satisfaction = parseInt(document.getElementById('satisfaction').value);
// Vérifier les données obligatoires
if (!workoutName) {
alert("Veuillez saisir un nom pour la séance");
return;
}
if (!workoutDate) {
alert("Veuillez saisir une date");
return;
}
if (workoutDuration <= 0) {
alert("Veuillez saisir une durée valide");
return;
}
// Récupérer les exercices
const exercises = [];
const exerciseElements = exercisesContainer.querySelectorAll('.exercise');
if (exerciseElements.length === 0) {
alert("Ajoutez au moins un exercice");
return;
}
for (const exerciseEl of exerciseElements) {
const exerciseName = exerciseEl.querySelector('.exercise-name').value.trim();
const isUnilateral = exerciseEl.querySelector('.unilateral-checkbox').checked;
if (!exerciseName) {
alert("Veuillez saisir un nom pour chaque exercice");
return;
}
// Récupérer les séries
const series = [];
const seriesElements = exerciseEl.querySelectorAll('.series');
if (seriesElements.length === 0) {
alert(`Ajoutez au moins une série à l'exercice "${exerciseName}"`);
return;
}
for (const seriesEl of seriesElements) {
const reps = parseInt(seriesEl.querySelector('.reps').value) || 0;
const weight = parseFloat(seriesEl.querySelector('.weight').value) || 0;
const isDegressive = seriesEl.querySelector('.degressive-checkbox').checked;
if (reps <= 0) {
alert(`Veuillez saisir un nombre de répétitions valide pour l'exercice "${exerciseName}"`);
return;
}
series.push({
reps,
weight,
isDegressive
});
}
exercises.push({
name: exerciseName,
isUnilateral,
series
});
}
// Calculer le tonnage total
let totalTonnage = 0;
exercises.forEach(exercise => {
exercise.series.forEach(series => {
// Si l'exercice est unilatéral, on multiplie par 2 le tonnage
const weightFactor = exercise.isUnilateral ? 2 : 1;
totalTonnage += series.reps * series.weight * weightFactor;
});
});
// Créer l'objet séance
const workout = {
id: currentWorkoutId || `workout-${Date.now()}`,
name: workoutName,
date: workoutDate,
duration: workoutDuration,
exercises,
totalTonnage,
satisfaction
};
// Ajouter ou mettre à jour la séance
if (currentWorkoutId) {
// Mise à jour d'une séance existante
const index = workouts.findIndex(w => w.id === currentWorkoutId);
if (index !== -1) {
workouts[index] = workout;
}
} else {
// Nouvelle séance
workouts.push(workout);
}
// Sauvegarder les données
saveWorkouts();
// Retourner à la liste des séances
showPage('home-page');
renderWorkoutsList();
}
// Afficher la liste des séances
function renderWorkoutsList() {
// Vider la liste
workoutsList.innerHTML = '';
// Afficher le message si pas de séances
if (workouts.length === 0) {
emptyWorkoutMessage.classList.remove('hidden');
return;
}
emptyWorkoutMessage.classList.add('hidden');
// Trier les séances par date (la plus récente en premier)
const sortedWorkouts = [...workouts].sort((a, b) => {
return new Date(b.date) - new Date(a.date);
});
// Générer le HTML pour chaque séance
sortedWorkouts.forEach(workout => {
const workoutDate = new Date(workout.date).toLocaleDateString('fr-FR');
const workoutDiv = document.createElement('div');
workoutDiv.className = 'card workout-card';
workoutDiv.setAttribute('data-workout-id', workout.id);
workoutDiv.innerHTML = `
<div class="workout-header">
<h3>${workout.name}</h3>
<div class="badge">${workoutDate}</div>
</div>
<div class="workout-details">
<p><strong>Durée:</strong> ${workout.duration} min</p>
<p><strong>Exercices:</strong> ${workout.exercises.length}</p>
<p><strong>Tonnage:</strong> ${workout.totalTonnage.toFixed(1)} kg</p>
<p><strong>Satisfaction:</strong> ${workout.satisfaction}%</p>
</div>
`;
// Ajouter l'écouteur pour afficher les détails
workoutDiv.addEventListener('click', () => {
displayWorkoutDetails(workout.id);
});
workoutsList.appendChild(workoutDiv);
});
// Mettre à jour les statistiques globales
updateStats();
}
// Afficher les détails d'une séance
function displayWorkoutDetails(workoutId) {
const workout = workouts.find(w => w.id === workoutId);
if (!workout) return;
// Mettre à jour les informations de base
document.getElementById('detail-workout-name').textContent = workout.name;
document.getElementById('detail-date').textContent = new Date(workout.date).toLocaleDateString('fr-FR');
document.getElementById('detail-duration').textContent = workout.duration;
document.getElementById('detail-tonnage').textContent = workout.totalTonnage.toFixed(1);
document.getElementById('detail-satisfaction').textContent = `${workout.satisfaction}%`;
document.getElementById('detail-exercises-count').textContent = workout.exercises.length;
// Afficher les exercices
const exercisesContainer = document.getElementById('detail-exercises-container');
exercisesContainer.innerHTML = '';
workout.exercises.forEach(exercise => {
const exerciseDiv = document.createElement('div');
exerciseDiv.className = 'card';
let seriesHtml = '';
exercise.series.forEach(series => {
const degressiveLabel = series.isDegressive ? ' <span class="badge">Dégressive</span>' : '';
const weightFactor = exercise.isUnilateral ? 2 : 1;
const seriesTonnage = series.reps * series.weight * weightFactor;
seriesHtml += `
<div class="exercise-summary">
<div class="flex-between">
<span>${series.reps} répétitions × ${series.weight} kg${degressiveLabel}</span>
<span>${seriesTonnage.toFixed(1)} kg</span>
</div>
</div>
`;
});
const unilateralLabel = exercise.isUnilateral ? ' <span class="badge">Unilatéral</span>' : '';
exerciseDiv.innerHTML = `
<h3>${exercise.name}${unilateralLabel}</h3>
<div class="series-summary">
${seriesHtml}
</div>
`;
exercisesContainer.appendChild(exerciseDiv);
});
// Stocker l'ID pour les éditions/suppressions
currentWorkoutId = workoutId;
// Afficher la page
showPage('workout-details-page');
}
// Supprimer une séance
function deleteWorkout() {
if (!currentWorkoutId) return;
const confirmDelete = confirm("Êtes-vous sûr de vouloir supprimer cette séance ?");
if (!confirmDelete) return
// Continuation du script précédent
if (!confirmDelete) return;
// Filtrer la séance à supprimer
workouts = workouts.filter(w => w.id !== currentWorkoutId);
// Sauvegarder les données
saveWorkouts();
// Retourner à la liste des séances
showPage('home-page');
renderWorkoutsList();
}
// Effacer le formulaire de nouvelle séance
function clearNewWorkoutForm() {
document.getElementById('workout-name').value = '';
document.getElementById('workout-duration').value = '';
setTodayDate();
exercisesContainer.innerHTML = '';
document.getElementById('satisfaction').value = 75;
satisfactionValue.textContent = '75%';
}
// Mettre à jour les statistiques
function updateStats() {
if (workouts.length === 0) {
document.getElementById('stats-workout-count').textContent = '0';
document.getElementById('stats-avg-tonnage').textContent = '0';
document.getElementById('stats-avg-satisfaction').textContent = '0%';
return;
}
// Nombre total de séances
document.getElementById('stats-workout-count').textContent = workouts.length;
// Tonnage moyen
const totalTonnage = workouts.reduce((sum, workout) => sum + workout.totalTonnage, 0);
const avgTonnage = totalTonnage / workouts.length;
document.getElementById('stats-avg-tonnage').textContent = avgTonnage.toFixed(1);
// Satisfaction moyenne
const totalSatisfaction = workouts.reduce((sum, workout) => sum + workout.satisfaction, 0);
const avgSatisfaction = totalSatisfaction / workouts.length;
document.getElementById('stats-avg-satisfaction').textContent = `${Math.round(avgSatisfaction)}%`;
}
</script>
</body>
</html>