calendrier-interactif / index.html
PAUTREL Johan
Add 1 files
35f6ce7 verified
<!DOCTYPE html>
<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>