<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Journal de Sessions de Surf</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f0f9ff; /* Tailwind's sky-50 /
}
.calendar-day.has-session {
background-color: #2563eb; / Tailwind's blue-600 /
color: white;
border-radius: 50%;
position: relative;
}
.calendar-day.has-session::after {
content: '';
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
background-color: white;
border-radius: 50%;
}
.calendar-day:hover {
background-color: #dbeafe; / Tailwind's blue-100 /
}
.rating input[type="radio"] {
display: none;
}
.rating label {
cursor: pointer;
color: #cbd5e1; / Tailwind's slate-300 /
font-size: 1.75rem; / text-2xl or 3xl /
transition: color 0.2s;
}
.rating input[type="radio"]:checked ~ label,
.rating label:hover,
.rating label:hover ~ label {
color: #facc15; / Tailwind's yellow-400 /
}
/ Custom scrollbar for session list and stats /
.scrollable-container::-webkit-scrollbar {
width: 8px;
}
.scrollable-container::-webkit-scrollbar-track {
background: #e0f2fe; / sky-100 /
}
.scrollable-container::-webkit-scrollbar-thumb {
background-color: #38bdf8; / sky-500 /
border-radius: 10px;
border: 2px solid #e0f2fe; / sky-100 /
}
.stat-item {
padding: 0.5rem;
background-color: #f0f9ff; / sky-50 /
border-radius: 0.375rem; / rounded-md */
margin-bottom: 0.5rem;
}
</style>
</head>
<body class="text-slate-800">
<div class="container mx-auto p-4 md:p-8 max-w-6xl">
<header class="mb-8 text-center">
<h1 class="text-4xl font-bold text-sky-700">🏄 Mon Journal de Surf 🌊</h1>
</header>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="md:col-span-1 bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-2xl font-semibold mb-6 text-sky-600">Nouvelle Session</h2>
<form id="sessionForm" class="space-y-4">
<div>
<label for="sessionDate" class="block text-sm font-medium text-slate-700">Date</label>
<input type="date" id="sessionDate" name="sessionDate" required class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm p-2">
</div>
<div>
<label for="surfSpot" class="block text-sm font-medium text-slate-700">Spot de Surf</label>
<input type="text" id="surfSpot" name="surfSpot" required class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm p-2" placeholder="Ex: Parlementia">
</div>
<div>
<label for="waveSize" class="block text-sm font-medium text-slate-700">Taille des Vagues</label>
<input type="text" id="waveSize" name="waveSize" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm p-2" placeholder="Ex: 1.5m, épaule...">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Note de la Session</label>
<div class="rating flex flex-row-reverse justify-end mt-1" id="sessionRating">
<input type="radio" id="star5" name="rating" value="5"><label for="star5" title="5 étoiles"><i class="fas fa-star"></i></label>
<input type="radio" id="star4" name="rating" value="4"><label for="star4" title="4 étoiles"><i class="fas fa-star"></i></label>
<input type="radio" id="star3" name="rating" value="3"><label for="star3" title="3 étoiles"><i class="fas fa-star"></i></label>
<input type="radio" id="star2" name="rating" value="2"><label for="star2" title="2 étoiles"><i class="fas fa-star"></i></label>
<input type="radio" id="star1" name="rating" value="1" checked><label for="star1" title="1 étoile"><i class="fas fa-star"></i></label>
</div>
</div>
<h3 class="text-lg font-medium text-sky-600 pt-2">Conditions Windguru (manuel)</h3>
<div>
<label for="windSpeed" class="block text-sm font-medium text-slate-700">Vent (vitesse, direction)</label>
<div class="flex space-x-2">
<input type="text" id="windSpeed" name="windSpeed" class="mt-1 block w-1/2 rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm p-2" placeholder="Ex: 15kts">
<input type="text" id="windDirection" name="windDirection" class="mt-1 block w-1/2 rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm p-2" placeholder="Ex: NO">
</div>
</div>
<div>
<label for="swellHeight" class="block text-sm font-medium text-slate-700">Houle (taille, période, direction)</label>
<div class="flex space-x-2">
<input type="text" id="swellHeight" name="swellHeight" class="mt-1 block w-1/3 rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm p-2" placeholder="Ex: 2m">
<input type="text" id="swellPeriod" name="swellPeriod" class="mt-1 block w-1/3 rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm p-2" placeholder="Ex: 10s">
<input type="text" id="swellDirection" name="swellDirection" class="mt-1 block w-1/3 rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm p-2" placeholder="Ex: O">
</div>
</div>
<div>
<label for="notes" class="block text-sm font-medium text-slate-700">Notes</label>
<textarea id="notes" name="notes" rows="3" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm p-2" placeholder="Planche utilisée, sensations..."></textarea>
</div>
<button type="submit" class="w-full bg-sky-600 hover:bg-sky-700 text-white font-semibold py-2 px-4 rounded-md shadow-md transition duration-150 ease-in-out">
<i class="fas fa-plus-circle mr-2"></i>Ajouter Session
</button>
</form>
</div>
<div class="md:col-span-2 space-y-6">
<div class="bg-white p-6 rounded-xl shadow-lg">
<div class="flex justify-between items-center mb-4">
<button id="prevMonth" class="p-2 rounded-md hover:bg-sky-100 text-sky-600"><i class="fas fa-chevron-left"></i></button>
<h2 id="calendarMonthYear" class="text-xl font-semibold text-sky-600"></h2>
<button id="nextMonth" class="p-2 rounded-md hover:bg-sky-100 text-sky-600"><i class="fas fa-chevron-right"></i></button>
</div>
<div id="calendarGrid" class="grid grid-cols-7 gap-1 text-center">
<div class="font-medium text-xs text-slate-500">Dim</div>
<div class="font-medium text-xs text-slate-500">Lun</div>
<div class="font-medium text-xs text-slate-500">Mar</div>
<div class="font-medium text-xs text-slate-500">Mer</div>
<div class="font-medium text-xs text-slate-500">Jeu</div>
<div class="font-medium text-xs text-slate-500">Ven</div>
<div class="font-medium text-xs text-slate-500">Sam</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-2xl font-semibold mb-4 text-sky-600">Sessions Enregistrées</h2>
<div id="sessionList" class="space-y-4 scrollable-container max-h-[400px] overflow-y-auto pr-2">
<p class="text-slate-500 italic">Aucune session pour le moment.</p>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-lg">
<h2 class="text-2xl font-semibold mb-4 text-sky-600">Statistiques</h2>
<div id="statsContainer" class="space-y-4 scrollable-container max-h-[400px] overflow-y-auto pr-2">
<div>
<h3 class="text-lg font-medium text-sky-500 mb-2">Sessions par Mois :</h3>
<div id="statsByMonth" class="text-sm text-slate-700"></div>
</div>
<div>
<h3 class="text-lg font-medium text-sky-500 mb-2">Sessions par Spot :</h3>
<div id="statsBySpot" class="text-sm text-slate-700"></div>
</div>
<div>
<h3 class="text-lg font-medium text-sky-500 mb-2">Spots par Note :</h3>
<div id="statsByRatingSpot" class="text-sm text-slate-700"></div>
</div>
</div>
</div>
</div>
</div>
<div id="sessionModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
<div class="relative top-20 mx-auto p-5 border w-full max-w-lg shadow-lg rounded-md bg-white">
<div class="mt-3 text-center">
<h3 class="text-2xl leading-6 font-medium text-sky-700 mb-4" id="modalSessionTitle">Détails de la Session</h3>
<div class="mt-2 px-7 py-3 space-y-3 text-left" id="modalSessionDetails">
</div>
<div class="items-center px-4 py-3">
<button id="closeModal" class="px-4 py-2 bg-sky-600 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500">
Fermer
</button>
</div>
</div>
</div>
</div>
</div>
<script>
// Attend que le DOM soit entièrement chargé
document.addEventListener('DOMContentLoaded', () => {
const sessionForm = document.getElementById('sessionForm');
const sessionList = document.getElementById('sessionList');
const calendarGrid = document.getElementById('calendarGrid');
const calendarMonthYear = document.getElementById('calendarMonthYear');
const prevMonthButton = document.getElementById('prevMonth');
const nextMonthButton = document.getElementById('nextMonth');
const sessionModal = document.getElementById('sessionModal');
const closeModalButton = document.getElementById('closeModal');
const modalSessionTitle = document.getElementById('modalSessionTitle');
const modalSessionDetails = document.getElementById('modalSessionDetails');
const statsByMonthDiv = document.getElementById('statsByMonth');
const statsBySpotDiv = document.getElementById('statsBySpot');
const statsByRatingSpotDiv = document.getElementById('statsByRatingSpot');
// Définit la date actuelle dans le champ de date par défaut
const today = new Date().toISOString().split('T')[0];
document.getElementById('sessionDate').value = today;
let currentDate = new Date(); // Date pour la navigation du calendrier
let sessions = JSON.parse(localStorage.getItem('surfSessions')) || [];
// Fonction pour sauvegarder les sessions dans localStorage
const saveSessions = () => {
localStorage.setItem('surfSessions', JSON.stringify(sessions));
};
// Fonction pour afficher les sessions
const renderSessions = (sessionsToRender = sessions) => {
sessionList.innerHTML = ''; // Vide la liste actuelle
if (sessionsToRender.length === 0) {
sessionList.innerHTML = '<p class="text-slate-500 italic">Aucune session pour le moment ou pour cette date.</p>';
return;
}
// Trie les sessions par date (plus récente en premier)
const sortedSessions = [...sessionsToRender].sort((a, b) => new Date(b.date) - new Date(a.date));
sortedSessions.forEach((session) => {
const sessionElement = document.createElement('div');
sessionElement.classList.add('p-4', 'bg-sky-50', 'rounded-lg', 'shadow');
const sessionDateFormatted = new Date(session.date).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' });
let ratingStars = '';
for (let i = 0; i < 5; i++) {
ratingStars += `<i class="fas fa-star ${i < session.rating ? 'text-yellow-400' : 'text-slate-300'}"></i>`;
}
sessionElement.innerHTML = `
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold text-sky-700">${session.spot} - ${sessionDateFormatted}</h3>
<p class="text-sm text-slate-600">Vagues: ${session.waveSize || 'N/A'}</p>
<p class="text-sm text-slate-600">Note: <span class="ml-1">${ratingStars}</span></p>
</div>
<div>
<button data-id="${session.id}" class="view-button text-sky-500 hover:text-sky-700 mr-2"><i class="fas fa-eye"></i></button>
<button data-id="${session.id}" class="delete-button text-red-500 hover:text-red-700"><i class="fas fa-trash"></i></button>
</div>
</div>
`;
sessionList.appendChild(sessionElement);
});
};
// Fonction pour afficher le calendrier
const renderCalendar = () => {
calendarGrid.innerHTML = '';
const daysOfWeek = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'];
daysOfWeek.forEach(day => {
const dayEl = document.createElement('div');
dayEl.className = 'font-medium text-xs text-slate-500 py-2';
dayEl.textContent = day;
calendarGrid.appendChild(dayEl);
});
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
calendarMonthYear.textContent = `${currentDate.toLocaleDateString('fr-FR', { month: 'long' })} ${year}`;
const firstDayOfMonth = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
for (let i = 0; i < firstDayOfMonth; i++) {
const emptyCell = document.createElement('div');
calendarGrid.appendChild(emptyCell);
}
for (let day = 1; day <= daysInMonth; day++) {
const dayCell = document.createElement('div');
dayCell.textContent = day;
dayCell.classList.add('p-2', 'cursor-pointer', 'calendar-day', 'rounded-full', 'text-sm');
const cellDate = new Date(year, month, day);
const cellDateString = cellDate.toISOString().split('T')[0];
if (sessions.some(session => session.date === cellDateString)) {
dayCell.classList.add('has-session');
}
if (cellDateString === new Date().toISOString().split('T')[0]) {
dayCell.classList.add('bg-sky-200', 'text-sky-700', 'font-bold');
}
dayCell.addEventListener('click', () => {
document.getElementById('sessionDate').value = cellDateString;
const sessionsForDay = sessions.filter(session => session.date === cellDateString);
if (sessionsForDay.length > 0) {
renderSessions(sessionsForDay);
} else {
sessionList.innerHTML = `<p class="text-slate-500 italic">Aucune session enregistrée pour le ${cellDate.toLocaleDateString('fr-FR')}.</p>`;
}
});
calendarGrid.appendChild(dayCell);
}
};
// Fonctions pour calculer et afficher les statistiques
const renderStatistics = () => {
if (sessions.length === 0) {
statsByMonthDiv.innerHTML = '<p class="italic">Aucune donnée.</p>';
statsBySpotDiv.innerHTML = '<p class="italic">Aucune donnée.</p>';
statsByRatingSpotDiv.innerHTML = '<p class="italic">Aucune donnée.</p>';
return;
}
// Stats par mois
const sessionsByMonth = sessions.reduce((acc, session) => {
const monthYear = new Date(session.date).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long' });
acc[monthYear] = (acc[monthYear] || 0) + 1;
return acc;
}, {});
statsByMonthDiv.innerHTML = Object.entries(sessionsByMonth)
.sort((a,b) => new Date(b[0].split(" ")[1], getMonthIndex(b[0].split(" ")[0])) - new Date(a[0].split(" ")[1], getMonthIndex(a[0].split(" ")[0]))) // Trie par date décroissante
.map(([key, value]) => `<div class="stat-item">${key}: ${value} session(s)</div>`).join('');
// Stats par spot
const sessionsBySpot = sessions.reduce((acc, session) => {
acc[session.spot] = (acc[session.spot] || 0) + 1;
return acc;
}, {});
statsBySpotDiv.innerHTML = Object.entries(sessionsBySpot)
.sort((a,b) => b[1] - a[1]) // Trie par nombre de sessions décroissant
.map(([key, value]) => `<div class="stat-item">${key}: ${value} session(s)</div>`).join('');
// Stats spots par note
let statsByRatingHtml = '';
for (let i = 5; i >= 1; i--) { // De 5 étoiles à 1 étoile
const sessionsWithRating = sessions.filter(s => s.rating === i);
if (sessionsWithRating.length > 0) {
const spotsForRating = sessionsWithRating.reduce((acc, session) => {
acc[session.spot] = (acc[session.spot] || 0) + 1;
return acc;
}, {});
const spotsList = Object.entries(spotsForRating)
.sort((a,b) => b[1] - a[1]) // Trie par nombre de sessions décroissant
.map(([spot, count]) => `${spot} (${count})`)
.join(', ');
if (spotsList) {
statsByRatingHtml += `<div class="stat-item"><strong>${i} étoile(s):</strong> ${spotsList}</div>`;
}
}
}
statsByRatingSpotDiv.innerHTML = statsByRatingHtml || '<p class="italic">Aucune donnée de note.</p>';
};
// Helper function to get month index for sorting
const getMonthIndex = (monthName) => {
const months = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"];
return months.indexOf(monthName.toLowerCase());
};
// Gestionnaire de soumission du formulaire
sessionForm.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(sessionForm);
const ratingInput = document.querySelector('input[name="rating"]:checked');
const newSession = {
id: Date.now().toString(),
date: formData.get('sessionDate'),
spot: formData.get('surfSpot').trim() || "Spot inconnu", // Ajout d'une valeur par défaut
waveSize: formData.get('waveSize'),
rating: ratingInput ? parseInt(ratingInput.value) : 1,
windSpeed: formData.get('windSpeed'),
windDirection: formData.get('windDirection'),
swellHeight: formData.get('swellHeight'),
swellPeriod: formData.get('swellPeriod'),
swellDirection: formData.get('swellDirection'),
notes: formData.get('notes')
};
sessions.push(newSession);
saveSessions();
renderSessions();
renderCalendar();
renderStatistics(); // Met à jour les statistiques
sessionForm.reset();
document.getElementById('sessionDate').value = today;
document.getElementById('star1').checked = true;
});
// Gestionnaire pour les boutons de suppression et de vue
sessionList.addEventListener('click', (e) => {
if (e.target.closest('.delete-button')) {
const button = e.target.closest('.delete-button');
const sessionId = button.dataset.id;
if (confirm('Êtes-vous sûr de vouloir supprimer cette session ?')) {
sessions = sessions.filter(session => session.id !== sessionId);
saveSessions();
renderSessions();
renderCalendar();
renderStatistics(); // Met à jour les statistiques
}
}
if (e.target.closest('.view-button')) {
const button = e.target.closest('.view-button');
const sessionId = button.dataset.id;
const session = sessions.find(s => s.id === sessionId);
if (session) {
showSessionModal(session);
}
}
});
const showSessionModal = (session) => {
const sessionDateFormatted = new Date(session.date).toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
modalSessionTitle.textContent = `Session à ${session.spot}`;
let ratingStarsModal = '';
for (let i = 0; i < 5; i++) {
ratingStarsModal += `<i class="fas fa-star ${i < session.rating ? 'text-yellow-400' : 'text-slate-300'}"></i>`;
}
modalSessionDetails.innerHTML = `
<p><strong>Date :</strong> ${sessionDateFormatted}</p>
<p><strong>Spot :</strong> ${session.spot}</p>
<p><strong>Taille des vagues :</strong> ${session.waveSize || 'N/A'}</p>
<p><strong>Note :</strong> <span class="ml-1">${ratingStarsModal}</span></p>
<hr class="my-2">
<p class="font-semibold text-sky-600">Conditions Windguru :</p>
<p><strong>Vent :</strong> ${session.windSpeed || 'N/A'} ${session.windDirection || ''}</p>
<p><strong>Houle :</strong> ${session.swellHeight || 'N/A'} - ${session.swellPeriod || 'N/A'} - ${session.swellDirection || ''}</p>
<hr class="my-2">
<p><strong>Notes :</strong></p>
<p class="whitespace-pre-wrap">${session.notes || 'Aucune note.'}</p>
`;
sessionModal.classList.remove('hidden');
};
closeModalButton.addEventListener('click', () => {
sessionModal.classList.add('hidden');
});
sessionModal.addEventListener('click', (e) => {
if (e.target === sessionModal) {
sessionModal.classList.add('hidden');
}
});
prevMonthButton.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
});
nextMonthButton.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
});
// Initialisation de l'affichage
renderSessions();
renderCalendar();
renderStatistics(); // Affiche les statistiques au chargement
});
</script>
</body>
</html>