PAUTREL Johan commited on
Commit
35f6ce7
·
verified ·
1 Parent(s): 2b05c91

Add 1 files

Browse files
Files changed (1) hide show
  1. index.html +777 -962
index.html CHANGED
@@ -3,1111 +3,926 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Calendrier Premium 2025-2030</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
- <script src="https://accounts.google.com/gsi/client" async defer></script>
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
- <style>
11
- .calendar-day {
12
- min-height: 120px;
13
- transition: all 0.2s ease;
14
- }
15
- .calendar-day:hover {
16
- transform: translateY(-2px);
17
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
18
- }
19
- .task-item {
20
- transition: all 0.2s ease;
21
- }
22
- .task-item:hover {
23
- transform: translateX(2px);
24
- }
25
- .multi-day-selected {
26
- background-color: #e0f2fe !important;
27
- }
28
- .multi-day-start {
29
- border-top-left-radius: 0.5rem;
30
- border-bottom-left-radius: 0.5rem;
31
- }
32
- .multi-day-end {
33
- border-top-right-radius: 0.5rem;
34
- border-bottom-right-radius: 0.5rem;
35
- }
36
- .multi-day-middle {
37
- border-radius: 0;
38
- }
39
- .week-view-day {
40
- min-height: 150px;
41
  }
42
- .color-picker {
43
- width: 24px;
44
- height: 24px;
45
- border-radius: 50%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  cursor: pointer;
47
- border: 2px solid transparent;
48
- }
49
- .color-picker.selected {
50
- border-color: #000;
51
- }
52
- @media (max-width: 768px) {
53
- .calendar-day {
54
- min-height: 100px;
55
- }
56
- .week-view-day {
57
- min-height: 120px;
58
- }
 
 
 
 
 
 
 
 
59
  }
60
  </style>
61
  </head>
62
- <body class="bg-blue-50 min-h-screen">
63
  <div class="container mx-auto px-4 py-8">
64
  <!-- Header -->
65
- <header class="flex flex-col md:flex-row justify-between items-center mb-8">
66
- <h1 class="text-3xl font-bold text-blue-800 mb-4 md:mb-0">Calendrier Premium</h1>
67
- <div id="auth-section" class="flex items-center space-x-4">
68
- <div id="user-info" class="hidden items-center">
69
- <img id="user-pic" class="w-10 h-10 rounded-full mr-2" src="" alt="Profile">
70
- <span id="user-name" class="text-blue-800 font-medium"></span>
71
- </div>
72
- <div id="g_id_onload"
73
- data-client_id="YOUR_GOOGLE_CLIENT_ID"
74
- data-context="signin"
75
- data-ux_mode="popup"
76
- data-callback="handleGoogleSignIn"
77
- data-auto_prompt="false">
78
- </div>
79
- <div class="g_id_signin"
80
- data-type="standard"
81
- data-shape="pill"
82
- data-theme="filled_blue"
83
- data-text="signin_with"
84
- data-size="medium"
85
- data-logo_alignment="left">
86
- </div>
87
- <button id="sign-out" class="hidden bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-full text-sm font-medium transition">
88
- Déconnexion
89
  </button>
90
  </div>
91
  </header>
92
 
93
- <!-- Controls -->
94
- <div class="bg-white rounded-xl shadow-md p-6 mb-8">
95
- <div class="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
96
- <div class="flex items-center space-x-4">
97
- <button id="prev-period" class="bg-blue-100 hover:bg-blue-200 text-blue-800 p-2 rounded-full transition">
98
- <i class="fas fa-chevron-left"></i>
99
- </button>
100
- <h2 id="current-period" class="text-2xl font-bold text-blue-800">Janvier 2025</h2>
101
- <button id="next-period" class="bg-blue-100 hover:bg-blue-200 text-blue-800 p-2 rounded-full transition">
102
- <i class="fas fa-chevron-right"></i>
103
- </button>
104
- <button id="today-btn" class="bg-blue-100 hover:bg-blue-200 text-blue-800 px-3 py-1 rounded-full text-sm transition">
105
- Aujourd'hui
106
- </button>
107
- </div>
108
- <div class="flex items-center space-x-2">
109
- <div class="flex space-x-1 mr-4">
110
- <button id="month-view-btn" class="bg-blue-600 text-white px-3 py-1 rounded-l-lg text-sm transition">
111
- Mois
112
  </button>
113
- <button id="week-view-btn" class="bg-blue-100 hover:bg-blue-200 text-blue-800 px-3 py-1 rounded-r-lg text-sm transition">
114
- Semaine
 
115
  </button>
116
  </div>
117
- <button id="export-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition flex items-center">
118
- <i class="fas fa-file-export mr-2"></i> Exporter
119
- </button>
120
- <label for="import-file" class="bg-blue-100 hover:bg-blue-200 text-blue-800 px-4 py-2 rounded-lg text-sm font-medium transition flex items-center cursor-pointer">
121
- <i class="fas fa-file-import mr-2"></i> Importer
122
- <input id="import-file" type="file" accept=".json" class="hidden">
123
- </label>
124
- <button id="save-drive-btn" class="hidden bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition flex items-center">
125
- <i class="fab fa-google-drive mr-2"></i> Sauvegarder
126
- </button>
127
  </div>
128
- </div>
129
- </div>
130
-
131
- <!-- Search Bar -->
132
- <div class="bg-white rounded-xl shadow-md p-4 mb-8">
133
- <div class="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-4">
134
- <div class="relative w-full md:w-96">
135
- <input type="text" id="search-input" placeholder="Rechercher des tâches..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
136
- <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
 
137
  </div>
138
- <div class="flex items-center space-x-2">
139
- <label for="search-category" class="text-sm text-gray-600">Catégorie:</label>
140
- <select id="search-category" class="border border-gray-300 rounded-lg px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
141
- <option value="all">Toutes</option>
142
- <option value="work">Travail</option>
143
- <option value="personal">Personnel</option>
144
- <option value="family">Famille</option>
145
- <option value="other">Autre</option>
146
- </select>
147
  </div>
148
- <button id="search-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition">
149
- Rechercher
150
- </button>
151
- <button id="clear-search" class="text-gray-600 hover:text-gray-800 text-sm font-medium transition">
152
- Effacer
153
- </button>
154
- </div>
155
- </div>
156
-
157
- <!-- Calendar Views -->
158
- <div id="month-view" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
159
- <!-- Months will be inserted here by JavaScript -->
160
- </div>
161
-
162
- <div id="week-view" class="hidden bg-white rounded-xl shadow-md overflow-hidden">
163
- <div class="grid grid-cols-7 gap-px bg-gray-200 p-px">
164
- <!-- Week days header -->
165
- ${['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'].map(day => `
166
- <div class="bg-blue-100 text-blue-800 text-center font-medium py-2">
167
- ${day}
 
 
168
  </div>
169
- `).join('')}
170
- </div>
171
- <div class="grid grid-cols-7 gap-px bg-gray-200 p-px" id="week-days">
172
- <!-- Week days content will be inserted here -->
173
- </div>
174
- </div>
175
-
176
- <!-- Task Modal -->
177
- <div id="task-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
178
- <div class="bg-white rounded-xl shadow-xl p-6 w-full max-w-md">
179
- <div class="flex justify-between items-center mb-4">
180
- <h3 class="text-xl font-bold text-blue-800" id="modal-date">Ajouter une tâche</h3>
181
- <button id="close-modal" class="text-gray-500 hover:text-gray-700">
182
- <i class="fas fa-times"></i>
183
- </button>
184
  </div>
185
- <div class="space-y-4">
 
 
 
 
 
 
186
  <div>
187
- <label for="task-text" class="block text-sm font-medium text-gray-700 mb-1">Tâche</label>
188
- <input type="text" id="task-text" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
189
  </div>
 
190
  <div>
191
- <label for="task-time" class="block text-sm font-medium text-gray-700 mb-1">Heure (optionnel)</label>
192
- <input type="time" id="task-time" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
193
  </div>
 
194
  <div>
195
- <label class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label>
196
- <div class="flex space-x-2">
197
- <div class="color-picker bg-red-200 selected" data-color="red" data-category="work"></div>
198
- <div class="color-picker bg-blue-200" data-color="blue" data-category="personal"></div>
199
- <div class="color-picker bg-green-200" data-color="green" data-category="family"></div>
200
- <div class="color-picker bg-yellow-200" data-color="yellow" data-category="other"></div>
201
- <div class="color-picker bg-purple-200" data-color="purple" data-category="event"></div>
 
 
202
  </div>
203
  </div>
204
- <div class="flex justify-end space-x-3 pt-2">
205
- <button id="cancel-task" class="px-4 py-2 text-gray-600 hover:text-gray-800 font-medium rounded-lg transition">
206
- Annuler
207
- </button>
208
- <button id="save-task" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition">
209
- Ajouter
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  </button>
211
  </div>
212
- </div>
213
  </div>
214
  </div>
215
-
216
- <!-- Search Results Modal -->
217
- <div id="search-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
218
- <div class="bg-white rounded-xl shadow-xl p-6 w-full max-w-2xl max-h-[80vh] overflow-y-auto">
219
- <div class="flex justify-between items-center mb-4">
220
- <h3 class="text-xl font-bold text-blue-800">Résultats de recherche</h3>
221
- <button id="close-search-modal" class="text-gray-500 hover:text-gray-700">
 
222
  <i class="fas fa-times"></i>
223
  </button>
224
  </div>
225
- <div id="search-results" class="space-y-3">
226
- <!-- Search results will be inserted here -->
 
 
 
 
 
227
  </div>
228
  </div>
229
  </div>
230
-
231
- <!-- Multi-day selection info -->
232
- <div id="multi-day-info" class="fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-white shadow-lg rounded-lg px-4 py-2 hidden">
233
- <span class="text-blue-800 font-medium">Sélection multiple activée - Sélectionnez plusieurs jours</span>
234
- <button id="cancel-multi-day" class="ml-4 text-blue-600 hover:text-blue-800">
235
- <i class="fas fa-times"></i> Annuler
236
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  </div>
238
  </div>
239
 
240
  <script>
241
- // Global variables
242
- let currentYear = 2025;
243
- let currentMonth = 0; // January
244
- let currentWeek = 0; // First week of the year
245
- let calendarData = {};
246
- let selectedDate = null;
247
- let multiDaySelection = false;
248
- let multiDayStart = null;
249
- let multiDayEnd = null;
250
- let currentUser = null;
251
- let isDragging = false;
252
- let currentView = 'month'; // 'month' or 'week'
253
- let selectedColor = 'red';
254
- let selectedCategory = 'work';
255
-
256
- // Initialize the calendar
257
- document.addEventListener('DOMContentLoaded', function() {
258
- initializeCalendar();
259
- setupEventListeners();
260
- loadFromLocalStorage();
261
- updateCurrentPeriodDisplay();
262
- setupColorPickers();
263
- });
264
-
265
- function initializeCalendar() {
266
- // Initialize empty calendar data structure
267
- for (let year = 2025; year <= 2030; year++) {
268
- calendarData[year] = {};
269
-
270
- for (let month = 0; month < 12; month++) {
271
- calendarData[year][month] = {};
272
-
273
- const daysInMonth = new Date(year, month + 1, 0).getDate();
274
- for (let day = 1; day <= daysInMonth; day++) {
275
- calendarData[year][month][day] = [];
276
- }
277
- }
278
- }
279
-
 
 
 
 
 
 
 
 
280
  renderCalendar();
281
- }
282
-
283
- function renderCalendar() {
284
- if (currentView === 'month') {
285
- renderMonthView();
286
- } else {
287
- renderWeekView();
288
- }
289
- }
290
-
291
- function renderMonthView() {
292
- const container = document.getElementById('month-view');
293
- container.innerHTML = '';
294
-
295
- for (let month = 0; month < 12; month++) {
296
- const monthElement = document.createElement('div');
297
- monthElement.className = 'bg-white rounded-xl shadow-md overflow-hidden';
298
-
299
- const monthName = new Date(currentYear, month, 1).toLocaleString('fr-FR', { month: 'long' });
300
- const capitalizedMonthName = monthName.charAt(0).toUpperCase() + monthName.slice(1);
301
-
302
- monthElement.innerHTML = `
303
- <div class="bg-blue-600 text-white px-4 py-3 font-bold">
304
- ${capitalizedMonthName} ${currentYear}
305
- </div>
306
- <div class="grid grid-cols-7 gap-px bg-gray-200 p-px">
307
- ${['Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa', 'Di'].map(day => `
308
- <div class="bg-blue-100 text-blue-800 text-center text-sm font-medium py-1">
309
- ${day}
310
- </div>
311
- `).join('')}
312
- </div>
313
- <div class="grid grid-cols-7 gap-px bg-gray-200 p-px" id="month-${month}">
314
- <!-- Days will be inserted here -->
315
- </div>
316
- `;
317
-
318
- container.appendChild(monthElement);
319
- renderMonthDays(month);
320
- }
321
- }
322
-
323
- function renderWeekView() {
324
- const weekContainer = document.getElementById('week-days');
325
- weekContainer.innerHTML = '';
326
 
327
- // Get the first day of the week
328
- const firstDayOfWeek = getFirstDayOfWeek(currentYear, currentWeek);
 
329
 
330
- // Render each day of the week
331
- for (let i = 0; i < 7; i++) {
332
- const dayDate = new Date(firstDayOfWeek);
333
- dayDate.setDate(firstDayOfWeek.getDate() + i);
334
-
335
- const year = dayDate.getFullYear();
336
- const month = dayDate.getMonth();
337
- const day = dayDate.getDate();
338
- const dayOfWeek = dayDate.getDay();
339
- const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
340
-
341
- const dayElement = document.createElement('div');
342
- dayElement.className = `week-view-day bg-white ${isWeekend ? 'bg-blue-50' : ''} p-2 relative`;
343
- dayElement.dataset.year = year;
344
- dayElement.dataset.month = month;
345
- dayElement.dataset.day = day;
346
-
347
- const dayName = dayDate.toLocaleString('fr-FR', { weekday: 'short' });
348
- const formattedDate = dayDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
349
-
350
- dayElement.innerHTML = `
351
- <div class="flex justify-between items-start mb-1">
352
- <div>
353
- <span class="text-sm font-medium text-gray-500">${dayName}</span>
354
- <span class="text-lg font-medium ${isWeekend ? 'text-blue-600' : 'text-gray-800'} ml-2">${formattedDate}</span>
355
- </div>
356
- <button class="add-task-btn text-blue-500 hover:text-blue-700 text-sm p-1 rounded-full transition">
357
- <i class="fas fa-plus"></i>
358
- </button>
359
- </div>
360
- <div class="tasks-container space-y-1 overflow-y-auto max-h-32">
361
- ${renderTasks(year, month, day)}
362
- </div>
363
- `;
364
-
365
- weekContainer.appendChild(dayElement);
366
- }
367
  }
368
 
369
- function getFirstDayOfWeek(year, week) {
370
- const date = new Date(year, 0, 1);
371
- const dayOfWeek = date.getDay();
372
- const firstDay = new Date(date);
373
 
374
- // Adjust for Monday as first day of week
375
- const diff = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
376
- firstDay.setDate(date.getDate() - diff);
377
 
378
- // Add the weeks
379
- firstDay.setDate(firstDay.getDate() + (week * 7));
 
 
380
 
381
- return firstDay;
382
- }
383
-
384
- function renderMonthDays(month) {
385
- const monthContainer = document.getElementById(`month-${month}`);
386
- const firstDay = new Date(currentYear, month, 1).getDay();
387
- const daysInMonth = new Date(currentYear, month + 1, 0).getDate();
388
 
389
- // Adjust for Monday as first day (French calendar)
390
- const adjustedFirstDay = firstDay === 0 ? 6 : firstDay - 1;
391
 
392
  // Add empty cells for days before the first day of the month
393
- for (let i = 0; i < adjustedFirstDay; i++) {
394
- const emptyCell = document.createElement('div');
395
- emptyCell.className = 'bg-gray-100 h-12';
396
- monthContainer.appendChild(emptyCell);
397
  }
398
 
399
  // Add cells for each day of the month
400
- for (let day = 1; day <= daysInMonth; day++) {
401
- const date = new Date(currentYear, month, day);
402
- const dayOfWeek = date.getDay();
403
- const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
404
 
405
- const dayElement = document.createElement('div');
406
- dayElement.className = `calendar-day bg-white ${isWeekend ? 'bg-blue-50' : ''} p-2 relative`;
407
- dayElement.dataset.year = currentYear;
408
- dayElement.dataset.month = month;
409
- dayElement.dataset.day = day;
410
 
411
- // Check if this day is today
412
- const today = new Date();
413
- if (date.getFullYear() === today.getFullYear() &&
414
- date.getMonth() === today.getMonth() &&
415
- date.getDate() === today.getDate()) {
416
- dayElement.classList.add('border-2', 'border-blue-500');
 
 
 
 
417
  }
418
 
419
- // Check if this day is part of a multi-day selection
420
- const isMultiDay = checkMultiDaySelection(currentYear, month, day);
421
- if (isMultiDay) {
422
- dayElement.classList.add('multi-day-selected', isMultiDay.position);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  }
424
 
425
- dayElement.innerHTML = `
426
- <div class="flex justify-between items-start mb-1">
427
- <span class="text-lg font-medium ${isWeekend ? 'text-blue-600' : 'text-gray-800'}">${day}</span>
428
- <button class="add-task-btn text-blue-500 hover:text-blue-700 text-sm p-1 rounded-full transition">
429
- <i class="fas fa-plus"></i>
430
- </button>
431
- </div>
432
- <div class="tasks-container space-y-1 overflow-y-auto max-h-20">
433
- ${renderTasks(currentYear, month, day)}
434
- </div>
435
- `;
 
 
 
 
 
 
 
 
 
436
 
437
- monthContainer.appendChild(dayElement);
438
  }
439
  }
440
 
441
- function renderTasks(year, month, day) {
442
- if (!calendarData[year] || !calendarData[year][month] || !calendarData[year][month][day]) {
443
- return '';
444
- }
445
 
446
- return calendarData[year][month][day].map((task, index) => {
447
- const colorClass = getColorClass(task.color);
448
- return `
449
- <div class="task-item ${colorClass} rounded px-2 py-1 text-sm flex justify-between items-center" data-index="${index}">
 
 
 
 
 
 
450
  <div>
451
- ${task.time ? `<span class="font-medium">${task.time}</span> - ` : ''}
452
- <span>${task.text}</span>
453
- ${task.user ? `<span class="text-xs text-gray-500 ml-1">(${task.user})</span>` : ''}
454
  </div>
455
- <button class="delete-task text-gray-600 hover:text-gray-800 text-xs p-1">
456
- <i class="fas fa-trash-alt"></i>
 
 
 
 
 
 
 
 
457
  </button>
458
  </div>
459
- `;
460
- }).join('');
461
- }
462
-
463
- function getColorClass(color) {
464
- switch(color) {
465
- case 'red': return 'bg-red-200';
466
- case 'blue': return 'bg-blue-200';
467
- case 'green': return 'bg-green-200';
468
- case 'yellow': return 'bg-yellow-200';
469
- case 'purple': return 'bg-purple-200';
470
- default: return 'bg-blue-100';
471
- }
472
- }
473
-
474
- function setupEventListeners() {
475
- // Period navigation
476
- document.getElementById('prev-period').addEventListener('click', () => {
477
- if (currentView === 'month') {
478
- if (currentYear > 2025 || (currentYear === 2025 && currentMonth > 0)) {
479
- if (currentMonth === 0) {
480
- currentYear--;
481
- currentMonth = 11;
482
- } else {
483
- currentMonth--;
484
- }
485
- updateCurrentPeriodDisplay();
486
- renderCalendar();
487
- }
488
- } else {
489
- if (currentWeek > 0) {
490
- currentWeek--;
491
- } else {
492
- currentYear--;
493
- currentWeek = getWeeksInYear(currentYear) - 1;
494
- }
495
- updateCurrentPeriodDisplay();
496
- renderCalendar();
497
- }
498
- });
499
 
500
- document.getElementById('next-period').addEventListener('click', () => {
501
- if (currentView === 'month') {
502
- if (currentYear < 2030 || (currentYear === 2030 && currentMonth < 11)) {
503
- if (currentMonth === 11) {
504
- currentYear++;
505
- currentMonth = 0;
506
- } else {
507
- currentMonth++;
508
- }
509
- updateCurrentPeriodDisplay();
510
- renderCalendar();
511
- }
512
  } else {
513
- const weeksInYear = getWeeksInYear(currentYear);
514
- if (currentWeek < weeksInYear - 1) {
515
- currentWeek++;
516
- } else {
517
- currentYear++;
518
- currentWeek = 0;
519
- }
520
- updateCurrentPeriodDisplay();
521
- renderCalendar();
522
  }
523
  });
 
 
 
 
 
524
 
525
- document.getElementById('today-btn').addEventListener('click', () => {
526
- const today = new Date();
527
- currentYear = today.getFullYear();
528
- currentMonth = today.getMonth();
529
-
530
- // Calculate current week for week view
531
- const firstDayOfYear = new Date(currentYear, 0, 1);
532
- const pastDaysOfYear = (today - firstDayOfYear) / 86400000;
533
- currentWeek = Math.floor((pastDaysOfYear + firstDayOfYear.getDay() - 1) / 7);
534
-
535
- updateCurrentPeriodDisplay();
536
- renderCalendar();
537
- });
538
-
539
- // View switching
540
- document.getElementById('month-view-btn').addEventListener('click', () => {
541
- if (currentView !== 'month') {
542
- currentView = 'month';
543
- document.getElementById('month-view-btn').classList.remove('bg-blue-100', 'text-blue-800');
544
- document.getElementById('month-view-btn').classList.add('bg-blue-600', 'text-white');
545
- document.getElementById('week-view-btn').classList.remove('bg-blue-600', 'text-white');
546
- document.getElementById('week-view-btn').classList.add('bg-blue-100', 'text-blue-800');
547
-
548
- document.getElementById('month-view').classList.remove('hidden');
549
- document.getElementById('week-view').classList.add('hidden');
550
-
551
- updateCurrentPeriodDisplay();
552
- renderCalendar();
553
- }
554
- });
555
 
556
- document.getElementById('week-view-btn').addEventListener('click', () => {
557
- if (currentView !== 'week') {
558
- currentView = 'week';
559
- document.getElementById('week-view-btn').classList.remove('bg-blue-100', 'text-blue-800');
560
- document.getElementById('week-view-btn').classList.add('bg-blue-600', 'text-white');
561
- document.getElementById('month-view-btn').classList.remove('bg-blue-600', 'text-white');
562
- document.getElementById('month-view-btn').classList.add('bg-blue-100', 'text-blue-800');
563
-
564
- document.getElementById('month-view').classList.add('hidden');
565
- document.getElementById('week-view').classList.remove('hidden');
566
-
567
- updateCurrentPeriodDisplay();
568
- renderCalendar();
569
- }
570
- });
571
-
572
- // Task modal
573
- document.addEventListener('click', function(e) {
574
- if (e.target.closest('.add-task-btn')) {
575
- const dayElement = e.target.closest('.calendar-day, .week-view-day');
576
- selectedDate = {
577
- year: parseInt(dayElement.dataset.year),
578
- month: parseInt(dayElement.dataset.month),
579
- day: parseInt(dayElement.dataset.day)
580
- };
581
-
582
- const dateStr = new Date(selectedDate.year, selectedDate.month, selectedDate.day)
583
- .toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' });
584
-
585
- document.getElementById('modal-date').textContent = `Tâche pour le ${dateStr}`;
586
- document.getElementById('task-text').value = '';
587
- document.getElementById('task-time').value = '';
588
- document.getElementById('task-modal').classList.remove('hidden');
589
- }
590
-
591
- if (e.target.closest('.delete-task')) {
592
- const taskItem = e.target.closest('.task-item');
593
- const dayElement = e.target.closest('.calendar-day, .week-view-day');
594
- const taskIndex = parseInt(taskItem.dataset.index);
595
-
596
- const year = parseInt(dayElement.dataset.year);
597
- const month = parseInt(dayElement.dataset.month);
598
- const day = parseInt(dayElement.dataset.day);
599
-
600
- calendarData[year][month][day].splice(taskIndex, 1);
601
- saveToLocalStorage();
602
- renderCalendar();
603
- }
604
- });
605
 
606
- // Modal buttons
607
- document.getElementById('close-modal').addEventListener('click', () => {
608
- document.getElementById('task-modal').classList.add('hidden');
609
- });
 
 
 
 
 
610
 
611
- document.getElementById('cancel-task').addEventListener('click', () => {
612
- document.getElementById('task-modal').classList.add('hidden');
613
- });
614
 
615
- document.getElementById('save-task').addEventListener('click', () => {
616
- const taskText = document.getElementById('task-text').value.trim();
617
- const taskTime = document.getElementById('task-time').value;
 
618
 
619
- if (taskText) {
620
- const newTask = {
621
- text: taskText,
622
- time: taskTime || null,
623
- user: currentUser ? currentUser.name : null,
624
- color: selectedColor,
625
- category: selectedCategory
626
- };
627
-
628
- calendarData[selectedDate.year][selectedDate.month][selectedDate.day].push(newTask);
629
- saveToLocalStorage();
630
- renderCalendar();
631
- document.getElementById('task-modal').classList.add('hidden');
632
- }
633
- });
634
-
635
- // Export/Import
636
- document.getElementById('export-btn').addEventListener('click', exportCalendar);
637
-
638
- document.getElementById('import-file').addEventListener('change', function(e) {
639
- const file = e.target.files[0];
640
- if (!file) return;
641
 
642
- const reader = new FileReader();
643
- reader.onload = function(e) {
644
- try {
645
- const importedData = JSON.parse(e.target.result);
646
- mergeCalendarData(importedData);
647
- saveToLocalStorage();
648
- renderCalendar();
649
- alert('Données importées avec succès!');
650
- } catch (error) {
651
- alert('Erreur lors de l\'importation: ' + error.message);
652
- }
653
- };
654
- reader.readAsText(file);
655
- e.target.value = ''; // Reset file input
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  });
 
 
 
 
 
657
 
658
- // Multi-day selection
659
- document.addEventListener('mousedown', handleDayMouseDown);
660
- document.addEventListener('mousemove', handleDayMouseMove);
661
- document.addEventListener('mouseup', handleDayMouseUp);
662
-
663
- document.addEventListener('touchstart', handleDayTouchStart, { passive: false });
664
- document.addEventListener('touchmove', handleDayTouchMove, { passive: false });
665
- document.addEventListener('touchend', handleDayTouchEnd);
666
-
667
- document.getElementById('cancel-multi-day').addEventListener('click', cancelMultiDaySelection);
668
-
669
- // Enable multi-day selection mode
670
- document.addEventListener('keydown', function(e) {
671
- if (e.key === 'Shift' && !multiDaySelection) {
672
- startMultiDaySelection();
673
- }
674
- });
675
 
676
- document.addEventListener('keyup', function(e) {
677
- if (e.key === 'Shift' && multiDaySelection) {
678
- endMultiDaySelection();
679
- }
 
 
 
680
  });
681
 
682
- // Search functionality
683
- document.getElementById('search-btn').addEventListener('click', performSearch);
684
- document.getElementById('clear-search').addEventListener('click', clearSearch);
685
- document.getElementById('close-search-modal').addEventListener('click', () => {
686
- document.getElementById('search-modal').classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
  });
688
 
689
- // Allow pressing Enter in search input
690
- document.getElementById('search-input').addEventListener('keypress', function(e) {
691
- if (e.key === 'Enter') {
692
- performSearch();
693
- }
694
- });
695
- }
696
-
697
- function setupColorPickers() {
698
- const colorPickers = document.querySelectorAll('.color-picker');
699
- colorPickers.forEach(picker => {
700
- picker.addEventListener('click', function() {
701
- // Remove selected class from all pickers
702
- document.querySelectorAll('.color-picker').forEach(p => {
703
- p.classList.remove('selected');
704
- });
 
 
 
 
 
 
705
 
706
- // Add selected class to clicked picker
707
- this.classList.add('selected');
 
 
 
 
708
 
709
- // Update selected color and category
710
- selectedColor = this.dataset.color;
711
- selectedCategory = this.dataset.category;
712
- });
713
- });
 
 
 
 
 
 
 
 
 
 
 
714
  }
715
 
716
- function updateCurrentPeriodDisplay() {
717
- const periodElement = document.getElementById('current-period');
718
-
719
- if (currentView === 'month') {
720
- const monthName = new Date(currentYear, currentMonth, 1).toLocaleString('fr-FR', { month: 'long' });
721
- const capitalizedMonthName = monthName.charAt(0).toUpperCase() + monthName.slice(1);
722
- periodElement.textContent = `${capitalizedMonthName} ${currentYear}`;
723
  } else {
724
- const firstDayOfWeek = getFirstDayOfWeek(currentYear, currentWeek);
725
- const lastDayOfWeek = new Date(firstDayOfWeek);
726
- lastDayOfWeek.setDate(firstDayOfWeek.getDate() + 6);
727
-
728
- const firstDayStr = firstDayOfWeek.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
729
- const lastDayStr = lastDayOfWeek.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
730
-
731
- periodElement.textContent = `Semaine du ${firstDayStr} au ${lastDayStr}`;
732
  }
733
- }
734
-
735
- function getWeeksInYear(year) {
736
- const firstDay = new Date(year, 0, 1);
737
- const lastDay = new Date(year, 11, 31);
738
-
739
- // Adjust for Monday as first day of week
740
- const dayOffset = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1;
741
- firstDay.setDate(firstDay.getDate() - dayOffset);
742
 
743
- const diff = lastDay - firstDay;
744
- return Math.ceil(diff / (1000 * 60 * 60 * 24 * 7));
 
745
  }
746
 
747
- // Google Sign-In
748
- function handleGoogleSignIn(response) {
749
- const user = response.credential ? parseJWT(response.credential) : response;
750
- currentUser = {
751
- name: user.name,
752
- email: user.email,
753
- picture: user.picture
754
- };
755
-
756
- document.getElementById('user-info').classList.remove('hidden');
757
- document.getElementById('user-pic').src = currentUser.picture;
758
- document.getElementById('user-name').textContent = currentUser.name;
759
-
760
- document.querySelector('.g_id_signin').classList.add('hidden');
761
- document.getElementById('sign-out').classList.remove('hidden');
762
- document.getElementById('save-drive-btn').classList.remove('hidden');
763
-
764
- // Load data from Google Drive if available
765
- // This would require additional Google Drive API implementation
766
  }
767
 
768
- function parseJWT(token) {
769
- const base64Url = token.split('.')[1];
770
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
771
- const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
772
- return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
773
- }).join(''));
774
-
775
- return JSON.parse(jsonPayload);
776
  }
777
 
778
- document.getElementById('sign-out').addEventListener('click', function() {
779
- google.accounts.id.disableAutoSelect();
780
- currentUser = null;
781
-
782
- document.getElementById('user-info').classList.add('hidden');
783
- document.querySelector('.g_id_signin').classList.remove('hidden');
784
- document.getElementById('sign-out').classList.add('hidden');
785
- document.getElementById('save-drive-btn').classList.add('hidden');
786
- });
787
-
788
- // Save to Google Drive (placeholder - would need actual implementation)
789
- document.getElementById('save-drive-btn').addEventListener('click', function() {
790
- if (!currentUser) return;
791
-
792
- // In a real implementation, this would use the Google Drive API
793
- alert('Fonctionnalité de sauvegarde sur Google Drive à implémenter');
794
- });
795
-
796
- // Local storage
797
- function saveToLocalStorage() {
798
- localStorage.setItem('calendarData', JSON.stringify(calendarData));
799
  }
800
 
801
- function loadFromLocalStorage() {
802
- const savedData = localStorage.getItem('calendarData');
803
- if (savedData) {
 
 
 
804
  try {
805
- const parsedData = JSON.parse(savedData);
 
806
 
807
- // Merge with existing structure to ensure all years are present
808
- for (let year in parsedData) {
809
- if (parsedData.hasOwnProperty(year)) {
810
- if (!calendarData[year]) {
811
- calendarData[year] = {};
812
- }
813
-
814
- for (let month in parsedData[year]) {
815
- if (parsedData[year].hasOwnProperty(month)) {
816
- if (!calendarData[year][month]) {
817
- calendarData[year][month] = {};
818
- }
819
-
820
- for (let day in parsedData[year][month]) {
821
- if (parsedData[year][month].hasOwnProperty(day)) {
822
- calendarData[year][month][day] = parsedData[year][month][day];
823
- }
824
- }
825
- }
826
- }
827
- }
828
  }
829
 
 
 
 
 
830
  renderCalendar();
831
- } catch (error) {
832
- console.error('Error loading from localStorage:', error);
 
 
 
833
  }
834
  }
835
  }
836
 
837
- // Export/Import functions
838
- function exportCalendar() {
839
- const dataStr = JSON.stringify(calendarData, null, 2);
840
- const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
841
-
842
- const exportName = `calendrier_${currentYear}.json`;
843
-
844
- const linkElement = document.createElement('a');
845
- linkElement.setAttribute('href', dataUri);
846
- linkElement.setAttribute('download', exportName);
847
- linkElement.click();
848
  }
849
 
850
- function mergeCalendarData(importedData) {
851
- for (let year in importedData) {
852
- if (importedData.hasOwnProperty(year)) {
853
- if (!calendarData[year]) {
854
- calendarData[year] = {};
855
- }
856
-
857
- for (let month in importedData[year]) {
858
- if (importedData[year].hasOwnProperty(month)) {
859
- if (!calendarData[year][month]) {
860
- calendarData[year][month] = {};
861
- }
862
-
863
- for (let day in importedData[year][month]) {
864
- if (importedData[year][month].hasOwnProperty(day)) {
865
- if (!calendarData[year][month][day]) {
866
- calendarData[year][month][day] = [];
867
- }
868
-
869
- // Add imported tasks with user prefix if available
870
- const importedTasks = importedData[year][month][day];
871
- for (let task of importedTasks) {
872
- if (currentUser) {
873
- task.text = `[${currentUser.name}] ${task.text}`;
874
- task.user = currentUser.name;
875
- }
876
- calendarData[year][month][day].push(task);
877
- }
878
- }
879
- }
880
- }
881
- }
882
- }
883
  }
884
  }
885
 
886
- // Multi-day selection functions
887
- function startMultiDaySelection() {
888
- multiDaySelection = true;
889
- document.getElementById('multi-day-info').classList.remove('hidden');
890
- }
891
-
892
- function endMultiDaySelection() {
893
- multiDaySelection = false;
894
- document.getElementById('multi-day-info').classList.add('hidden');
895
-
896
- if (multiDayStart && multiDayEnd) {
897
- // Here you would implement what happens when a multi-day selection is made
898
- // For example, create a task that spans multiple days
899
- alert(`Sélection multiple du ${formatDate(multiDayStart)} au ${formatDate(multiDayEnd)}`);
900
  }
901
-
902
- multiDayStart = null;
903
- multiDayEnd = null;
904
- renderCalendar();
905
  }
906
 
907
- function cancelMultiDaySelection() {
908
- multiDaySelection = false;
909
- multiDayStart = null;
910
- multiDayEnd = null;
911
- document.getElementById('multi-day-info').classList.add('hidden');
912
  renderCalendar();
913
- }
914
 
915
- function formatDate(date) {
916
- return new Date(date.year, date.month, date.day).toLocaleDateString('fr-FR');
917
- }
 
918
 
919
- function checkMultiDaySelection(year, month, day) {
920
- if (!multiDayStart || !multiDayEnd) return false;
921
-
922
- const currentDate = new Date(year, month, day).getTime();
923
- const startDate = new Date(multiDayStart.year, multiDayStart.month, multiDayStart.day).getTime();
924
- const endDate = new Date(multiDayEnd.year, multiDayEnd.month, multiDayEnd.day).getTime();
925
-
926
- const minDate = Math.min(startDate, endDate);
927
- const maxDate = Math.max(startDate, endDate);
928
-
929
- if (currentDate >= minDate && currentDate <= maxDate) {
930
- if (currentDate === minDate) return { selected: true, position: 'multi-day-start' };
931
- if (currentDate === maxDate) return { selected: true, position: 'multi-day-end' };
932
- return { selected: true, position: 'multi-day-middle' };
933
- }
934
-
935
- return false;
936
- }
937
 
938
- // Mouse event handlers for multi-day selection
939
- function handleDayMouseDown(e) {
940
- if (!multiDaySelection) return;
941
 
942
- const dayElement = e.target.closest('.calendar-day');
943
- if (!dayElement) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
944
 
945
- isDragging = true;
 
 
 
 
 
 
 
 
 
 
 
946
 
947
- const year = parseInt(dayElement.dataset.year);
948
- const month = parseInt(dayElement.dataset.month);
949
- const day = parseInt(dayElement.dataset.day);
950
 
951
- multiDayStart = { year, month, day };
952
- multiDayEnd = { year, month, day };
 
 
 
 
953
 
954
- renderCalendar();
955
- }
 
956
 
957
- function handleDayMouseMove(e) {
958
- if (!isDragging || !multiDaySelection) return;
959
-
960
- const dayElement = e.target.closest('.calendar-day');
961
- if (!dayElement) return;
962
-
963
- const year = parseInt(dayElement.dataset.year);
964
- const month = parseInt(dayElement.dataset.month);
965
- const day = parseInt(dayElement.dataset.day);
966
-
967
- multiDayEnd = { year, month, day };
968
- renderCalendar();
969
- }
970
 
971
- function handleDayMouseUp() {
972
- isDragging = false;
973
- }
974
 
975
- // Touch event handlers for multi-day selection
976
- function handleDayTouchStart(e) {
977
- if (!multiDaySelection) return;
978
-
979
- e.preventDefault();
980
- const touch = e.touches[0];
981
- const dayElement = document.elementFromPoint(touch.clientX, touch.clientY).closest('.calendar-day');
982
- if (!dayElement) return;
983
-
984
- isDragging = true;
985
-
986
- const year = parseInt(dayElement.dataset.year);
987
- const month = parseInt(dayElement.dataset.month);
988
- const day = parseInt(dayElement.dataset.day);
989
-
990
- multiDayStart = { year, month, day };
991
- multiDayEnd = { year, month, day };
992
-
993
- renderCalendar();
994
- }
995
 
996
- function handleDayTouchMove(e) {
997
- if (!isDragging || !multiDaySelection) return;
998
-
999
- e.preventDefault();
1000
- const touch = e.touches[0];
1001
- const dayElement = document.elementFromPoint(touch.clientX, touch.clientY).closest('.calendar-day');
1002
- if (!dayElement) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1003
 
1004
- const year = parseInt(dayElement.dataset.year);
1005
- const month = parseInt(dayElement.dataset.month);
1006
- const day = parseInt(dayElement.dataset.day);
 
 
1007
 
1008
- multiDayEnd = { year, month, day };
1009
- renderCalendar();
1010
- }
1011
 
1012
- function handleDayTouchEnd() {
1013
- isDragging = false;
1014
- }
 
 
 
 
 
 
 
 
 
 
 
1015
 
1016
- // Search functions
1017
- function performSearch() {
1018
- const searchTerm = document.getElementById('search-input').value.trim().toLowerCase();
1019
- const categoryFilter = document.getElementById('search-category').value;
1020
-
1021
- if (!searchTerm && categoryFilter === 'all') {
1022
- alert('Veuillez entrer un terme de recherche ou sélectionner une catégorie');
 
 
1023
  return;
1024
  }
1025
 
1026
- const results = [];
1027
-
1028
- // Search through all calendar data
1029
- for (let year in calendarData) {
1030
- for (let month in calendarData[year]) {
1031
- for (let day in calendarData[year][month]) {
1032
- const tasks = calendarData[year][month][day];
1033
-
1034
- for (let task of tasks) {
1035
- // Check if task matches search criteria
1036
- const matchesText = searchTerm ? task.text.toLowerCase().includes(searchTerm) : true;
1037
- const matchesCategory = categoryFilter === 'all' || task.category === categoryFilter;
1038
-
1039
- if (matchesText && matchesCategory) {
1040
- results.push({
1041
- year: parseInt(year),
1042
- month: parseInt(month),
1043
- day: parseInt(day),
1044
- task: task
1045
- });
1046
- }
1047
- }
1048
- }
1049
- }
1050
- }
1051
-
1052
- displaySearchResults(results);
1053
- }
1054
 
1055
- function displaySearchResults(results) {
1056
- const resultsContainer = document.getElementById('search-results');
1057
- resultsContainer.innerHTML = '';
1058
-
1059
- if (results.length === 0) {
1060
- resultsContainer.innerHTML = '<p class="text-gray-600">Aucun résultat trouvé</p>';
1061
- } else {
1062
- results.forEach(result => {
1063
- const dateStr = new Date(result.year, result.month, result.day)
1064
- .toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
1065
-
1066
- const colorClass = getColorClass(result.task.color);
1067
-
1068
- const resultElement = document.createElement('div');
1069
- resultElement.className = `p-3 rounded-lg ${colorClass}`;
1070
- resultElement.innerHTML = `
1071
- <div class="flex justify-between items-start">
1072
- <div>
1073
- <h4 class="font-bold">${result.task.text}</h4>
1074
- <p class="text-sm">${dateStr}${result.task.time ? ` à ${result.task.time}` : ''}</p>
1075
- <p class="text-xs mt-1">Catégorie: ${result.task.category}</p>
1076
- </div>
1077
- <button class="delete-search-result text-gray-600 hover:text-gray-800 text-sm p-1"
1078
- data-year="${result.year}" data-month="${result.month}" data-day="${result.day}" data-index="${calendarData[result.year][result.month][result.day].indexOf(result.task)}">
1079
- <i class="fas fa-trash-alt"></i>
1080
- </button>
1081
- </div>
1082
- `;
1083
-
1084
- resultsContainer.appendChild(resultElement);
1085
- });
1086
-
1087
- // Add event listeners to delete buttons in search results
1088
- document.querySelectorAll('.delete-search-result').forEach(btn => {
1089
- btn.addEventListener('click', function() {
1090
- const year = parseInt(this.dataset.year);
1091
- const month = parseInt(this.dataset.month);
1092
- const day = parseInt(this.dataset.day);
1093
- const index = parseInt(this.dataset.index);
1094
-
1095
- calendarData[year][month][day].splice(index, 1);
1096
- saveToLocalStorage();
1097
- performSearch(); // Refresh search results
1098
- renderCalendar();
1099
- });
1100
- });
1101
- }
1102
-
1103
- document.getElementById('search-modal').classList.remove('hidden');
1104
- }
1105
 
1106
- function clearSearch() {
1107
- document.getElementById('search-input').value = '';
1108
- document.getElementById('search-category').value = 'all';
1109
- document.getElementById('search-modal').classList.add('hidden');
1110
- }
 
 
1111
  </script>
1112
  <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>
1113
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>MyFitCalendar - Suivi Musculation</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
 
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script>
10
+ tailwind.config = {
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ primary: '#10B981',
15
+ dark: '#111827',
16
+ darker: '#0B1120',
17
+ light: '#F3F4F6',
18
+ }
19
+ }
20
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
+ </script>
23
+ <style>
24
+ /* Custom scrollbar */
25
+ ::-webkit-scrollbar {
26
+ width: 8px;
27
+ }
28
+ ::-webkit-scrollbar-track {
29
+ background: #1F2937;
30
+ }
31
+ ::-webkit-scrollbar-thumb {
32
+ background: #10B981;
33
+ border-radius: 4px;
34
+ }
35
+
36
+ /* Animation for workout cards */
37
+ @keyframes fadeIn {
38
+ from { opacity: 0; transform: translateY(10px); }
39
+ to { opacity: 1; transform: translateY(0); }
40
+ }
41
+
42
+ .workout-card {
43
+ animation: fadeIn 0.3s ease-out forwards;
44
+ }
45
+
46
+ /* Custom checkbox */
47
+ .custom-checkbox {
48
+ appearance: none;
49
+ -webkit-appearance: none;
50
+ width: 20px;
51
+ height: 20px;
52
+ border: 2px solid #10B981;
53
+ border-radius: 4px;
54
+ outline: none;
55
  cursor: pointer;
56
+ position: relative;
57
+ }
58
+
59
+ .custom-checkbox:checked {
60
+ background-color: #10B981;
61
+ }
62
+
63
+ .custom-checkbox:checked::after {
64
+ content: '✓';
65
+ position: absolute;
66
+ color: white;
67
+ font-size: 14px;
68
+ top: 50%;
69
+ left: 50%;
70
+ transform: translate(-50%, -50%);
71
+ }
72
+
73
+ /* Date picker custom style */
74
+ input[type="date"]::-webkit-calendar-picker-indicator {
75
+ filter: invert(1);
76
  }
77
  </style>
78
  </head>
79
+ <body class="bg-dark text-light min-h-screen">
80
  <div class="container mx-auto px-4 py-8">
81
  <!-- Header -->
82
+ <header class="flex justify-between items-center mb-8">
83
+ <div>
84
+ <h1 class="text-3xl font-bold text-primary">MyFitCalendar</h1>
85
+ <p class="text-gray-400">Suivi de vos séances de musculation</p>
86
+ </div>
87
+ <div class="flex items-center space-x-4">
88
+ <button id="auth-btn" class="bg-primary hover:bg-green-600 text-dark font-medium py-2 px-4 rounded-lg transition">
89
+ <i class="fas fa-sign-in-alt mr-2"></i>Connexion
90
+ </button>
91
+ <button id="export-btn" class="border border-primary text-primary hover:bg-green-900/50 font-medium py-2 px-4 rounded-lg transition">
92
+ <i class="fas fa-file-export mr-2"></i>Exporter
93
+ </button>
94
+ <button id="import-btn" class="border border-primary text-primary hover:bg-green-900/50 font-medium py-2 px-4 rounded-lg transition">
95
+ <i class="fas fa-file-import mr-2"></i>Importer
 
 
 
 
 
 
 
 
 
 
96
  </button>
97
  </div>
98
  </header>
99
 
100
+ <!-- Main Content -->
101
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
102
+ <!-- Calendar Section -->
103
+ <div class="lg:col-span-2 bg-darker rounded-xl p-6 shadow-lg">
104
+ <div class="flex justify-between items-center mb-6">
105
+ <h2 class="text-2xl font-semibold text-primary">Calendrier</h2>
106
+ <div class="flex items-center space-x-4">
107
+ <button id="prev-month" class="text-primary hover:text-green-300 transition">
108
+ <i class="fas fa-chevron-left"></i>
 
 
 
 
 
 
 
 
 
 
109
  </button>
110
+ <span id="current-month" class="font-medium">Mai 2023</span>
111
+ <button id="next-month" class="text-primary hover:text-green-300 transition">
112
+ <i class="fas fa-chevron-right"></i>
113
  </button>
114
  </div>
 
 
 
 
 
 
 
 
 
 
115
  </div>
116
+
117
+ <!-- Calendar Grid -->
118
+ <div class="grid grid-cols-7 gap-2 mb-4">
119
+ <div class="text-center font-medium text-primary">Lun</div>
120
+ <div class="text-center font-medium text-primary">Mar</div>
121
+ <div class="text-center font-medium text-primary">Mer</div>
122
+ <div class="text-center font-medium text-primary">Jeu</div>
123
+ <div class="text-center font-medium text-primary">Ven</div>
124
+ <div class="text-center font-medium text-primary">Sam</div>
125
+ <div class="text-center font-medium text-primary">Dim</div>
126
  </div>
127
+
128
+ <div id="calendar-grid" class="grid grid-cols-7 gap-2">
129
+ <!-- Calendar days will be generated by JavaScript -->
 
 
 
 
 
 
130
  </div>
131
+
132
+ <!-- Multi-day selection controls -->
133
+ <div class="mt-6 p-4 bg-gray-800 rounded-lg">
134
+ <div class="flex items-center justify-between mb-2">
135
+ <h3 class="text-primary font-medium">Sélection multiple</h3>
136
+ <button id="clear-selection" class="text-xs text-gray-400 hover:text-primary transition">Effacer</button>
137
+ </div>
138
+ <p class="text-sm text-gray-400 mb-3">Sélectionnez plusieurs jours pour marquer une séance sur plusieurs jours</p>
139
+ <div class="flex items-center space-x-4">
140
+ <div class="flex-1">
141
+ <label class="block text-sm text-gray-400 mb-1">Couleur</label>
142
+ <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">
143
+ <option value="bg-green-500/30">Vert</option>
144
+ <option value="bg-blue-500/30">Bleu</option>
145
+ <option value="bg-purple-500/30">Violet</option>
146
+ <option value="bg-yellow-500/30">Jaune</option>
147
+ <option value="bg-red-500/30">Rouge</option>
148
+ </select>
149
+ </div>
150
+ <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">
151
+ <i class="fas fa-check mr-2"></i>Appliquer
152
+ </button>
153
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  </div>
155
+ </div>
156
+
157
+ <!-- Workout Form Section -->
158
+ <div class="bg-darker rounded-xl p-6 shadow-lg">
159
+ <h2 class="text-2xl font-semibold text-primary mb-6">Nouvelle Séance</h2>
160
+
161
+ <form id="workout-form" class="space-y-4">
162
  <div>
163
+ <label for="workout-name" class="block text-sm font-medium text-gray-400 mb-1">Nom de la séance</label>
164
+ <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>
165
  </div>
166
+
167
  <div>
168
+ <label for="workout-date" class="block text-sm font-medium text-gray-400 mb-1">Date</label>
169
+ <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>
170
  </div>
171
+
172
  <div>
173
+ <label for="workout-duration" class="block text-sm font-medium text-gray-400 mb-1">Durée (minutes)</label>
174
+ <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>
175
+ </div>
176
+
177
+ <div>
178
+ <label for="workout-satisfaction" class="block text-sm font-medium text-gray-400 mb-1">Satisfaction</label>
179
+ <div class="flex items-center space-x-2">
180
+ <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">
181
+ <span id="satisfaction-value" class="text-primary font-medium">50%</span>
182
  </div>
183
  </div>
184
+
185
+ <div class="pt-2">
186
+ <label class="flex items-center space-x-2 cursor-pointer">
187
+ <input type="checkbox" id="intensity-drop" class="custom-checkbox">
188
+ <span class="text-sm text-gray-400">Série dégressive</span>
189
+ </label>
190
+ </div>
191
+
192
+ <!-- Exercises Container -->
193
+ <div class="space-y-4">
194
+ <div class="flex justify-between items-center">
195
+ <h3 class="text-sm font-medium text-gray-400">Exercices</h3>
196
+ <button type="button" id="add-exercise" class="text-xs text-primary hover:text-green-300 transition flex items-center">
197
+ <i class="fas fa-plus mr-1"></i> Ajouter
198
+ </button>
199
+ </div>
200
+
201
+ <div id="exercises-container">
202
+ <!-- Exercise fields will be added here -->
203
+ </div>
204
+ </div>
205
+
206
+ <div class="pt-4">
207
+ <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">
208
+ <i class="fas fa-save mr-2"></i> Enregistrer
209
  </button>
210
  </div>
211
+ </form>
212
  </div>
213
  </div>
214
+
215
+ <!-- Workouts List Section -->
216
+ <div class="mt-8 bg-darker rounded-xl p-6 shadow-lg">
217
+ <div class="flex justify-between items-center mb-6">
218
+ <h2 class="text-2xl font-semibold text-primary">Historique des Séances</h2>
219
+ <div class="flex items-center space-x-2">
220
+ <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">
221
+ <button id="clear-search" class="text-gray-400 hover:text-primary transition">
222
  <i class="fas fa-times"></i>
223
  </button>
224
  </div>
225
+ </div>
226
+
227
+ <div id="workouts-list" class="space-y-3">
228
+ <!-- Workout cards will be added here -->
229
+ <div class="text-center text-gray-500 py-8">
230
+ <i class="fas fa-dumbbell text-4xl mb-2"></i>
231
+ <p>Aucune séance enregistrée</p>
232
  </div>
233
  </div>
234
  </div>
235
+ </div>
236
+
237
+ <!-- Modals -->
238
+ <!-- Workout Details Modal -->
239
+ <div id="workout-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden">
240
+ <div class="bg-darker rounded-xl p-6 w-full max-w-md mx-4 max-h-[90vh] overflow-y-auto">
241
+ <div class="flex justify-between items-center mb-4">
242
+ <h3 id="modal-title" class="text-xl font-semibold text-primary"></h3>
243
+ <button id="close-modal" class="text-gray-400 hover:text-primary transition">
244
+ <i class="fas fa-times"></i>
245
+ </button>
246
+ </div>
247
+ <div id="modal-content" class="space-y-4">
248
+ <!-- Workout details will be added here -->
249
+ </div>
250
+ <div class="mt-6 flex justify-end space-x-3">
251
+ <button id="delete-workout" class="text-red-400 hover:text-red-300 transition flex items-center">
252
+ <i class="fas fa-trash mr-2"></i> Supprimer
253
+ </button>
254
+ <button id="edit-workout" class="text-primary hover:text-green-300 transition flex items-center">
255
+ <i class="fas fa-edit mr-2"></i> Modifier
256
+ </button>
257
+ </div>
258
+ </div>
259
+ </div>
260
+
261
+ <!-- Import/Export Modal -->
262
+ <div id="data-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden">
263
+ <div class="bg-darker rounded-xl p-6 w-full max-w-md mx-4">
264
+ <div class="flex justify-between items-center mb-4">
265
+ <h3 id="data-modal-title" class="text-xl font-semibold text-primary"></h3>
266
+ <button id="close-data-modal" class="text-gray-400 hover:text-primary transition">
267
+ <i class="fas fa-times"></i>
268
+ </button>
269
+ </div>
270
+ <div id="data-modal-content" class="space-y-4">
271
+ <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>
272
+ <div id="user-prefix-container" class="hidden">
273
+ <label for="user-prefix" class="block text-sm font-medium text-gray-400 mb-1">Préfixe utilisateur</label>
274
+ <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">
275
+ </div>
276
+ </div>
277
+ <div class="mt-6 flex justify-end">
278
+ <button id="confirm-data-action" class="bg-primary hover:bg-green-600 text-dark font-medium py-2 px-4 rounded-lg transition">
279
+ Confirmer
280
+ </button>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ <!-- Auth Modal -->
286
+ <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden">
287
+ <div class="bg-darker rounded-xl p-6 w-full max-w-md mx-4">
288
+ <div class="flex justify-between items-center mb-4">
289
+ <h3 class="text-xl font-semibold text-primary">Connexion</h3>
290
+ <button id="close-auth-modal" class="text-gray-400 hover:text-primary transition">
291
+ <i class="fas fa-times"></i>
292
+ </button>
293
+ </div>
294
+ <div class="space-y-4">
295
+ <p class="text-gray-400">Connectez-vous avec Google pour sauvegarder vos données en ligne.</p>
296
+ <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">
297
+ <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">
298
+ Continuer avec Google
299
+ </button>
300
+ </div>
301
  </div>
302
  </div>
303
 
304
  <script>
305
+ // DOM Elements
306
+ const calendarGrid = document.getElementById('calendar-grid');
307
+ const currentMonthEl = document.getElementById('current-month');
308
+ const prevMonthBtn = document.getElementById('prev-month');
309
+ const nextMonthBtn = document.getElementById('next-month');
310
+ const workoutForm = document.getElementById('workout-form');
311
+ const exercisesContainer = document.getElementById('exercises-container');
312
+ const addExerciseBtn = document.getElementById('add-exercise');
313
+ const workoutsList = document.getElementById('workouts-list');
314
+ const workoutModal = document.getElementById('workout-modal');
315
+ const closeModalBtn = document.getElementById('close-modal');
316
+ const modalTitle = document.getElementById('modal-title');
317
+ const modalContent = document.getElementById('modal-content');
318
+ const deleteWorkoutBtn = document.getElementById('delete-workout');
319
+ const editWorkoutBtn = document.getElementById('edit-workout');
320
+ const satisfactionSlider = document.getElementById('workout-satisfaction');
321
+ const satisfactionValue = document.getElementById('satisfaction-value');
322
+ const dataModal = document.getElementById('data-modal');
323
+ const closeDataModalBtn = document.getElementById('close-data-modal');
324
+ const dataModalTitle = document.getElementById('data-modal-title');
325
+ const dataModalContent = document.getElementById('data-modal-content');
326
+ const dataTextarea = document.getElementById('data-textarea');
327
+ const confirmDataActionBtn = document.getElementById('confirm-data-action');
328
+ const exportBtn = document.getElementById('export-btn');
329
+ const importBtn = document.getElementById('import-btn');
330
+ const authModal = document.getElementById('auth-modal');
331
+ const closeAuthModalBtn = document.getElementById('close-auth-modal');
332
+ const authBtn = document.getElementById('auth-btn');
333
+ const googleAuthBtn = document.getElementById('google-auth-btn');
334
+ const clearSelectionBtn = document.getElementById('clear-selection');
335
+ const multiDayColorSelect = document.getElementById('multi-day-color');
336
+ const applyMultiDayBtn = document.getElementById('apply-multi-day');
337
+ const searchWorkoutsInput = document.getElementById('search-workouts');
338
+ const clearSearchBtn = document.getElementById('clear-search');
339
+ const userPrefixContainer = document.getElementById('user-prefix-container');
340
+ const userPrefixInput = document.getElementById('user-prefix');
341
+
342
+ // State
343
+ let currentDate = new Date();
344
+ let workouts = JSON.parse(localStorage.getItem('workouts')) || [];
345
+ let selectedWorkoutId = null;
346
+ let selectedDays = [];
347
+ let isExporting = false;
348
+ let isAuthenticated = false;
349
+
350
+ // Initialize
351
+ function init() {
352
  renderCalendar();
353
+ renderWorkouts();
354
+ addExercise();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
+ // Set current date in form
357
+ const today = new Date().toISOString().split('T')[0];
358
+ document.getElementById('workout-date').value = today;
359
 
360
+ // Check if user is authenticated (simulated)
361
+ checkAuthStatus();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  }
363
 
364
+ // Calendar Functions
365
+ function renderCalendar() {
366
+ calendarGrid.innerHTML = '';
 
367
 
368
+ const year = currentDate.getFullYear();
369
+ const month = currentDate.getMonth();
 
370
 
371
+ currentMonthEl.textContent = new Intl.DateTimeFormat('fr-FR', {
372
+ month: 'long',
373
+ year: 'numeric'
374
+ }).format(currentDate);
375
 
376
+ const firstDay = new Date(year, month, 1);
377
+ const lastDay = new Date(year, month + 1, 0);
 
 
 
 
 
378
 
379
+ const startDay = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1;
380
+ const totalDays = lastDay.getDate();
381
 
382
  // Add empty cells for days before the first day of the month
383
+ for (let i = 0; i < startDay; i++) {
384
+ const cell = document.createElement('div');
385
+ cell.className = 'h-24 bg-gray-800 rounded-md opacity-50';
386
+ calendarGrid.appendChild(cell);
387
  }
388
 
389
  // Add cells for each day of the month
390
+ for (let i = 1; i <= totalDays; i++) {
391
+ const cell = document.createElement('div');
392
+ cell.className = 'h-24 bg-gray-800 rounded-md p-2 overflow-hidden relative';
 
393
 
394
+ const dayDate = new Date(year, month, i);
395
+ const dayISO = dayDate.toISOString().split('T')[0];
 
 
 
396
 
397
+ // Check if day has workouts
398
+ const dayWorkouts = workouts.filter(w => w.date === dayISO);
399
+
400
+ // Check if day is part of a multi-day workout
401
+ const multiDayWorkout = workouts.find(w =>
402
+ w.multiDayDates && w.multiDayDates.includes(dayISO)
403
+ );
404
+
405
+ if (multiDayWorkout) {
406
+ cell.classList.add(multiDayWorkout.multiDayColor);
407
  }
408
 
409
+ // Add day number
410
+ const dayNumber = document.createElement('div');
411
+ dayNumber.className = 'text-right font-medium';
412
+ dayNumber.textContent = i;
413
+ cell.appendChild(dayNumber);
414
+
415
+ // Add workout indicators
416
+ if (dayWorkouts.length > 0) {
417
+ const workoutIndicator = document.createElement('div');
418
+ workoutIndicator.className = 'absolute bottom-1 left-1 right-1 flex space-x-1';
419
+
420
+ dayWorkouts.forEach(workout => {
421
+ const dot = document.createElement('div');
422
+ dot.className = 'h-2 w-2 rounded-full bg-primary';
423
+ workoutIndicator.appendChild(dot);
424
+ });
425
+
426
+ cell.appendChild(workoutIndicator);
427
  }
428
 
429
+ // Highlight today
430
+ const today = new Date();
431
+ if (
432
+ dayDate.getDate() === today.getDate() &&
433
+ dayDate.getMonth() === today.getMonth() &&
434
+ dayDate.getFullYear() === today.getFullYear()
435
+ ) {
436
+ cell.classList.add('border', 'border-primary');
437
+ }
438
+
439
+ // Add click event for selection
440
+ cell.addEventListener('click', () => {
441
+ if (selectedDays.includes(dayISO)) {
442
+ selectedDays = selectedDays.filter(d => d !== dayISO);
443
+ cell.classList.remove('ring-2', 'ring-primary');
444
+ } else {
445
+ selectedDays.push(dayISO);
446
+ cell.classList.add('ring-2', 'ring-primary');
447
+ }
448
+ });
449
 
450
+ calendarGrid.appendChild(cell);
451
  }
452
  }
453
 
454
+ // Workout Form Functions
455
+ function addExercise() {
456
+ const exerciseDiv = document.createElement('div');
457
+ exerciseDiv.className = 'exercise bg-gray-700 p-3 rounded-lg';
458
 
459
+ exerciseDiv.innerHTML = `
460
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-3">
461
+ <div class="md:col-span-2">
462
+ <label class="block text-xs text-gray-400 mb-1">Exercice</label>
463
+ <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>
464
+ </div>
465
+ <div>
466
+ <label class="block text-xs text-gray-400 mb-1">Séries</label>
467
+ <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>
468
+ </div>
469
  <div>
470
+ <label class="block text-xs text-gray-400 mb-1">Charge (kg)</label>
471
+ <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>
 
472
  </div>
473
+ </div>
474
+ <div class="mt-2 flex items-center justify-between">
475
+ <div class="flex items-center space-x-3">
476
+ <label class="flex items-center space-x-1 cursor-pointer">
477
+ <input type="checkbox" class="exercise-unilateral custom-checkbox">
478
+ <span class="text-xs text-gray-400">Unilatéral</span>
479
+ </label>
480
+ </div>
481
+ <button type="button" class="remove-exercise text-xs text-red-400 hover:text-red-300 transition">
482
+ <i class="fas fa-trash mr-1"></i> Supprimer
483
  </button>
484
  </div>
485
+ `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
 
487
+ exercisesContainer.appendChild(exerciseDiv);
488
+
489
+ // Add event listener to remove button
490
+ const removeBtn = exerciseDiv.querySelector('.remove-exercise');
491
+ removeBtn.addEventListener('click', () => {
492
+ if (exercisesContainer.querySelectorAll('.exercise').length > 1) {
493
+ exerciseDiv.remove();
 
 
 
 
 
494
  } else {
495
+ alert('Vous devez avoir au moins un exercice.');
 
 
 
 
 
 
 
 
496
  }
497
  });
498
+ }
499
+
500
+ // Workout List Functions
501
+ function renderWorkouts(filter = '') {
502
+ workoutsList.innerHTML = '';
503
 
504
+ let filteredWorkouts = workouts;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
 
506
+ if (filter) {
507
+ const searchTerm = filter.toLowerCase();
508
+ filteredWorkouts = workouts.filter(workout =>
509
+ workout.name.toLowerCase().includes(searchTerm) ||
510
+ workout.exercises.some(ex => ex.name.toLowerCase().includes(searchTerm))
511
+ );
512
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
 
514
+ if (filteredWorkouts.length === 0) {
515
+ workoutsList.innerHTML = `
516
+ <div class="text-center text-gray-500 py-8">
517
+ <i class="fas fa-dumbbell text-4xl mb-2"></i>
518
+ <p>Aucune séance trouvée</p>
519
+ </div>
520
+ `;
521
+ return;
522
+ }
523
 
524
+ filteredWorkouts.sort((a, b) => new Date(b.date) - new Date(a.date));
 
 
525
 
526
+ filteredWorkouts.forEach(workout => {
527
+ const workoutCard = document.createElement('div');
528
+ workoutCard.className = 'workout-card bg-gray-800 rounded-lg p-4 hover:bg-gray-700/50 transition cursor-pointer';
529
+ workoutCard.dataset.id = workout.id;
530
 
531
+ // Calculate total volume
532
+ const totalVolume = workout.exercises.reduce((sum, ex) => {
533
+ return sum + (ex.sets * ex.weight);
534
+ }, 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
 
536
+ // Format date
537
+ const workoutDate = new Date(workout.date);
538
+ const formattedDate = workoutDate.toLocaleDateString('fr-FR', {
539
+ weekday: 'short',
540
+ day: 'numeric',
541
+ month: 'short'
542
+ });
543
+
544
+ workoutCard.innerHTML = `
545
+ <div class="flex justify-between items-start">
546
+ <div>
547
+ <h3 class="font-medium text-lg">${workout.name}</h3>
548
+ <p class="text-sm text-gray-400">${formattedDate}</p>
549
+ </div>
550
+ <div class="flex items-center space-x-2">
551
+ <span class="text-xs bg-primary/20 text-primary px-2 py-1 rounded-full">${workout.duration} min</span>
552
+ <span class="text-xs bg-green-900/50 text-primary px-2 py-1 rounded-full">${totalVolume} kg</span>
553
+ </div>
554
+ </div>
555
+ <div class="mt-3 flex items-center justify-between">
556
+ <div class="flex items-center space-x-2">
557
+ <div class="h-2 w-20 bg-gray-600 rounded-full overflow-hidden">
558
+ <div class="h-full bg-primary rounded-full" style="width: ${workout.satisfaction}%"></div>
559
+ </div>
560
+ <span class="text-xs text-gray-400">${workout.satisfaction}%</span>
561
+ </div>
562
+ <div class="flex -space-x-1">
563
+ ${workout.exercises.slice(0, 3).map(ex =>
564
+ `<div class="h-6 w-6 rounded-full bg-gray-600 flex items-center justify-center text-xs">${ex.name.charAt(0).toUpperCase()}</div>`
565
+ ).join('')}
566
+ ${workout.exercises.length > 3 ?
567
+ `<div class="h-6 w-6 rounded-full bg-gray-600 flex items-center justify-center text-xs">+${workout.exercises.length - 3}</div>` :
568
+ ''
569
+ }
570
+ </div>
571
+ </div>
572
+ `;
573
+
574
+ workoutCard.addEventListener('click', () => showWorkoutDetails(workout.id));
575
+
576
+ workoutsList.appendChild(workoutCard);
577
  });
578
+ }
579
+
580
+ function showWorkoutDetails(workoutId) {
581
+ const workout = workouts.find(w => w.id === workoutId);
582
+ if (!workout) return;
583
 
584
+ selectedWorkoutId = workoutId;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
 
586
+ // Format date
587
+ const workoutDate = new Date(workout.date);
588
+ const formattedDate = workoutDate.toLocaleDateString('fr-FR', {
589
+ weekday: 'long',
590
+ day: 'numeric',
591
+ month: 'long',
592
+ year: 'numeric'
593
  });
594
 
595
+ // Calculate total volume
596
+ const totalVolume = workout.exercises.reduce((sum, ex) => {
597
+ return sum + (ex.sets * ex.weight);
598
+ }, 0);
599
+
600
+ // Create modal content
601
+ modalTitle.textContent = workout.name;
602
+
603
+ let exercisesHtml = '';
604
+ workout.exercises.forEach(ex => {
605
+ exercisesHtml += `
606
+ <div class="bg-gray-700 p-3 rounded-lg">
607
+ <div class="flex justify-between items-center mb-1">
608
+ <h4 class="font-medium">${ex.name}</h4>
609
+ <div class="flex items-center space-x-2">
610
+ ${ex.unilateral ?
611
+ '<span class="text-xs bg-blue-900/50 text-blue-300 px-2 py-0.5 rounded-full">Unilatéral</span>' :
612
+ ''
613
+ }
614
+ ${workout.intensityDrop ?
615
+ '<span class="text-xs bg-purple-900/50 text-purple-300 px-2 py-0.5 rounded-full">Série dégressive</span>' :
616
+ ''
617
+ }
618
+ </div>
619
+ </div>
620
+ <div class="flex justify-between text-sm text-gray-400">
621
+ <span>${ex.sets} séries</span>
622
+ <span>${ex.weight} kg</span>
623
+ </div>
624
+ </div>
625
+ `;
626
  });
627
 
628
+ modalContent.innerHTML = `
629
+ <div class="space-y-4">
630
+ <div>
631
+ <p class="text-gray-400">${formattedDate}</p>
632
+ <p class="text-gray-400">Durée: ${workout.duration} minutes</p>
633
+ </div>
634
+
635
+ <div class="flex items-center justify-between">
636
+ <div>
637
+ <p class="text-sm text-gray-400">Satisfaction</p>
638
+ <div class="flex items-center space-x-2">
639
+ <div class="h-2 w-32 bg-gray-600 rounded-full overflow-hidden">
640
+ <div class="h-full bg-primary rounded-full" style="width: ${workout.satisfaction}%"></div>
641
+ </div>
642
+ <span class="text-sm text-primary">${workout.satisfaction}%</span>
643
+ </div>
644
+ </div>
645
+ <div class="text-right">
646
+ <p class="text-sm text-gray-400">Tonnage total</p>
647
+ <p class="text-primary font-medium">${totalVolume} kg</p>
648
+ </div>
649
+ </div>
650
 
651
+ <div class="pt-2">
652
+ <h4 class="font-medium text-gray-400 mb-2">Exercices (${workout.exercises.length})</h4>
653
+ <div class="space-y-2">
654
+ ${exercisesHtml}
655
+ </div>
656
+ </div>
657
 
658
+ ${workout.multiDayDates && workout.multiDayDates.length > 1 ?
659
+ `<div class="pt-2">
660
+ <h4 class="font-medium text-gray-400 mb-2">Séance sur plusieurs jours</h4>
661
+ <div class="flex flex-wrap gap-1">
662
+ ${workout.multiDayDates.map(date => {
663
+ const d = new Date(date);
664
+ return `<span class="text-xs bg-gray-600 px-2 py-1 rounded-full">${d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}</span>`;
665
+ }).join('')}
666
+ </div>
667
+ </div>` :
668
+ ''
669
+ }
670
+ </div>
671
+ `;
672
+
673
+ workoutModal.classList.remove('hidden');
674
  }
675
 
676
+ // Data Functions
677
+ function saveWorkout(workoutData) {
678
+ // Generate ID if new workout
679
+ if (!workoutData.id) {
680
+ workoutData.id = Date.now().toString();
681
+ workouts.push(workoutData);
 
682
  } else {
683
+ // Update existing workout
684
+ const index = workouts.findIndex(w => w.id === workoutData.id);
685
+ if (index !== -1) {
686
+ workouts[index] = workoutData;
687
+ }
 
 
 
688
  }
 
 
 
 
 
 
 
 
 
689
 
690
+ localStorage.setItem('workouts', JSON.stringify(workouts));
691
+ renderCalendar();
692
+ renderWorkouts();
693
  }
694
 
695
+ function deleteWorkout(workoutId) {
696
+ workouts = workouts.filter(w => w.id !== workoutId);
697
+ localStorage.setItem('workouts', JSON.stringify(workouts));
698
+ renderCalendar();
699
+ renderWorkouts();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700
  }
701
 
702
+ function exportData() {
703
+ isExporting = true;
704
+ dataModalTitle.textContent = 'Exporter les données';
705
+ dataTextarea.value = JSON.stringify(workouts, null, 2);
706
+ userPrefixContainer.classList.add('hidden');
707
+ dataModal.classList.remove('hidden');
 
 
708
  }
709
 
710
+ function importData() {
711
+ isExporting = false;
712
+ dataModalTitle.textContent = 'Importer des données';
713
+ dataTextarea.value = '';
714
+ userPrefixContainer.classList.remove('hidden');
715
+ dataModal.classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  }
717
 
718
+ function confirmDataAction() {
719
+ if (isExporting) {
720
+ // Export logic (already handled by textarea value)
721
+ dataModal.classList.add('hidden');
722
+ } else {
723
+ // Import logic
724
  try {
725
+ const importedWorkouts = JSON.parse(dataTextarea.value);
726
+ const prefix = userPrefixInput.value.trim();
727
 
728
+ if (prefix) {
729
+ importedWorkouts.forEach(workout => {
730
+ workout.name = `${prefix} - ${workout.name}`;
731
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
  }
733
 
734
+ // Merge with existing workouts
735
+ workouts = [...workouts, ...importedWorkouts];
736
+ localStorage.setItem('workouts', JSON.stringify(workouts));
737
+
738
  renderCalendar();
739
+ renderWorkouts();
740
+ dataModal.classList.add('hidden');
741
+ } catch (e) {
742
+ alert('Données JSON invalides');
743
+ console.error(e);
744
  }
745
  }
746
  }
747
 
748
+ // Auth Functions (simulated)
749
+ function checkAuthStatus() {
750
+ // In a real app, this would check Firebase auth state
751
+ isAuthenticated = localStorage.getItem('isAuthenticated') === 'true';
752
+ updateAuthUI();
 
 
 
 
 
 
753
  }
754
 
755
+ function updateAuthUI() {
756
+ if (isAuthenticated) {
757
+ authBtn.innerHTML = '<i class="fas fa-sign-out-alt mr-2"></i>Déconnexion';
758
+ } else {
759
+ authBtn.innerHTML = '<i class="fas fa-sign-in-alt mr-2"></i>Connexion';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  }
761
  }
762
 
763
+ function toggleAuth() {
764
+ if (isAuthenticated) {
765
+ // Sign out
766
+ isAuthenticated = false;
767
+ localStorage.removeItem('isAuthenticated');
768
+ } else {
769
+ // Show auth modal
770
+ authModal.classList.remove('hidden');
 
 
 
 
 
 
771
  }
772
+ updateAuthUI();
 
 
 
773
  }
774
 
775
+ // Event Listeners
776
+ prevMonthBtn.addEventListener('click', () => {
777
+ currentDate.setMonth(currentDate.getMonth() - 1);
 
 
778
  renderCalendar();
779
+ });
780
 
781
+ nextMonthBtn.addEventListener('click', () => {
782
+ currentDate.setMonth(currentDate.getMonth() + 1);
783
+ renderCalendar();
784
+ });
785
 
786
+ addExerciseBtn.addEventListener('click', addExercise);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
787
 
788
+ workoutForm.addEventListener('submit', (e) => {
789
+ e.preventDefault();
 
790
 
791
+ const workoutName = document.getElementById('workout-name').value;
792
+ const workoutDate = document.getElementById('workout-date').value;
793
+ const workoutDuration = document.getElementById('workout-duration').value;
794
+ const workoutSatisfaction = document.getElementById('workout-satisfaction').value;
795
+ const intensityDrop = document.getElementById('intensity-drop').checked;
796
+
797
+ // Get exercises
798
+ const exercises = [];
799
+ const exerciseElements = document.querySelectorAll('.exercise');
800
+
801
+ exerciseElements.forEach(exEl => {
802
+ exercises.push({
803
+ name: exEl.querySelector('.exercise-name').value,
804
+ sets: parseInt(exEl.querySelector('.exercise-sets').value),
805
+ weight: parseFloat(exEl.querySelector('.exercise-weight').value),
806
+ unilateral: exEl.querySelector('.exercise-unilateral').checked
807
+ });
808
+ });
809
 
810
+ // Create workout object
811
+ const workoutData = {
812
+ id: selectedWorkoutId,
813
+ name: workoutName,
814
+ date: workoutDate,
815
+ duration: workoutDuration,
816
+ satisfaction: workoutSatisfaction,
817
+ intensityDrop: intensityDrop,
818
+ exercises: exercises,
819
+ multiDayDates: selectedDays.length > 0 ? selectedDays : [workoutDate],
820
+ multiDayColor: selectedDays.length > 0 ? multiDayColorSelect.value : ''
821
+ };
822
 
823
+ saveWorkout(workoutData);
 
 
824
 
825
+ // Reset form
826
+ workoutForm.reset();
827
+ exercisesContainer.innerHTML = '';
828
+ addExercise();
829
+ selectedDays = [];
830
+ selectedWorkoutId = null;
831
 
832
+ // Set current date
833
+ document.getElementById('workout-date').value = new Date().toISOString().split('T')[0];
834
+ });
835
 
836
+ satisfactionSlider.addEventListener('input', () => {
837
+ satisfactionValue.textContent = `${satisfactionSlider.value}%`;
838
+ });
 
 
 
 
 
 
 
 
 
 
839
 
840
+ closeModalBtn.addEventListener('click', () => {
841
+ workoutModal.classList.add('hidden');
842
+ });
843
 
844
+ deleteWorkoutBtn.addEventListener('click', () => {
845
+ if (confirm('Supprimer cette séance ?')) {
846
+ deleteWorkout(selectedWorkoutId);
847
+ workoutModal.classList.add('hidden');
848
+ }
849
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
850
 
851
+ editWorkoutBtn.addEventListener('click', () => {
852
+ const workout = workouts.find(w => w.id === selectedWorkoutId);
853
+ if (!workout) return;
854
+
855
+ // Fill form with workout data
856
+ document.getElementById('workout-name').value = workout.name;
857
+ document.getElementById('workout-date').value = workout.date;
858
+ document.getElementById('workout-duration').value = workout.duration;
859
+ document.getElementById('workout-satisfaction').value = workout.satisfaction;
860
+ satisfactionValue.textContent = `${workout.satisfaction}%`;
861
+ document.getElementById('intensity-drop').checked = workout.intensityDrop;
862
+
863
+ // Clear exercises and add current ones
864
+ exercisesContainer.innerHTML = '';
865
+
866
+ workout.exercises.forEach(ex => {
867
+ addExercise();
868
+ const lastExercise = exercisesContainer.lastElementChild;
869
+ lastExercise.querySelector('.exercise-name').value = ex.name;
870
+ lastExercise.querySelector('.exercise-sets').value = ex.sets;
871
+ lastExercise.querySelector('.exercise-weight').value = ex.weight;
872
+ lastExercise.querySelector('.exercise-unilateral').checked = ex.unilateral;
873
+ });
874
 
875
+ // Set multi-day selection if applicable
876
+ if (workout.multiDayDates && workout.multiDayDates.length > 1) {
877
+ selectedDays = workout.multiDayDates;
878
+ multiDayColorSelect.value = workout.multiDayColor || 'bg-green-500/30';
879
+ }
880
 
881
+ workoutModal.classList.add('hidden');
882
+ });
 
883
 
884
+ exportBtn.addEventListener('click', exportData);
885
+ importBtn.addEventListener('click', importData);
886
+ closeDataModalBtn.addEventListener('click', () => dataModal.classList.add('hidden'));
887
+ confirmDataActionBtn.addEventListener('click', confirmDataAction);
888
+
889
+ authBtn.addEventListener('click', toggleAuth);
890
+ closeAuthModalBtn.addEventListener('click', () => authModal.classList.add('hidden'));
891
+ googleAuthBtn.addEventListener('click', () => {
892
+ // Simulate successful auth
893
+ isAuthenticated = true;
894
+ localStorage.setItem('isAuthenticated', 'true');
895
+ updateAuthUI();
896
+ authModal.classList.add('hidden');
897
+ });
898
 
899
+ clearSelectionBtn.addEventListener('click', () => {
900
+ selectedDays = [];
901
+ const selectedCells = document.querySelectorAll('.ring-2.ring-primary');
902
+ selectedCells.forEach(cell => cell.classList.remove('ring-2', 'ring-primary'));
903
+ });
904
+
905
+ applyMultiDayBtn.addEventListener('click', () => {
906
+ if (selectedDays.length === 0) {
907
+ alert('Sélectionnez au moins un jour');
908
  return;
909
  }
910
 
911
+ // Just update the selection UI, actual multi-day will be handled on form submit
912
+ alert(`${selectedDays.length} jours sélectionnés pour une séance sur plusieurs jours`);
913
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
914
 
915
+ searchWorkoutsInput.addEventListener('input', (e) => {
916
+ renderWorkouts(e.target.value);
917
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
 
919
+ clearSearchBtn.addEventListener('click', () => {
920
+ searchWorkoutsInput.value = '';
921
+ renderWorkouts();
922
+ });
923
+
924
+ // Initialize the app
925
+ init();
926
  </script>
927
  <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>
928
  </html>