Spaces:
Running
Running
<html lang="fr"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>MyFitCalendar - Suivi Musculation</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<script> | |
tailwind.config = { | |
theme: { | |
extend: { | |
colors: { | |
primary: '#10B981', | |
dark: '#111827', | |
darker: '#0B1120', | |
light: '#F3F4F6', | |
} | |
} | |
} | |
} | |
</script> | |
<style> | |
/* Custom scrollbar */ | |
::-webkit-scrollbar { | |
width: 8px; | |
} | |
::-webkit-scrollbar-track { | |
background: #1F2937; | |
} | |
::-webkit-scrollbar-thumb { | |
background: #10B981; | |
border-radius: 4px; | |
} | |
/* Animation for workout cards */ | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
.workout-card { | |
animation: fadeIn 0.3s ease-out forwards; | |
} | |
/* Custom checkbox */ | |
.custom-checkbox { | |
appearance: none; | |
-webkit-appearance: none; | |
width: 20px; | |
height: 20px; | |
border: 2px solid #10B981; | |
border-radius: 4px; | |
outline: none; | |
cursor: pointer; | |
position: relative; | |
} | |
.custom-checkbox:checked { | |
background-color: #10B981; | |
} | |
.custom-checkbox:checked::after { | |
content: '✓'; | |
position: absolute; | |
color: white; | |
font-size: 14px; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
} | |
/* Date picker custom style */ | |
input[type="date"]::-webkit-calendar-picker-indicator { | |
filter: invert(1); | |
} | |
</style> | |
</head> | |
<body class="bg-dark text-light min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<!-- Header --> | |
<header class="flex justify-between items-center mb-8"> | |
<div> | |
<h1 class="text-3xl font-bold text-primary">MyFitCalendar</h1> | |
<p class="text-gray-400">Suivi de vos séances de musculation</p> | |
</div> | |
<div class="flex items-center space-x-4"> | |
<button id="auth-btn" class="bg-primary hover:bg-green-600 text-dark font-medium py-2 px-4 rounded-lg transition"> | |
<i class="fas fa-sign-in-alt mr-2"></i>Connexion | |
</button> | |
<button id="export-btn" class="border border-primary text-primary hover:bg-green-900/50 font-medium py-2 px-4 rounded-lg transition"> | |
<i class="fas fa-file-export mr-2"></i>Exporter | |
</button> | |
<button id="import-btn" class="border border-primary text-primary hover:bg-green-900/50 font-medium py-2 px-4 rounded-lg transition"> | |
<i class="fas fa-file-import mr-2"></i>Importer | |
</button> | |
</div> | |
</header> | |
<!-- Main Content --> | |
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
<!-- Calendar Section --> | |
<div class="lg:col-span-2 bg-darker rounded-xl p-6 shadow-lg"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 class="text-2xl font-semibold text-primary">Calendrier</h2> | |
<div class="flex items-center space-x-4"> | |
<button id="prev-month" class="text-primary hover:text-green-300 transition"> | |
<i class="fas fa-chevron-left"></i> | |
</button> | |
<span id="current-month" class="font-medium">Mai 2023</span> | |
<button id="next-month" class="text-primary hover:text-green-300 transition"> | |
<i class="fas fa-chevron-right"></i> | |
</button> | |
</div> | |
</div> | |
<!-- Calendar Grid --> | |
<div class="grid grid-cols-7 gap-2 mb-4"> | |
<div class="text-center font-medium text-primary">Lun</div> | |
<div class="text-center font-medium text-primary">Mar</div> | |
<div class="text-center font-medium text-primary">Mer</div> | |
<div class="text-center font-medium text-primary">Jeu</div> | |
<div class="text-center font-medium text-primary">Ven</div> | |
<div class="text-center font-medium text-primary">Sam</div> | |
<div class="text-center font-medium text-primary">Dim</div> | |
</div> | |
<div id="calendar-grid" class="grid grid-cols-7 gap-2"> | |
<!-- Calendar days will be generated by JavaScript --> | |
</div> | |
<!-- Multi-day selection controls --> | |
<div class="mt-6 p-4 bg-gray-800 rounded-lg"> | |
<div class="flex items-center justify-between mb-2"> | |
<h3 class="text-primary font-medium">Sélection multiple</h3> | |
<button id="clear-selection" class="text-xs text-gray-400 hover:text-primary transition">Effacer</button> | |
</div> | |
<p class="text-sm text-gray-400 mb-3">Sélectionnez plusieurs jours pour marquer une séance sur plusieurs jours</p> | |
<div class="flex items-center space-x-4"> | |
<div class="flex-1"> | |
<label class="block text-sm text-gray-400 mb-1">Couleur</label> | |
<select id="multi-day-color" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"> | |
<option value="bg-green-500/30">Vert</option> | |
<option value="bg-blue-500/30">Bleu</option> | |
<option value="bg-purple-500/30">Violet</option> | |
<option value="bg-yellow-500/30">Jaune</option> | |
<option value="bg-red-500/30">Rouge</option> | |
</select> | |
</div> | |
<button id="apply-multi-day" class="mt-5 bg-primary hover:bg-green-600 text-dark font-medium py-2 px-4 rounded-lg transition"> | |
<i class="fas fa-check mr-2"></i>Appliquer | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Workout Form Section --> | |
<div class="bg-darker rounded-xl p-6 shadow-lg"> | |
<h2 class="text-2xl font-semibold text-primary mb-6">Nouvelle Séance</h2> | |
<form id="workout-form" class="space-y-4"> | |
<div> | |
<label for="workout-name" class="block text-sm font-medium text-gray-400 mb-1">Nom de la séance</label> | |
<input type="text" id="workout-name" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary" placeholder="Ex: Push Day" required> | |
</div> | |
<div> | |
<label for="workout-date" class="block text-sm font-medium text-gray-400 mb-1">Date</label> | |
<input type="date" id="workout-date" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary" required> | |
</div> | |
<div> | |
<label for="workout-duration" class="block text-sm font-medium text-gray-400 mb-1">Durée (minutes)</label> | |
<input type="number" id="workout-duration" min="1" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary" placeholder="60" required> | |
</div> | |
<div> | |
<label for="workout-satisfaction" class="block text-sm font-medium text-gray-400 mb-1">Satisfaction</label> | |
<div class="flex items-center space-x-2"> | |
<input type="range" id="workout-satisfaction" min="0" max="100" value="50" class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer"> | |
<span id="satisfaction-value" class="text-primary font-medium">50%</span> | |
</div> | |
</div> | |
<div class="pt-2"> | |
<label class="flex items-center space-x-2 cursor-pointer"> | |
<input type="checkbox" id="intensity-drop" class="custom-checkbox"> | |
<span class="text-sm text-gray-400">Série dégressive</span> | |
</label> | |
</div> | |
<!-- Exercises Container --> | |
<div class="space-y-4"> | |
<div class="flex justify-between items-center"> | |
<h3 class="text-sm font-medium text-gray-400">Exercices</h3> | |
<button type="button" id="add-exercise" class="text-xs text-primary hover:text-green-300 transition flex items-center"> | |
<i class="fas fa-plus mr-1"></i> Ajouter | |
</button> | |
</div> | |
<div id="exercises-container"> | |
<!-- Exercise fields will be added here --> | |
</div> | |
</div> | |
<div class="pt-4"> | |
<button type="submit" class="w-full bg-primary hover:bg-green-600 text-dark font-medium py-2 px-4 rounded-lg transition flex items-center justify-center"> | |
<i class="fas fa-save mr-2"></i> Enregistrer | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
<!-- Workouts List Section --> | |
<div class="mt-8 bg-darker rounded-xl p-6 shadow-lg"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 class="text-2xl font-semibold text-primary">Historique des Séances</h2> | |
<div class="flex items-center space-x-2"> | |
<input type="text" id="search-workouts" placeholder="Rechercher..." class="bg-gray-700 border border-gray-600 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-primary"> | |
<button id="clear-search" class="text-gray-400 hover:text-primary transition"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
</div> | |
<div id="workouts-list" class="space-y-3"> | |
<!-- Workout cards will be added here --> | |
<div class="text-center text-gray-500 py-8"> | |
<i class="fas fa-dumbbell text-4xl mb-2"></i> | |
<p>Aucune séance enregistrée</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Modals --> | |
<!-- Workout Details Modal --> | |
<div id="workout-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden"> | |
<div class="bg-darker rounded-xl p-6 w-full max-w-md mx-4 max-h-[90vh] overflow-y-auto"> | |
<div class="flex justify-between items-center mb-4"> | |
<h3 id="modal-title" class="text-xl font-semibold text-primary"></h3> | |
<button id="close-modal" class="text-gray-400 hover:text-primary transition"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div id="modal-content" class="space-y-4"> | |
<!-- Workout details will be added here --> | |
</div> | |
<div class="mt-6 flex justify-end space-x-3"> | |
<button id="delete-workout" class="text-red-400 hover:text-red-300 transition flex items-center"> | |
<i class="fas fa-trash mr-2"></i> Supprimer | |
</button> | |
<button id="edit-workout" class="text-primary hover:text-green-300 transition flex items-center"> | |
<i class="fas fa-edit mr-2"></i> Modifier | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Import/Export Modal --> | |
<div id="data-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden"> | |
<div class="bg-darker rounded-xl p-6 w-full max-w-md mx-4"> | |
<div class="flex justify-between items-center mb-4"> | |
<h3 id="data-modal-title" class="text-xl font-semibold text-primary"></h3> | |
<button id="close-data-modal" class="text-gray-400 hover:text-primary transition"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div id="data-modal-content" class="space-y-4"> | |
<textarea id="data-textarea" class="w-full h-40 bg-gray-700 border border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary" placeholder="Données JSON..."></textarea> | |
<div id="user-prefix-container" class="hidden"> | |
<label for="user-prefix" class="block text-sm font-medium text-gray-400 mb-1">Préfixe utilisateur</label> | |
<input type="text" id="user-prefix" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary" placeholder="Votre nom"> | |
</div> | |
</div> | |
<div class="mt-6 flex justify-end"> | |
<button id="confirm-data-action" class="bg-primary hover:bg-green-600 text-dark font-medium py-2 px-4 rounded-lg transition"> | |
Confirmer | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Auth Modal --> | |
<div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden"> | |
<div class="bg-darker rounded-xl p-6 w-full max-w-md mx-4"> | |
<div class="flex justify-between items-center mb-4"> | |
<h3 class="text-xl font-semibold text-primary">Connexion</h3> | |
<button id="close-auth-modal" class="text-gray-400 hover:text-primary transition"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="space-y-4"> | |
<p class="text-gray-400">Connectez-vous avec Google pour sauvegarder vos données en ligne.</p> | |
<button id="google-auth-btn" class="w-full bg-white hover:bg-gray-100 text-dark font-medium py-2 px-4 rounded-lg transition flex items-center justify-center"> | |
<img src="https://upload.wikimedia.org/wikipedia/commons/5/53/Google_%22G%22_Logo.svg" alt="Google logo" class="w-5 h-5 mr-3"> | |
Continuer avec Google | |
</button> | |
</div> | |
</div> | |
</div> | |
<script> | |
// DOM Elements | |
const calendarGrid = document.getElementById('calendar-grid'); | |
const currentMonthEl = document.getElementById('current-month'); | |
const prevMonthBtn = document.getElementById('prev-month'); | |
const nextMonthBtn = document.getElementById('next-month'); | |
const workoutForm = document.getElementById('workout-form'); | |
const exercisesContainer = document.getElementById('exercises-container'); | |
const addExerciseBtn = document.getElementById('add-exercise'); | |
const workoutsList = document.getElementById('workouts-list'); | |
const workoutModal = document.getElementById('workout-modal'); | |
const closeModalBtn = document.getElementById('close-modal'); | |
const modalTitle = document.getElementById('modal-title'); | |
const modalContent = document.getElementById('modal-content'); | |
const deleteWorkoutBtn = document.getElementById('delete-workout'); | |
const editWorkoutBtn = document.getElementById('edit-workout'); | |
const satisfactionSlider = document.getElementById('workout-satisfaction'); | |
const satisfactionValue = document.getElementById('satisfaction-value'); | |
const dataModal = document.getElementById('data-modal'); | |
const closeDataModalBtn = document.getElementById('close-data-modal'); | |
const dataModalTitle = document.getElementById('data-modal-title'); | |
const dataModalContent = document.getElementById('data-modal-content'); | |
const dataTextarea = document.getElementById('data-textarea'); | |
const confirmDataActionBtn = document.getElementById('confirm-data-action'); | |
const exportBtn = document.getElementById('export-btn'); | |
const importBtn = document.getElementById('import-btn'); | |
const authModal = document.getElementById('auth-modal'); | |
const closeAuthModalBtn = document.getElementById('close-auth-modal'); | |
const authBtn = document.getElementById('auth-btn'); | |
const googleAuthBtn = document.getElementById('google-auth-btn'); | |
const clearSelectionBtn = document.getElementById('clear-selection'); | |
const multiDayColorSelect = document.getElementById('multi-day-color'); | |
const applyMultiDayBtn = document.getElementById('apply-multi-day'); | |
const searchWorkoutsInput = document.getElementById('search-workouts'); | |
const clearSearchBtn = document.getElementById('clear-search'); | |
const userPrefixContainer = document.getElementById('user-prefix-container'); | |
const userPrefixInput = document.getElementById('user-prefix'); | |
// State | |
let currentDate = new Date(); | |
let workouts = JSON.parse(localStorage.getItem('workouts')) || []; | |
let selectedWorkoutId = null; | |
let selectedDays = []; | |
let isExporting = false; | |
let isAuthenticated = false; | |
// Initialize | |
function init() { | |
renderCalendar(); | |
renderWorkouts(); | |
addExercise(); | |
// Set current date in form | |
const today = new Date().toISOString().split('T')[0]; | |
document.getElementById('workout-date').value = today; | |
// Check if user is authenticated (simulated) | |
checkAuthStatus(); | |
} | |
// Calendar Functions | |
function renderCalendar() { | |
calendarGrid.innerHTML = ''; | |
const year = currentDate.getFullYear(); | |
const month = currentDate.getMonth(); | |
currentMonthEl.textContent = new Intl.DateTimeFormat('fr-FR', { | |
month: 'long', | |
year: 'numeric' | |
}).format(currentDate); | |
const firstDay = new Date(year, month, 1); | |
const lastDay = new Date(year, month + 1, 0); | |
const startDay = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1; | |
const totalDays = lastDay.getDate(); | |
// Add empty cells for days before the first day of the month | |
for (let i = 0; i < startDay; i++) { | |
const cell = document.createElement('div'); | |
cell.className = 'h-24 bg-gray-800 rounded-md opacity-50'; | |
calendarGrid.appendChild(cell); | |
} | |
// Add cells for each day of the month | |
for (let i = 1; i <= totalDays; i++) { | |
const cell = document.createElement('div'); | |
cell.className = 'h-24 bg-gray-800 rounded-md p-2 overflow-hidden relative'; | |
const dayDate = new Date(year, month, i); | |
const dayISO = dayDate.toISOString().split('T')[0]; | |
// Check if day has workouts | |
const dayWorkouts = workouts.filter(w => w.date === dayISO); | |
// Check if day is part of a multi-day workout | |
const multiDayWorkout = workouts.find(w => | |
w.multiDayDates && w.multiDayDates.includes(dayISO) | |
); | |
if (multiDayWorkout) { | |
cell.classList.add(multiDayWorkout.multiDayColor); | |
} | |
// Add day number | |
const dayNumber = document.createElement('div'); | |
dayNumber.className = 'text-right font-medium'; | |
dayNumber.textContent = i; | |
cell.appendChild(dayNumber); | |
// Add workout indicators | |
if (dayWorkouts.length > 0) { | |
const workoutIndicator = document.createElement('div'); | |
workoutIndicator.className = 'absolute bottom-1 left-1 right-1 flex space-x-1'; | |
dayWorkouts.forEach(workout => { | |
const dot = document.createElement('div'); | |
dot.className = 'h-2 w-2 rounded-full bg-primary'; | |
workoutIndicator.appendChild(dot); | |
}); | |
cell.appendChild(workoutIndicator); | |
} | |
// Highlight today | |
const today = new Date(); | |
if ( | |
dayDate.getDate() === today.getDate() && | |
dayDate.getMonth() === today.getMonth() && | |
dayDate.getFullYear() === today.getFullYear() | |
) { | |
cell.classList.add('border', 'border-primary'); | |
} | |
// Add click event for selection | |
cell.addEventListener('click', () => { | |
if (selectedDays.includes(dayISO)) { | |
selectedDays = selectedDays.filter(d => d !== dayISO); | |
cell.classList.remove('ring-2', 'ring-primary'); | |
} else { | |
selectedDays.push(dayISO); | |
cell.classList.add('ring-2', 'ring-primary'); | |
} | |
}); | |
calendarGrid.appendChild(cell); | |
} | |
} | |
// Workout Form Functions | |
function addExercise() { | |
const exerciseDiv = document.createElement('div'); | |
exerciseDiv.className = 'exercise bg-gray-700 p-3 rounded-lg'; | |
exerciseDiv.innerHTML = ` | |
<div class="grid grid-cols-1 md:grid-cols-4 gap-3"> | |
<div class="md:col-span-2"> | |
<label class="block text-xs text-gray-400 mb-1">Exercice</label> | |
<input type="text" class="exercise-name w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-primary" placeholder="Ex: Développé couché" required> | |
</div> | |
<div> | |
<label class="block text-xs text-gray-400 mb-1">Séries</label> | |
<input type="number" min="1" class="exercise-sets w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-primary" placeholder="4" required> | |
</div> | |
<div> | |
<label class="block text-xs text-gray-400 mb-1">Charge (kg)</label> | |
<input type="number" min="0" step="0.5" class="exercise-weight w-full bg-gray-600 border border-gray-500 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-primary" placeholder="60" required> | |
</div> | |
</div> | |
<div class="mt-2 flex items-center justify-between"> | |
<div class="flex items-center space-x-3"> | |
<label class="flex items-center space-x-1 cursor-pointer"> | |
<input type="checkbox" class="exercise-unilateral custom-checkbox"> | |
<span class="text-xs text-gray-400">Unilatéral</span> | |
</label> | |
</div> | |
<button type="button" class="remove-exercise text-xs text-red-400 hover:text-red-300 transition"> | |
<i class="fas fa-trash mr-1"></i> Supprimer | |
</button> | |
</div> | |
`; | |
exercisesContainer.appendChild(exerciseDiv); | |
// Add event listener to remove button | |
const removeBtn = exerciseDiv.querySelector('.remove-exercise'); | |
removeBtn.addEventListener('click', () => { | |
if (exercisesContainer.querySelectorAll('.exercise').length > 1) { | |
exerciseDiv.remove(); | |
} else { | |
alert('Vous devez avoir au moins un exercice.'); | |
} | |
}); | |
} | |
// Workout List Functions | |
function renderWorkouts(filter = '') { | |
workoutsList.innerHTML = ''; | |
let filteredWorkouts = workouts; | |
if (filter) { | |
const searchTerm = filter.toLowerCase(); | |
filteredWorkouts = workouts.filter(workout => | |
workout.name.toLowerCase().includes(searchTerm) || | |
workout.exercises.some(ex => ex.name.toLowerCase().includes(searchTerm)) | |
); | |
} | |
if (filteredWorkouts.length === 0) { | |
workoutsList.innerHTML = ` | |
<div class="text-center text-gray-500 py-8"> | |
<i class="fas fa-dumbbell text-4xl mb-2"></i> | |
<p>Aucune séance trouvée</p> | |
</div> | |
`; | |
return; | |
} | |
filteredWorkouts.sort((a, b) => new Date(b.date) - new Date(a.date)); | |
filteredWorkouts.forEach(workout => { | |
const workoutCard = document.createElement('div'); | |
workoutCard.className = 'workout-card bg-gray-800 rounded-lg p-4 hover:bg-gray-700/50 transition cursor-pointer'; | |
workoutCard.dataset.id = workout.id; | |
// Calculate total volume | |
const totalVolume = workout.exercises.reduce((sum, ex) => { | |
return sum + (ex.sets * ex.weight); | |
}, 0); | |
// Format date | |
const workoutDate = new Date(workout.date); | |
const formattedDate = workoutDate.toLocaleDateString('fr-FR', { | |
weekday: 'short', | |
day: 'numeric', | |
month: 'short' | |
}); | |
workoutCard.innerHTML = ` | |
<div class="flex justify-between items-start"> | |
<div> | |
<h3 class="font-medium text-lg">${workout.name}</h3> | |
<p class="text-sm text-gray-400">${formattedDate}</p> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<span class="text-xs bg-primary/20 text-primary px-2 py-1 rounded-full">${workout.duration} min</span> | |
<span class="text-xs bg-green-900/50 text-primary px-2 py-1 rounded-full">${totalVolume} kg</span> | |
</div> | |
</div> | |
<div class="mt-3 flex items-center justify-between"> | |
<div class="flex items-center space-x-2"> | |
<div class="h-2 w-20 bg-gray-600 rounded-full overflow-hidden"> | |
<div class="h-full bg-primary rounded-full" style="width: ${workout.satisfaction}%"></div> | |
</div> | |
<span class="text-xs text-gray-400">${workout.satisfaction}%</span> | |
</div> | |
<div class="flex -space-x-1"> | |
${workout.exercises.slice(0, 3).map(ex => | |
`<div class="h-6 w-6 rounded-full bg-gray-600 flex items-center justify-center text-xs">${ex.name.charAt(0).toUpperCase()}</div>` | |
).join('')} | |
${workout.exercises.length > 3 ? | |
`<div class="h-6 w-6 rounded-full bg-gray-600 flex items-center justify-center text-xs">+${workout.exercises.length - 3}</div>` : | |
'' | |
} | |
</div> | |
</div> | |
`; | |
workoutCard.addEventListener('click', () => showWorkoutDetails(workout.id)); | |
workoutsList.appendChild(workoutCard); | |
}); | |
} | |
function showWorkoutDetails(workoutId) { | |
const workout = workouts.find(w => w.id === workoutId); | |
if (!workout) return; | |
selectedWorkoutId = workoutId; | |
// Format date | |
const workoutDate = new Date(workout.date); | |
const formattedDate = workoutDate.toLocaleDateString('fr-FR', { | |
weekday: 'long', | |
day: 'numeric', | |
month: 'long', | |
year: 'numeric' | |
}); | |
// Calculate total volume | |
const totalVolume = workout.exercises.reduce((sum, ex) => { | |
return sum + (ex.sets * ex.weight); | |
}, 0); | |
// Create modal content | |
modalTitle.textContent = workout.name; | |
let exercisesHtml = ''; | |
workout.exercises.forEach(ex => { | |
exercisesHtml += ` | |
<div class="bg-gray-700 p-3 rounded-lg"> | |
<div class="flex justify-between items-center mb-1"> | |
<h4 class="font-medium">${ex.name}</h4> | |
<div class="flex items-center space-x-2"> | |
${ex.unilateral ? | |
'<span class="text-xs bg-blue-900/50 text-blue-300 px-2 py-0.5 rounded-full">Unilatéral</span>' : | |
'' | |
} | |
${workout.intensityDrop ? | |
'<span class="text-xs bg-purple-900/50 text-purple-300 px-2 py-0.5 rounded-full">Série dégressive</span>' : | |
'' | |
} | |
</div> | |
</div> | |
<div class="flex justify-between text-sm text-gray-400"> | |
<span>${ex.sets} séries</span> | |
<span>${ex.weight} kg</span> | |
</div> | |
</div> | |
`; | |
}); | |
modalContent.innerHTML = ` | |
<div class="space-y-4"> | |
<div> | |
<p class="text-gray-400">${formattedDate}</p> | |
<p class="text-gray-400">Durée: ${workout.duration} minutes</p> | |
</div> | |
<div class="flex items-center justify-between"> | |
<div> | |
<p class="text-sm text-gray-400">Satisfaction</p> | |
<div class="flex items-center space-x-2"> | |
<div class="h-2 w-32 bg-gray-600 rounded-full overflow-hidden"> | |
<div class="h-full bg-primary rounded-full" style="width: ${workout.satisfaction}%"></div> | |
</div> | |
<span class="text-sm text-primary">${workout.satisfaction}%</span> | |
</div> | |
</div> | |
<div class="text-right"> | |
<p class="text-sm text-gray-400">Tonnage total</p> | |
<p class="text-primary font-medium">${totalVolume} kg</p> | |
</div> | |
</div> | |
<div class="pt-2"> | |
<h4 class="font-medium text-gray-400 mb-2">Exercices (${workout.exercises.length})</h4> | |
<div class="space-y-2"> | |
${exercisesHtml} | |
</div> | |
</div> | |
${workout.multiDayDates && workout.multiDayDates.length > 1 ? | |
`<div class="pt-2"> | |
<h4 class="font-medium text-gray-400 mb-2">Séance sur plusieurs jours</h4> | |
<div class="flex flex-wrap gap-1"> | |
${workout.multiDayDates.map(date => { | |
const d = new Date(date); | |
return `<span class="text-xs bg-gray-600 px-2 py-1 rounded-full">${d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}</span>`; | |
}).join('')} | |
</div> | |
</div>` : | |
'' | |
} | |
</div> | |
`; | |
workoutModal.classList.remove('hidden'); | |
} | |
// Data Functions | |
function saveWorkout(workoutData) { | |
// Generate ID if new workout | |
if (!workoutData.id) { | |
workoutData.id = Date.now().toString(); | |
workouts.push(workoutData); | |
} else { | |
// Update existing workout | |
const index = workouts.findIndex(w => w.id === workoutData.id); | |
if (index !== -1) { | |
workouts[index] = workoutData; | |
} | |
} | |
localStorage.setItem('workouts', JSON.stringify(workouts)); | |
renderCalendar(); | |
renderWorkouts(); | |
} | |
function deleteWorkout(workoutId) { | |
workouts = workouts.filter(w => w.id !== workoutId); | |
localStorage.setItem('workouts', JSON.stringify(workouts)); | |
renderCalendar(); | |
renderWorkouts(); | |
} | |
function exportData() { | |
isExporting = true; | |
dataModalTitle.textContent = 'Exporter les données'; | |
dataTextarea.value = JSON.stringify(workouts, null, 2); | |
userPrefixContainer.classList.add('hidden'); | |
dataModal.classList.remove('hidden'); | |
} | |
function importData() { | |
isExporting = false; | |
dataModalTitle.textContent = 'Importer des données'; | |
dataTextarea.value = ''; | |
userPrefixContainer.classList.remove('hidden'); | |
dataModal.classList.remove('hidden'); | |
} | |
function confirmDataAction() { | |
if (isExporting) { | |
// Export logic (already handled by textarea value) | |
dataModal.classList.add('hidden'); | |
} else { | |
// Import logic | |
try { | |
const importedWorkouts = JSON.parse(dataTextarea.value); | |
const prefix = userPrefixInput.value.trim(); | |
if (prefix) { | |
importedWorkouts.forEach(workout => { | |
workout.name = `${prefix} - ${workout.name}`; | |
}); | |
} | |
// Merge with existing workouts | |
workouts = [...workouts, ...importedWorkouts]; | |
localStorage.setItem('workouts', JSON.stringify(workouts)); | |
renderCalendar(); | |
renderWorkouts(); | |
dataModal.classList.add('hidden'); | |
} catch (e) { | |
alert('Données JSON invalides'); | |
console.error(e); | |
} | |
} | |
} | |
// Auth Functions (simulated) | |
function checkAuthStatus() { | |
// In a real app, this would check Firebase auth state | |
isAuthenticated = localStorage.getItem('isAuthenticated') === 'true'; | |
updateAuthUI(); | |
} | |
function updateAuthUI() { | |
if (isAuthenticated) { | |
authBtn.innerHTML = '<i class="fas fa-sign-out-alt mr-2"></i>Déconnexion'; | |
} else { | |
authBtn.innerHTML = '<i class="fas fa-sign-in-alt mr-2"></i>Connexion'; | |
} | |
} | |
function toggleAuth() { | |
if (isAuthenticated) { | |
// Sign out | |
isAuthenticated = false; | |
localStorage.removeItem('isAuthenticated'); | |
} else { | |
// Show auth modal | |
authModal.classList.remove('hidden'); | |
} | |
updateAuthUI(); | |
} | |
// Event Listeners | |
prevMonthBtn.addEventListener('click', () => { | |
currentDate.setMonth(currentDate.getMonth() - 1); | |
renderCalendar(); | |
}); | |
nextMonthBtn.addEventListener('click', () => { | |
currentDate.setMonth(currentDate.getMonth() + 1); | |
renderCalendar(); | |
}); | |
addExerciseBtn.addEventListener('click', addExercise); | |
workoutForm.addEventListener('submit', (e) => { | |
e.preventDefault(); | |
const workoutName = document.getElementById('workout-name').value; | |
const workoutDate = document.getElementById('workout-date').value; | |
const workoutDuration = document.getElementById('workout-duration').value; | |
const workoutSatisfaction = document.getElementById('workout-satisfaction').value; | |
const intensityDrop = document.getElementById('intensity-drop').checked; | |
// Get exercises | |
const exercises = []; | |
const exerciseElements = document.querySelectorAll('.exercise'); | |
exerciseElements.forEach(exEl => { | |
exercises.push({ | |
name: exEl.querySelector('.exercise-name').value, | |
sets: parseInt(exEl.querySelector('.exercise-sets').value), | |
weight: parseFloat(exEl.querySelector('.exercise-weight').value), | |
unilateral: exEl.querySelector('.exercise-unilateral').checked | |
}); | |
}); | |
// Create workout object | |
const workoutData = { | |
id: selectedWorkoutId, | |
name: workoutName, | |
date: workoutDate, | |
duration: workoutDuration, | |
satisfaction: workoutSatisfaction, | |
intensityDrop: intensityDrop, | |
exercises: exercises, | |
multiDayDates: selectedDays.length > 0 ? selectedDays : [workoutDate], | |
multiDayColor: selectedDays.length > 0 ? multiDayColorSelect.value : '' | |
}; | |
saveWorkout(workoutData); | |
// Reset form | |
workoutForm.reset(); | |
exercisesContainer.innerHTML = ''; | |
addExercise(); | |
selectedDays = []; | |
selectedWorkoutId = null; | |
// Set current date | |
document.getElementById('workout-date').value = new Date().toISOString().split('T')[0]; | |
}); | |
satisfactionSlider.addEventListener('input', () => { | |
satisfactionValue.textContent = `${satisfactionSlider.value}%`; | |
}); | |
closeModalBtn.addEventListener('click', () => { | |
workoutModal.classList.add('hidden'); | |
}); | |
deleteWorkoutBtn.addEventListener('click', () => { | |
if (confirm('Supprimer cette séance ?')) { | |
deleteWorkout(selectedWorkoutId); | |
workoutModal.classList.add('hidden'); | |
} | |
}); | |
editWorkoutBtn.addEventListener('click', () => { | |
const workout = workouts.find(w => w.id === selectedWorkoutId); | |
if (!workout) return; | |
// Fill form with workout data | |
document.getElementById('workout-name').value = workout.name; | |
document.getElementById('workout-date').value = workout.date; | |
document.getElementById('workout-duration').value = workout.duration; | |
document.getElementById('workout-satisfaction').value = workout.satisfaction; | |
satisfactionValue.textContent = `${workout.satisfaction}%`; | |
document.getElementById('intensity-drop').checked = workout.intensityDrop; | |
// Clear exercises and add current ones | |
exercisesContainer.innerHTML = ''; | |
workout.exercises.forEach(ex => { | |
addExercise(); | |
const lastExercise = exercisesContainer.lastElementChild; | |
lastExercise.querySelector('.exercise-name').value = ex.name; | |
lastExercise.querySelector('.exercise-sets').value = ex.sets; | |
lastExercise.querySelector('.exercise-weight').value = ex.weight; | |
lastExercise.querySelector('.exercise-unilateral').checked = ex.unilateral; | |
}); | |
// Set multi-day selection if applicable | |
if (workout.multiDayDates && workout.multiDayDates.length > 1) { | |
selectedDays = workout.multiDayDates; | |
multiDayColorSelect.value = workout.multiDayColor || 'bg-green-500/30'; | |
} | |
workoutModal.classList.add('hidden'); | |
}); | |
exportBtn.addEventListener('click', exportData); | |
importBtn.addEventListener('click', importData); | |
closeDataModalBtn.addEventListener('click', () => dataModal.classList.add('hidden')); | |
confirmDataActionBtn.addEventListener('click', confirmDataAction); | |
authBtn.addEventListener('click', toggleAuth); | |
closeAuthModalBtn.addEventListener('click', () => authModal.classList.add('hidden')); | |
googleAuthBtn.addEventListener('click', () => { | |
// Simulate successful auth | |
isAuthenticated = true; | |
localStorage.setItem('isAuthenticated', 'true'); | |
updateAuthUI(); | |
authModal.classList.add('hidden'); | |
}); | |
clearSelectionBtn.addEventListener('click', () => { | |
selectedDays = []; | |
const selectedCells = document.querySelectorAll('.ring-2.ring-primary'); | |
selectedCells.forEach(cell => cell.classList.remove('ring-2', 'ring-primary')); | |
}); | |
applyMultiDayBtn.addEventListener('click', () => { | |
if (selectedDays.length === 0) { | |
alert('Sélectionnez au moins un jour'); | |
return; | |
} | |
// Just update the selection UI, actual multi-day will be handled on form submit | |
alert(`${selectedDays.length} jours sélectionnés pour une séance sur plusieurs jours`); | |
}); | |
searchWorkoutsInput.addEventListener('input', (e) => { | |
renderWorkouts(e.target.value); | |
}); | |
clearSearchBtn.addEventListener('click', () => { | |
searchWorkoutsInput.value = ''; | |
renderWorkouts(); | |
}); | |
// Initialize the app | |
init(); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=johanpautrel/calendrier-interactif" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |