|
|
<!DOCTYPE html> |
|
|
<html lang="fr"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, viewport-fit=cover"> |
|
|
<title>Carte Météo - Architecture Modulaire</title> |
|
|
<meta name="description" content="Carte interactive météo pour le sud de la France avec spots personnalisés"> |
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/> |
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" integrity="sha384-lPzjPsFqXGR4j69hiC9fEY25qUp5wxLmEoI80K1Bz5P18D9XtSPs464N3wF8aLWlC8cXp" crossorigin=""/> |
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" integrity="sha384-5kSRZBJjZ+W2kZ/+huW4O7mQmDFTsM4VhO7sCGXkQs9j6iQeOJKsQYkXhJZSfS9S5X" crossorigin=""/> |
|
|
|
|
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
|
|
|
|
|
|
<link rel="prefetch" href="sw-enhanced.js"> |
|
|
|
|
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
|
|
|
<style> |
|
|
body { |
|
|
font-family: 'Inter', sans-serif; |
|
|
overflow: hidden; |
|
|
touch-action: pan-x pan-y; |
|
|
-webkit-user-select: none; |
|
|
-moz-user-select: none; |
|
|
-ms-user-select: none; |
|
|
user-select: none; |
|
|
} |
|
|
|
|
|
|
|
|
:focus { |
|
|
outline: 2px solid #10b981; |
|
|
outline-offset: 2px; |
|
|
} |
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) { |
|
|
*, *::before, *::after { |
|
|
animation-duration: 0.01ms !important; |
|
|
animation-iteration-count: 1 !important; |
|
|
transition-duration: 0.01ms !important; |
|
|
} |
|
|
} |
|
|
|
|
|
#map { |
|
|
height: 100vh; |
|
|
width: 100vw; |
|
|
z-index: 10; |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
} |
|
|
|
|
|
|
|
|
.mobile-panel { |
|
|
position: fixed; |
|
|
z-index: 20; |
|
|
background: rgba(255, 255, 255, 0.95); |
|
|
backdrop-filter: blur(10px); |
|
|
border-radius: 16px; |
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.2); |
|
|
border: 1px solid rgba(255,255,255,0.3); |
|
|
} |
|
|
|
|
|
.mobile-header { |
|
|
top: env(safe-area-inset-top, 12px); |
|
|
left: 12px; |
|
|
right: 12px; |
|
|
padding: 16px; |
|
|
max-height: 30vh; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.mobile-controls { |
|
|
bottom: env(safe-area-inset-bottom, 12px); |
|
|
left: 12px; |
|
|
right: 12px; |
|
|
padding: 16px; |
|
|
} |
|
|
|
|
|
|
|
|
.touch-button { |
|
|
min-height: 48px; |
|
|
min-width: 48px; |
|
|
padding: 12px 16px; |
|
|
border-radius: 12px; |
|
|
font-weight: 600; |
|
|
font-size: 15px; |
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
touch-action: manipulation; |
|
|
-webkit-tap-highlight-color: transparent; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.touch-button:active { |
|
|
transform: scale(0.96); |
|
|
} |
|
|
|
|
|
|
|
|
.carte-meteo { |
|
|
min-height: 48px; |
|
|
padding: 16px; |
|
|
margin: 12px 0; |
|
|
border-radius: 12px; |
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
touch-action: manipulation; |
|
|
} |
|
|
|
|
|
.carte-meteo:active { |
|
|
transform: scale(0.98); |
|
|
background-color: #f3f4f6; |
|
|
} |
|
|
|
|
|
|
|
|
.mobile-side-panel { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
right: -100%; |
|
|
width: 90vw; |
|
|
max-width: 420px; |
|
|
height: 100vh; |
|
|
background: white; |
|
|
z-index: 30; |
|
|
transition: right 0.4s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
overflow-y: auto; |
|
|
padding: env(safe-area-inset-top, 24px) 24px 24px; |
|
|
box-shadow: -10px 0 30px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
.mobile-side-panel.open { |
|
|
right: 0; |
|
|
} |
|
|
|
|
|
.mobile-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100vw; |
|
|
height: 100vh; |
|
|
background: rgba(0,0,0,0.6); |
|
|
z-index: 25; |
|
|
opacity: 0; |
|
|
visibility: hidden; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.mobile-overlay.show { |
|
|
opacity: 1; |
|
|
visibility: visible; |
|
|
} |
|
|
|
|
|
|
|
|
.data-warning { |
|
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); |
|
|
border: 2px solid #f59e0b; |
|
|
color: #92400e; |
|
|
padding: 12px 16px; |
|
|
border-radius: 12px; |
|
|
font-size: 14px; |
|
|
font-weight: 600; |
|
|
margin-bottom: 12px; |
|
|
animation: pulse 2s infinite; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.data-error { |
|
|
background: linear-gradient(135deg, #fef2f2 0%, #fecaca 100%); |
|
|
border: 2px solid #ef4444; |
|
|
color: #dc2626; |
|
|
} |
|
|
|
|
|
.data-success { |
|
|
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); |
|
|
border: 2px solid #10b981; |
|
|
color: #059669; |
|
|
} |
|
|
|
|
|
|
|
|
.loading-overlay { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100vw; |
|
|
height: 100vh; |
|
|
background: rgba(0,0,0,0.9); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
z-index: 50; |
|
|
color: white; |
|
|
backdrop-filter: blur(10px); |
|
|
} |
|
|
|
|
|
|
|
|
.connection-status { |
|
|
position: fixed; |
|
|
top: env(safe-area-inset-top, 12px); |
|
|
left: 12px; |
|
|
padding: 6px 12px; |
|
|
border-radius: 8px; |
|
|
font-size: 12px; |
|
|
font-weight: 600; |
|
|
z-index: 35; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
} |
|
|
|
|
|
.connection-status.online { |
|
|
background: #10b981; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.connection-status.offline { |
|
|
background: #ef4444; |
|
|
color: white; |
|
|
animation: pulse 1s infinite; |
|
|
} |
|
|
|
|
|
|
|
|
.module-status { |
|
|
position: fixed; |
|
|
bottom: 12px; |
|
|
right: 12px; |
|
|
background: rgba(0,0,0,0.8); |
|
|
color: white; |
|
|
padding: 8px 16px; |
|
|
border-radius: 8px; |
|
|
font-size: 12px; |
|
|
z-index: 60; |
|
|
backdrop-filter: blur(4px); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.module-status.ready { |
|
|
background: rgba(16, 185, 129, 0.9); |
|
|
} |
|
|
|
|
|
|
|
|
.custom-div-icon { |
|
|
background: transparent !important; |
|
|
border: none !important; |
|
|
} |
|
|
|
|
|
.custom-spot-marker { |
|
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
position: relative; |
|
|
font-size: 24px; |
|
|
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); |
|
|
} |
|
|
|
|
|
.custom-spot-marker:hover { |
|
|
transform: rotate(-15deg) scale(1.2) !important; |
|
|
} |
|
|
|
|
|
|
|
|
#add-spot-btn { |
|
|
box-shadow: 0 6px 20px rgba(0,0,0,0.25); |
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
} |
|
|
|
|
|
#add-spot-btn:hover { |
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.3); |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
|
|
|
#add-spot-btn:active { |
|
|
transform: translateY(0) scale(0.96); |
|
|
} |
|
|
|
|
|
|
|
|
#spot-modal, #import-export-modal { |
|
|
backdrop-filter: blur(8px); |
|
|
} |
|
|
|
|
|
|
|
|
.spot-type-option { |
|
|
cursor: pointer; |
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
padding: 12px; |
|
|
border: 2px solid transparent; |
|
|
border-radius: 12px; |
|
|
text-align: center; |
|
|
background: rgba(0,0,0,0.03); |
|
|
} |
|
|
|
|
|
.spot-type-option:hover { |
|
|
transform: translateY(-2px); |
|
|
background: rgba(0,0,0,0.08); |
|
|
} |
|
|
|
|
|
.spot-type-option input[type="radio"] { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.spot-type-option input:checked + label { |
|
|
border-color: #10b981; |
|
|
background: rgba(16, 185, 129, 0.15); |
|
|
transform: translateY(-3px); |
|
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2); |
|
|
} |
|
|
|
|
|
|
|
|
.custom-spot-popup .leaflet-popup-content-wrapper { |
|
|
border-radius: 16px; |
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
.custom-spot-popup .leaflet-popup-content { |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
|
|
|
.spot-controls { |
|
|
position: fixed; |
|
|
bottom: 24px; |
|
|
left: 12px; |
|
|
z-index: 30; |
|
|
} |
|
|
|
|
|
|
|
|
@keyframes pulse { |
|
|
0% { opacity: 1; } |
|
|
50% { opacity: 0.7; } |
|
|
100% { opacity: 1; } |
|
|
} |
|
|
|
|
|
@keyframes fadeIn { |
|
|
from { opacity: 0; transform: translateY(10px); } |
|
|
to { opacity: 1; transform: translateY(0); } |
|
|
} |
|
|
|
|
|
.fade-in { |
|
|
animation: fadeIn 0.3s ease-out forwards; |
|
|
} |
|
|
|
|
|
|
|
|
.stat-card { |
|
|
background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(249,250,251,0.9) 100%); |
|
|
border-radius: 12px; |
|
|
padding: 16px; |
|
|
text-align: center; |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.05); |
|
|
transition: transform 0.2s ease; |
|
|
} |
|
|
|
|
|
.stat-card:hover { |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 375px) { |
|
|
.touch-button { |
|
|
font-size: 13px; |
|
|
padding: 10px 12px; |
|
|
} |
|
|
|
|
|
.mobile-header h1 { |
|
|
font-size: 19px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-height: 500px) and (orientation: landscape) { |
|
|
.mobile-header { |
|
|
max-height: 25vh; |
|
|
} |
|
|
|
|
|
.mobile-controls { |
|
|
padding: 12px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@media (min-width: 768px) { |
|
|
.mobile-panel { |
|
|
position: absolute; |
|
|
border-radius: 20px; |
|
|
box-shadow: 0 12px 40px rgba(0,0,0,0.25); |
|
|
} |
|
|
|
|
|
.mobile-header { |
|
|
top: 24px; |
|
|
left: 24px; |
|
|
right: 24px; |
|
|
max-height: none; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.mobile-controls { |
|
|
bottom: 24px; |
|
|
left: 24px; |
|
|
right: 24px; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.mobile-side-panel { |
|
|
position: absolute; |
|
|
right: 24px; |
|
|
top: 140px; |
|
|
width: 380px; |
|
|
height: auto; |
|
|
max-height: 75vh; |
|
|
padding: 24px; |
|
|
border-radius: 20px; |
|
|
box-shadow: -15px 0 40px rgba(0,0,0,0.15); |
|
|
} |
|
|
|
|
|
.touch-button { |
|
|
min-height: 52px; |
|
|
padding: 14px 18px; |
|
|
font-size: 16px; |
|
|
border-radius: 14px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#recherche-spots { |
|
|
transition: all 0.3s ease; |
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05); |
|
|
} |
|
|
|
|
|
#recherche-spots:focus { |
|
|
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.2); |
|
|
border-color: #10b981; |
|
|
} |
|
|
|
|
|
|
|
|
.filtre-type { |
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.05); |
|
|
} |
|
|
|
|
|
.filtre-type:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
|
|
|
.modal-content { |
|
|
animation: modalSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; |
|
|
} |
|
|
|
|
|
@keyframes modalSlideIn { |
|
|
from { opacity: 0; transform: translateY(30px) scale(0.95); } |
|
|
to { opacity: 1; transform: translateY(0) scale(1); } |
|
|
} |
|
|
|
|
|
|
|
|
#day-slider { |
|
|
height: 6px; |
|
|
border-radius: 3px; |
|
|
background: #e5e7eb; |
|
|
} |
|
|
|
|
|
#day-slider::-webkit-slider-thumb { |
|
|
appearance: none; |
|
|
height: 20px; |
|
|
width: 20px; |
|
|
border-radius: 50%; |
|
|
background: #10b981; |
|
|
cursor: pointer; |
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
|
|
|
.btn-primary { |
|
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-primary:hover { |
|
|
background: linear-gradient(135deg, #059669 0%, #047857 100%); |
|
|
} |
|
|
|
|
|
.btn-secondary { |
|
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-secondary:hover { |
|
|
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); |
|
|
} |
|
|
|
|
|
.btn-warning { |
|
|
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-warning:hover { |
|
|
background: linear-gradient(135deg, #d97706 0%, #b45309 100%); |
|
|
} |
|
|
|
|
|
.btn-danger { |
|
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-danger:hover { |
|
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); |
|
|
} |
|
|
|
|
|
.btn-purple { |
|
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-purple:hover { |
|
|
background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%); |
|
|
} |
|
|
|
|
|
.btn-emerald { |
|
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-emerald:hover { |
|
|
background: linear-gradient(135deg, #059669 0%, #047857 100%); |
|
|
} |
|
|
|
|
|
.btn-gray { |
|
|
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-gray:hover { |
|
|
background: linear-gradient(135deg, #4b5563 0%, #374151 100%); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-100" role="application" aria-label="Carte Météo Interactive"> |
|
|
|
|
|
<div id="map" role="region" aria-label="Carte interactive" tabindex="-1"></div> |
|
|
|
|
|
|
|
|
<div id="connection-status" class="connection-status online" role="status" aria-live="polite"> |
|
|
<span aria-hidden="true">●</span> |
|
|
<span id="connection-text">En ligne</span> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="module-status" class="module-status" role="status" aria-live="polite"> |
|
|
<span aria-hidden="true">⚡</span> |
|
|
<span id="module-status-text">Initializing...</span> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="mobile-panel mobile-header fade-in" role="banner"> |
|
|
<h1 class="text-xl md:text-2xl font-bold text-gray-800 text-center mb-3" tabindex="-1"> |
|
|
🌦️ Météo Sud France - Modulaire |
|
|
</h1> |
|
|
|
|
|
|
|
|
<div id="data-status" class="data-warning hidden"> |
|
|
⚠️ Vérification des données météo... |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="grid grid-cols-3 gap-3 mb-4"> |
|
|
<div class="stat-card"> |
|
|
<div class="text-2xl font-bold text-green-600" id="villes-sans-pluie" aria-label="Nombre de villes sans pluie">0</div> |
|
|
<div class="text-xs text-gray-600 mt-1">au sec</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="text-2xl font-bold text-amber-600" id="temperature-max" aria-label="Température maximale">--°</div> |
|
|
<div class="text-xs text-gray-600 mt-1">max</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="text-2xl font-bold text-cyan-600" id="activites-ideales" aria-label="Nombre d'activités idéales">0</div> |
|
|
<div class="text-xs text-gray-600 mt-1">idéal</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="mb-3"> |
|
|
<label for="day-slider" class="text-sm font-medium text-gray-700 block text-center mb-2" id="date-label">Chargement...</label> |
|
|
<input type="range" min="0" max="6" value="0" class="w-full cursor-pointer" id="day-slider" disabled aria-describedby="date-label"> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="mobile-panel mobile-controls fade-in" role="navigation" aria-label="Navigation principale"> |
|
|
<div class="grid grid-cols-2 gap-3"> |
|
|
<button id="btn-sans-pluie" class="touch-button btn-primary" aria-label="Afficher les villes sans pluie" type="button"> |
|
|
☀️ Sec |
|
|
</button> |
|
|
<button id="btn-activites" class="touch-button btn-secondary" aria-label="Afficher les sites naturels" type="button"> |
|
|
📍 Sites |
|
|
</button> |
|
|
<button id="btn-semaine" class="touch-button btn-purple" aria-label="Afficher la météo sur 7 jours" type="button"> |
|
|
📅 Semaine |
|
|
</button> |
|
|
<button id="btn-itineraire" class="touch-button btn-warning" aria-label="Planifier un itinéraire" type="button"> |
|
|
🗺️ Route |
|
|
</button> |
|
|
<button id="btn-mes-spots" class="touch-button btn-emerald" aria-label="Afficher mes spots personnalisés" type="button"> |
|
|
🏚️ Mes Spots |
|
|
</button> |
|
|
<button id="btn-import-export" class="touch-button btn-gray" aria-label="Importer ou exporter des spots" type="button"> |
|
|
📤 Import/Export |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="mobile-overlay" class="mobile-overlay"></div> |
|
|
|
|
|
|
|
|
<div id="panneau-activites" class="mobile-side-panel" role="dialog" aria-labelledby="sites-panel-title" aria-modal="true" aria-hidden="true"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h3 id="sites-panel-title" class="font-bold">🗺️ Sites Naturels</h3> |
|
|
<button id="fermer-activites" class="touch-button bg-gray-200 text-gray-700 p-2 rounded-full w-8 h-8 flex items-center justify-center text-sm" aria-label="Fermer le panneau des sites" type="button">×</button> |
|
|
</div> |
|
|
|
|
|
<div class="mb-3"> |
|
|
<input type="text" id="recherche-spots" placeholder="🔍 Rechercher..." |
|
|
class="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:border-blue-500" aria-label="Rechercher des sites"> |
|
|
</div> |
|
|
|
|
|
<div id="filtres-activites" class="flex flex-wrap gap-1 mb-3"> |
|
|
<button class="filtre-type touch-button bg-blue-100 text-blue-700 text-xs px-2 py-1" data-type="cascade" type="button">💧 Cascade</button> |
|
|
<button class="filtre-type touch-button bg-red-100 text-red-700 text-xs px-2 py-1" data-type="thermes" type="button">♨️ Thermes</button> |
|
|
<button class="filtre-type touch-button bg-cyan-100 text-cyan-700 text-xs px-2 py-1" data-type="lac" type="button">🏊 Lac</button> |
|
|
<button class="filtre-type touch-button bg-amber-100 text-amber-700 text-xs px-2 py-1" data-type="plage" type="button">🏖️ Plage</button> |
|
|
<button class="filtre-type touch-button bg-purple-100 text-purple-700 text-xs px-2 py-1" data-type="gorges" type="button">🏔️ Gorges</button> |
|
|
<button class="filtre-type touch-button bg-gray-100 text-gray-700 text-xs px-2 py-1" data-type="grotte" type="button">🕳️ Grotte</button> |
|
|
<button class="filtre-type touch-button bg-teal-100 text-teal-700 text-xs px-2 py-1" data-type="piscine" type="button">💎 Piscine</button> |
|
|
<button class="filtre-type touch-button bg-pink-100 text-pink-700 text-xs px-2 py-1" data-type="vue" type="button">👁️ Vue</button> |
|
|
<button class="filtre-type touch-button bg-orange-100 text-orange-700 text-xs px-2 py-1" data-type="canyon" type="button">🪂 Canyon</button> |
|
|
</div> |
|
|
|
|
|
<div id="liste-activites" class="space-y-2"></div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="overlay-chargement" class="loading-overlay"> |
|
|
<div class="text-center max-w-sm mx-auto p-6"> |
|
|
<div class="animate-spin w-12 h-12 border-4 border-white border-t-transparent rounded-full mx-auto mb-6"></div> |
|
|
<div id="texte-chargement" class="font-semibold text-lg mb-2">Initialisation des modules...</div> |
|
|
<div class="text-sm opacity-80">Préparation de la carte météo</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<button id="add-spot-btn" class="fixed bottom-28 right-5 z-30 bg-gradient-to-r from-green-500 to-emerald-600 |
|
|
text-white rounded-full w-16 h-16 shadow-xl flex items-center justify-center |
|
|
transition-all duration-300 hover:from-green-600 hover:to-emerald-700"> |
|
|
<span class="text-3xl">+</span> |
|
|
</button> |
|
|
|
|
|
|
|
|
<div id="spot-modal" class="hidden fixed inset-0 z-40 bg-black bg-opacity-60 |
|
|
flex items-center justify-center p-4" role="dialog" aria-labelledby="spot-modal-title" aria-modal="true" aria-hidden="true"> |
|
|
<div class="modal-content bg-white rounded-2xl shadow-2xl max-w-md w-full max-h-[90vh] |
|
|
overflow-y-auto"> |
|
|
<div class="p-6"> |
|
|
<h2 id="spot-modal-title" class="text-2xl font-bold mb-5 flex items-center"> |
|
|
<span class="mr-3 text-2xl">📍</span> |
|
|
Ajouter un spot secret |
|
|
</h2> |
|
|
|
|
|
|
|
|
<form id="spot-form" class="space-y-5"> |
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2" for="spot-name"> |
|
|
Nom du spot* |
|
|
</label> |
|
|
<input type="text" id="spot-name" required |
|
|
class="w-full px-4 py-3 border rounded-xl focus:outline-none |
|
|
focus:border-green-500 focus:ring-2 focus:ring-green-200 transition-all" |
|
|
placeholder="Ex: Toit de l'ancienne usine"> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-semibold text-gray-700 mb-3" id="spot-type-label"> |
|
|
Type de spot* |
|
|
</label> |
|
|
<div id="spot-type-grid" class="grid grid-cols-4 gap-3" role="radiogroup" aria-labelledby="spot-type-label"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2" for="spot-description"> |
|
|
Description |
|
|
</label> |
|
|
<textarea id="spot-description" rows="3" |
|
|
class="w-full px-4 py-3 border rounded-xl focus:outline-none |
|
|
focus:border-green-500 focus:ring-2 focus:ring-green-200 transition-all" |
|
|
placeholder="Décris ce spot... Vue incroyable, bon pour les photos, etc."></textarea> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2" for="spot-access"> |
|
|
Comment y accéder? |
|
|
</label> |
|
|
<textarea id="spot-access" rows="2" |
|
|
class="w-full px-4 py-3 border rounded-xl focus:outline-none |
|
|
focus:border-green-500 focus:ring-2 focus:ring-green-200 transition-all" |
|
|
placeholder="Ex: Entrée par derrière, trou dans la clôture près du container bleu"></textarea> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex items-center"> |
|
|
<input type="checkbox" id="weather-sensitive" |
|
|
class="mr-3 h-5 w-5 text-green-600 rounded focus:ring-green-500"> |
|
|
<label for="weather-sensitive" class="text-sm font-medium"> |
|
|
⚠️ Éviter en cas de pluie |
|
|
</label> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2" for="best-time"> |
|
|
Meilleur moment |
|
|
</label> |
|
|
<select id="best-time" class="w-full px-4 py-3 border rounded-xl |
|
|
focus:outline-none focus:border-green-500 focus:ring-2 focus:ring-green-200 transition-all"> |
|
|
<option value="anytime">N'importe quand</option> |
|
|
<option value="sunrise">Lever du soleil</option> |
|
|
<option value="morning">Matin</option> |
|
|
<option value="afternoon">Après-midi</option> |
|
|
<option value="sunset">Coucher du soleil</option> |
|
|
<option value="night">Nuit</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2" for="spot-tags"> |
|
|
Tags (séparés par des virgules) |
|
|
</label> |
|
|
<input type="text" id="spot-tags" |
|
|
class="w-full px-4 py-3 border rounded-xl focus:outline-none |
|
|
focus:border-green-500 focus:ring-2 focus:ring-green-200 transition-all" |
|
|
placeholder="urbex, photo, rooftop, tranquille"> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex justify-end space-x-3 pt-6 border-t"> |
|
|
<button type="button" id="cancel-spot" |
|
|
class="px-5 py-3 text-gray-600 hover:text-gray-800 transition-colors font-medium rounded-xl hover:bg-gray-100"> |
|
|
Annuler |
|
|
</button> |
|
|
<button type="submit" |
|
|
class="px-5 py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-xl |
|
|
hover:from-green-600 hover:to-emerald-700 transition-all font-medium shadow-md hover:shadow-lg"> |
|
|
Créer le spot |
|
|
</button> |
|
|
</div> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="import-export-modal" class="hidden fixed inset-0 z-40 bg-black bg-opacity-60 |
|
|
flex items-center justify-center p-4" role="dialog" aria-labelledby="import-export-title" aria-modal="true" aria-hidden="true"> |
|
|
<div class="modal-content bg-white rounded-2xl shadow-2xl max-w-md w-full"> |
|
|
<div class="p-6"> |
|
|
<h2 id="import-export-title" class="text-2xl font-bold mb-5">📤 Import/Export des spots</h2> |
|
|
|
|
|
<div class="space-y-5"> |
|
|
|
|
|
<div class="border rounded-xl p-5 bg-gradient-to-br from-blue-50 to-indigo-50"> |
|
|
<h3 class="font-bold text-lg mb-2">Exporter mes spots</h3> |
|
|
<p class="text-sm text-gray-600 mb-4"> |
|
|
Télécharge tous tes spots secrets en fichier JSON |
|
|
</p> |
|
|
<button id="export-spots-btn" |
|
|
class="w-full bg-gradient-to-r from-blue-500 to-indigo-600 text-white py-3 rounded-xl hover:from-blue-600 hover:to-indigo-700 font-medium shadow-md hover:shadow-lg transition-all"> |
|
|
📥 Télécharger mes spots |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="border rounded-xl p-5 bg-gradient-to-br from-green-50 to-emerald-50"> |
|
|
<h3 class="font-bold text-lg mb-2">Importer des spots</h3> |
|
|
<p class="text-sm text-gray-600 mb-4"> |
|
|
Charge des spots depuis un fichier JSON |
|
|
</p> |
|
|
<input type="file" id="import-file" accept=".json" |
|
|
class="mb-3 text-sm w-full" aria-label="Sélectionner un fichier JSON"> |
|
|
<button id="import-spots-btn" |
|
|
class="w-full bg-gradient-to-r from-green-500 to-emerald-600 text-white py-3 rounded-xl hover:from-green-600 hover:to-emerald-700 font-medium shadow-md hover:shadow-lg transition-all" type="button"> |
|
|
📤 Importer des spots |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button type="button" id="close-import-export" |
|
|
class="mt-5 w-full px-4 py-3 text-gray-600 hover:text-gray-800 font-medium rounded-xl hover:bg-gray-100 transition-colors"> |
|
|
Fermer |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> |
|
|
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script> |
|
|
|
|
|
<script> |
|
|
|
|
|
class CDNFallbackManager { |
|
|
constructor() { |
|
|
this.cdnStatus = new Map(); |
|
|
} |
|
|
|
|
|
async initialize() { |
|
|
console.log('CDN Fallback Manager initialized'); |
|
|
return Promise.resolve(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const registerServiceWorker = () => { |
|
|
if ('serviceWorker' in navigator) { |
|
|
navigator.serviceWorker.register('sw-enhanced.js') |
|
|
.then(registration => { |
|
|
console.log('Enhanced SW registered:', registration); |
|
|
}) |
|
|
.catch(error => console.log('SW registration failed:', error)); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
class WeatherApp { |
|
|
constructor(config) { |
|
|
this.config = config; |
|
|
this.isInitialized = false; |
|
|
this.map = null; |
|
|
this.markers = []; |
|
|
this.activities = []; |
|
|
this.customSpots = JSON.parse(localStorage.getItem('customSpots') || '[]'); |
|
|
this.eventListeners = {}; |
|
|
|
|
|
|
|
|
this.mockCities = [ |
|
|
{ nom: "Montpellier", lat: 43.6108, lon: 3.8767, pluie: false, temperature: 28 }, |
|
|
{ nom: "Nîmes", lat: 43.8367, lon: 4.3601, pluie: false, temperature: 30 }, |
|
|
{ nom: "Perpignan", lat: 42.6887, lon: 2.8948, pluie: true, temperature: 25 }, |
|
|
{ nom: "Béziers", lat: 43.3447, lon: 3.2167, pluie: false, temperature: 29 }, |
|
|
{ nom: "Sète", lat: 43.4068, lon: 3.6972, pluie: false, temperature: 27 }, |
|
|
{ nom: "Carcassonne", lat: 43.2125, lon: 2.3533, pluie: true, temperature: 24 }, |
|
|
{ nom: "Narbonne", lat: 43.1833, lon: 3.0000, pluie: false, temperature: 28 } |
|
|
]; |
|
|
|
|
|
this.mockActivities = [ |
|
|
{ id: 1, nom: "Cascade de la Vis", type: "cascade", lat: 43.8, lon: 3.5, description: "Belle cascade dans les gorges de l'Hérault" }, |
|
|
{ id: 2, nom: "Thermes de Bagnères", type: "thermes", lat: 42.9, lon: 1.6, description: "Sources thermales naturelles" }, |
|
|
{ id: 3, nom: "Lac de Thau", type: "lac", lat: 43.4, lon: 3.6, description: "Lac méditerranéen avec îles" }, |
|
|
{ id: 4, nom: "Plage de Palavas", type: "plage", lat: 43.5, lon: 3.9, description: "Plage de sable fin" }, |
|
|
{ id: 5, nom: "Gorges du Tarn", type: "gorges", lat: 44.0, lon: 3.2, description: "Canyon spectaculaire" }, |
|
|
{ id: 6, nom: "Grotte de Clamouse", type: "grotte", lat: 43.7, lon: 3.3, description: "Grotte ornée de concrétions" }, |
|
|
{ id: 7, nom: "Piscine naturelle", type: "piscine", lat: 43.6, lon: 3.8, description: "Piscine de roche naturelle" }, |
|
|
{ id: 8, nom: "Point de vue du Pic", type: "vue", lat: 43.9, lon: 3.1, description: "Panorama sur la vallée" }, |
|
|
{ id: 9, nom: "Canyon de l'Ardèche", type: "canyon", lat: 44.1, lon: 4.3, description: "Descente en eau vive" } |
|
|
]; |
|
|
|
|
|
this.spotTypes = [ |
|
|
{ id: 'cascade', emoji: '💧', label: 'Cascade' }, |
|
|
{ id: 'thermes', emoji: '♨️', label: 'Thermes' }, |
|
|
{ id: 'lac', emoji: '🏊', label: 'Lac' }, |
|
|
{ id: 'plage', emoji: '🏖️', label: 'Plage' }, |
|
|
{ id: 'gorges', emoji: '🏔️', label: 'Gorges' }, |
|
|
{ id: 'grotte', emoji: '🕳️', label: 'Grotte' }, |
|
|
{ id: 'piscine', emoji: '💎', label: 'Piscine' }, |
|
|
{ id: 'vue', emoji: '👁️', label: 'Point de vue' }, |
|
|
{ id: 'canyon', emoji: '🪂', label: 'Canyon' }, |
|
|
{ id: 'urbex', emoji: '🏙️', label: 'Urbex' }, |
|
|
{ id: 'foret', emoji: '🌲', label: 'Forêt' }, |
|
|
{ id: 'montagne', emoji: '⛰️', label: 'Montagne' } |
|
|
]; |
|
|
|
|
|
this.init(); |
|
|
} |
|
|
|
|
|
on(event, callback) { |
|
|
if (!this.eventListeners[event]) { |
|
|
this.eventListeners[event] = []; |
|
|
} |
|
|
this.eventListeners[event].push(callback); |
|
|
} |
|
|
|
|
|
emit(event, data) { |
|
|
if (this.eventListeners[event]) { |
|
|
this.eventListeners[event].forEach(callback => callback({ detail: data })); |
|
|
} |
|
|
} |
|
|
|
|
|
async init() { |
|
|
try { |
|
|
await this.initMap(); |
|
|
this.initUI(); |
|
|
this.loadMockData(); |
|
|
this.setupEventListeners(); |
|
|
this.isInitialized = true; |
|
|
this.emit('appReady'); |
|
|
} catch (error) { |
|
|
console.error('Initialization failed:', error); |
|
|
this.emit('initializationFailed', error); |
|
|
} |
|
|
} |
|
|
|
|
|
async initMap() { |
|
|
return new Promise((resolve) => { |
|
|
this.map = L.map('map').setView(this.config.mapOptions.center, this.config.mapOptions.zoom); |
|
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { |
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' |
|
|
}).addTo(this.map); |
|
|
|
|
|
resolve(); |
|
|
}); |
|
|
} |
|
|
|
|
|
initUI() { |
|
|
|
|
|
const spotTypeGrid = document.getElementById('spot-type-grid'); |
|
|
if (spotTypeGrid) { |
|
|
spotTypeGrid.innerHTML = this.spotTypes.map(type => ` |
|
|
<div class="spot-type-option"> |
|
|
<input type="radio" id="type-${type.id}" name="spot-type" value="${type.id}"> |
|
|
<label for="type-${type.id}" class="block cursor-pointer"> |
|
|
<div class="text-2xl mb-1">${type.emoji}</div> |
|
|
<div class="text-xs font-medium">${type.label}</div> |
|
|
</label> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
|
|
|
this.updateStats(); |
|
|
|
|
|
|
|
|
const dateLabel = document.getElementById('date-label'); |
|
|
const daySlider = document.getElementById('day-slider'); |
|
|
if (dateLabel && daySlider) { |
|
|
const today = new Date(); |
|
|
dateLabel.textContent = today.toLocaleDateString('fr-FR', { |
|
|
weekday: 'long', |
|
|
day: 'numeric', |
|
|
month: 'long' |
|
|
}); |
|
|
daySlider.disabled = false; |
|
|
} |
|
|
} |
|
|
|
|
|
loadMockData() { |
|
|
|
|
|
this.mockCities.forEach(city => { |
|
|
const marker = L.marker([city.lat, city.lon]).addTo(this.map); |
|
|
marker.bindPopup(` |
|
|
<div class="font-bold text-lg">${city.nom}</div> |
|
|
<div class="mt-1">Température: ${city.temperature}°C</div> |
|
|
<div class="mt-1">${city.pluie ? '🌧️ Pluie' : '☀️ Ensoleillé'}</div> |
|
|
`); |
|
|
this.markers.push(marker); |
|
|
}); |
|
|
|
|
|
|
|
|
this.mockActivities.forEach(activity => { |
|
|
const marker = L.marker([activity.lat, activity.lon]).addTo(this.map); |
|
|
marker.bindPopup(` |
|
|
<div class="font-bold text-lg">${activity.nom}</div> |
|
|
<div class="mt-1 capitalize">Type: ${activity.type}</div> |
|
|
<div class="mt-2 text-sm">${activity.description}</div> |
|
|
`); |
|
|
this.activities.push(marker); |
|
|
}); |
|
|
|
|
|
|
|
|
this.customSpots.forEach(spot => { |
|
|
const marker = L.marker([spot.lat, spot.lon], { |
|
|
icon: L.divIcon({ |
|
|
className: 'custom-div-icon', |
|
|
html: '<div class="custom-spot-marker">📍</div>', |
|
|
iconSize: [30, 30] |
|
|
}) |
|
|
}).addTo(this.map); |
|
|
marker.bindPopup(` |
|
|
<div class="font-bold text-lg">${spot.name}</div> |
|
|
<div class="mt-1 capitalize">Type: ${spot.type}</div> |
|
|
<div class="mt-2 text-sm">${spot.description || ''}</div> |
|
|
`); |
|
|
}); |
|
|
} |
|
|
|
|
|
updateStats() { |
|
|
const citiesWithoutRain = this.mockCities.filter(city => !city.pluie).length; |
|
|
const maxTemp = Math.max(...this.mockCities.map(city => city.temperature)); |
|
|
const idealActivities = this.mockActivities.length; |
|
|
|
|
|
document.getElementById('villes-sans-pluie').textContent = citiesWithoutRain; |
|
|
document.getElementById('temperature-max').textContent = `${maxTemp}°`; |
|
|
document.getElementById('activites-ideales').textContent = idealActivities; |
|
|
} |
|
|
|
|
|
setupEventListeners() { |
|
|
|
|
|
document.getElementById('btn-sans-pluie')?.addEventListener('click', () => { |
|
|
this.showMessage('Affichage des villes sans pluie', 'info'); |
|
|
}); |
|
|
|
|
|
document.getElementById('btn-activites')?.addEventListener('click', () => { |
|
|
const panel = document.getElementById('panneau-activites'); |
|
|
const overlay = document.getElementById('mobile-overlay'); |
|
|
panel.classList.add('open'); |
|
|
overlay.classList.add('show'); |
|
|
panel.setAttribute('aria-hidden', 'false'); |
|
|
}); |
|
|
|
|
|
document.getElementById('btn-semaine')?.addEventListener('click', () => { |
|
|
this.showMessage('Affichage de la météo sur 7 jours', 'info'); |
|
|
}); |
|
|
|
|
|
document.getElementById('btn-itineraire')?.addEventListener('click', () => { |
|
|
this.showMessage('Planification d\'itinéraire', 'info'); |
|
|
}); |
|
|
|
|
|
document.getElementById('btn-mes-spots')?.addEventListener('click', () => { |
|
|
this.showMessage('Affichage de vos spots personnalisés', 'info'); |
|
|
}); |
|
|
|
|
|
document.getElementById('btn-import-export')?.addEventListener('click', () => { |
|
|
const modal = document.getElementById('import-export-modal'); |
|
|
modal.classList.remove('hidden'); |
|
|
modal.setAttribute('aria-hidden', 'false'); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('fermer-activites')?.addEventListener('click', () => { |
|
|
const panel = document.getElementById('panneau-activites'); |
|
|
const overlay = document.getElementById('mobile-overlay'); |
|
|
panel.classList.remove('open'); |
|
|
overlay.classList.remove('show'); |
|
|
panel.setAttribute('aria-hidden', 'true'); |
|
|
}); |
|
|
|
|
|
document.getElementById('mobile-overlay')?.addEventListener('click', () => { |
|
|
const panel = document.getElementById('panneau-activites'); |
|
|
const overlay = document.getElementById('mobile-overlay'); |
|
|
panel.classList.remove('open'); |
|
|
overlay.classList.remove('show'); |
|
|
panel.setAttribute('aria-hidden', 'true'); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('add-spot-btn')?.addEventListener('click', () => { |
|
|
const modal = document.getElementById('spot-modal'); |
|
|
modal.classList.remove('hidden'); |
|
|
modal.setAttribute('aria-hidden', 'false'); |
|
|
}); |
|
|
|
|
|
document.getElementById('cancel-spot')?.addEventListener('click', () => { |
|
|
const modal = document.getElementById('spot-modal'); |
|
|
modal.classList.add('hidden'); |
|
|
modal.setAttribute('aria-hidden', 'true'); |
|
|
}); |
|
|
|
|
|
document.getElementById('close-import-export')?.addEventListener('click', () => { |
|
|
const modal = document.getElementById('import-export-modal'); |
|
|
modal.classList.add('hidden'); |
|
|
modal.setAttribute('aria-hidden', 'true'); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('spot-form')?.addEventListener('submit', (e) => { |
|
|
e.preventDefault(); |
|
|
this.createCustomSpot(); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('export-spots-btn')?.addEventListener('click', () => { |
|
|
this.exportSpots(); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('import-spots-btn')?.addEventListener('click', () => { |
|
|
this.importSpots(); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.filtre-type').forEach(button => { |
|
|
button.addEventListener('click', (e) => { |
|
|
const type = e.target.dataset.type || e.target.parentElement.dataset.type; |
|
|
this.showMessage(`Filtrage par type: ${type}`, 'info'); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
createCustomSpot() { |
|
|
const name = document.getElementById('spot-name').value; |
|
|
const type = document.querySelector('input[name="spot-type"]:checked')?.value; |
|
|
|
|
|
if (!name || !type) { |
|
|
this.showMessage('Veuillez remplir tous les champs obligatoires', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const spot = { |
|
|
id: Date.now(), |
|
|
name, |
|
|
type, |
|
|
lat: 43.6108, |
|
|
lon: 3.8767, |
|
|
description: document.getElementById('spot-description').value, |
|
|
access: document.getElementById('spot-access').value, |
|
|
weatherSensitive: document.getElementById('weather-sensitive').checked, |
|
|
bestTime: document.getElementById('best-time').value, |
|
|
tags: document.getElementById('spot-tags').value.split(',').map(tag => tag.trim()).filter(tag => tag) |
|
|
}; |
|
|
|
|
|
this.customSpots.push(spot); |
|
|
localStorage.setItem('customSpots', JSON.stringify(this.customSpots)); |
|
|
|
|
|
|
|
|
document.getElementById('spot-modal').classList.add('hidden'); |
|
|
document.getElementById('spot-form').reset(); |
|
|
|
|
|
this.showMessage('Spot créé avec succès!', 'success'); |
|
|
} |
|
|
|
|
|
exportSpots() { |
|
|
const dataStr = JSON.stringify(this.customSpots, null, 2); |
|
|
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); |
|
|
|
|
|
const exportFileDefaultName = 'mes-spots-meteo.json'; |
|
|
|
|
|
const linkElement = document.createElement('a'); |
|
|
linkElement.setAttribute('href', dataUri); |
|
|
linkElement.setAttribute('download', exportFileDefaultName); |
|
|
linkElement.click(); |
|
|
|
|
|
this.showMessage('Export réussi!', 'success'); |
|
|
} |
|
|
|
|
|
importSpots() { |
|
|
const fileInput = document.getElementById('import-file'); |
|
|
const file = fileInput.files[0]; |
|
|
|
|
|
if (!file) { |
|
|
this.showMessage('Veuillez sélectionner un fichier', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (file.type !== 'application/json' && !file.name.endsWith('.json')) { |
|
|
this.showMessage('Veuillez sélectionner un fichier JSON valide', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = (e) => { |
|
|
try { |
|
|
const importedSpots = JSON.parse(e.target.result); |
|
|
if (Array.isArray(importedSpots)) { |
|
|
|
|
|
const validSpots = importedSpots.filter(spot => |
|
|
spot.name && spot.type && spot.lat && spot.lon |
|
|
); |
|
|
|
|
|
if (validSpots.length !== importedSpots.length) { |
|
|
console.warn('Some spots were invalid and skipped'); |
|
|
} |
|
|
|
|
|
this.customSpots = [...this.customSpots, ...validSpots]; |
|
|
localStorage.setItem('customSpots', JSON.stringify(this.customSpots)); |
|
|
this.showMessage(`Import réussi: ${validSpots.length} spots ajoutés`, 'success'); |
|
|
document.getElementById('import-export-modal').classList.add('hidden'); |
|
|
fileInput.value = ''; |
|
|
} else { |
|
|
this.showMessage('Format de fichier invalide', 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Import error:', error); |
|
|
this.showMessage('Erreur lors de l\'import: ' + (error.message || 'Fichier invalide'), 'error'); |
|
|
} |
|
|
}; |
|
|
|
|
|
reader.onerror = () => { |
|
|
this.showMessage('Erreur lors de la lecture du fichier', 'error'); |
|
|
}; |
|
|
|
|
|
reader.readAsText(file); |
|
|
} |
|
|
|
|
|
showMessage(message, type = 'info') { |
|
|
|
|
|
const notification = document.createElement('div'); |
|
|
notification.className = `fixed bottom-24 left-1/2 transform -translate-x-1/2 z-50 px-6 py-3 rounded-xl shadow-lg text-white font-medium max-w-md text-center fade-in`; |
|
|
|
|
|
switch(type) { |
|
|
case 'success': |
|
|
notification.classList.add('bg-green-500'); |
|
|
break; |
|
|
case 'error': |
|
|
notification.classList.add('bg-red-500'); |
|
|
break; |
|
|
case 'info': |
|
|
default: |
|
|
notification.classList.add('bg-blue-500'); |
|
|
} |
|
|
|
|
|
notification.textContent = message; |
|
|
document.body.appendChild(notification); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
notification.style.opacity = '0'; |
|
|
setTimeout(() => { |
|
|
document.body.removeChild(notification); |
|
|
}, 300); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
getPerformanceMetrics() { |
|
|
return { |
|
|
initializationTime: Date.now() - performance.timing.navigationStart, |
|
|
markerCount: this.markers.length, |
|
|
activityCount: this.activities.length, |
|
|
customSpotCount: this.customSpots.length |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => { |
|
|
|
|
|
const moduleStatus = document.getElementById('module-status'); |
|
|
const moduleStatusText = document.getElementById('module-status-text'); |
|
|
const updateModuleStatus = (message, isReady = false) => { |
|
|
moduleStatusText.textContent = message; |
|
|
if (isReady) { |
|
|
moduleStatus.classList.add('ready'); |
|
|
} |
|
|
}; |
|
|
|
|
|
try { |
|
|
updateModuleStatus('🔧 Initializing modules...'); |
|
|
|
|
|
|
|
|
const appConfig = { |
|
|
isMobile: window.innerWidth < 768, |
|
|
|
|
|
|
|
|
weatherOptions: { |
|
|
timeout: /Mobi|Android/i.test(navigator.userAgent) ? 5000 : 8000, |
|
|
maxRetries: 1, |
|
|
cacheDuration: 3600000 |
|
|
}, |
|
|
|
|
|
mapOptions: { |
|
|
center: [43.7, 3.5], |
|
|
zoom: /Mobi|Android/i.test(navigator.userAgent) ? 6 : 7, |
|
|
clusterRadius: /Mobi|Android/i.test(navigator.userAgent) ? 40 : 50 |
|
|
}, |
|
|
|
|
|
features: { |
|
|
offlineMode: true, |
|
|
weeklyStats: true, |
|
|
routePlanning: true, |
|
|
backgroundSync: 'serviceWorker' in navigator |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const app = new WeatherApp(appConfig); |
|
|
|
|
|
|
|
|
window.weatherApp = app; |
|
|
|
|
|
|
|
|
app.on('initializationFailed', (e) => { |
|
|
console.error('App initialization failed:', e.detail); |
|
|
updateModuleStatus('❌ Initialization failed'); |
|
|
}); |
|
|
|
|
|
app.on('citySelected', (e) => { |
|
|
console.log('City selected:', e.detail.city.nom); |
|
|
}); |
|
|
|
|
|
app.on('activitySelected', (e) => { |
|
|
console.log('Activity selected:', e.detail.activity.nom); |
|
|
}); |
|
|
|
|
|
app.on('routeUpdated', (e) => { |
|
|
console.log('Route updated, length:', e.detail.route.length); |
|
|
}); |
|
|
|
|
|
app.on('connectionChanged', (e) => { |
|
|
const status = e.detail.isOnline ? 'online' : 'offline'; |
|
|
console.log('Connection status:', status); |
|
|
}); |
|
|
|
|
|
|
|
|
const checkInitialization = () => { |
|
|
if (app.isInitialized) { |
|
|
updateModuleStatus('✅ Modules ready', true); |
|
|
|
|
|
|
|
|
const overlay = document.getElementById('overlay-chargement'); |
|
|
if (overlay) { |
|
|
overlay.style.opacity = '0'; |
|
|
setTimeout(() => overlay.style.display = 'none', 500); |
|
|
} |
|
|
|
|
|
|
|
|
const metrics = app.getPerformanceMetrics(); |
|
|
console.log('WeatherApp Performance:', metrics); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
if (moduleStatus) { |
|
|
moduleStatus.style.opacity = '0'; |
|
|
setTimeout(() => moduleStatus.style.display = 'none', 500); |
|
|
} |
|
|
}, 3000); |
|
|
|
|
|
} else { |
|
|
setTimeout(checkInitialization, 50); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
checkInitialization(); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
const overlay = document.getElementById('overlay-chargement'); |
|
|
if (overlay && overlay.style.display !== 'none') { |
|
|
console.warn('Force hiding loading overlay after timeout'); |
|
|
overlay.style.display = 'none'; |
|
|
updateModuleStatus('⚠️ Loaded with timeout'); |
|
|
} |
|
|
}, 10000); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Failed to initialize weather app:', error); |
|
|
updateModuleStatus('❌ Module error'); |
|
|
|
|
|
|
|
|
const dataStatus = document.getElementById('data-status'); |
|
|
if (dataStatus) { |
|
|
dataStatus.textContent = `❌ Erreur d'initialisation: ${error.message}`; |
|
|
dataStatus.classList.remove('hidden', 'data-warning', 'data-success'); |
|
|
dataStatus.classList.add('data-error'); |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('overlay-chargement').style.display = 'none'; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
window.addEventListener('orientationchange', () => { |
|
|
setTimeout(() => { |
|
|
if (window.weatherApp && window.weatherApp.map) { |
|
|
window.weatherApp.map.invalidateSize(); |
|
|
} |
|
|
}, 100); |
|
|
}); |
|
|
|
|
|
|
|
|
window.addEventListener('load', () => { |
|
|
console.log('Page loaded at:', Date.now()); |
|
|
}); |
|
|
</script> |
|
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=SAINTHALF/arpentage" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |