arpentage / index.html
SAINTHALF's picture
too large - Initial Deployment
09a3de1 verified
<!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">
<!-- Leaflet CSS -->
<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=""/>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Prefetch critical resources -->
<link rel="prefetch" href="sw-enhanced.js">
<!-- Google Fonts -->
<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 styles for accessibility */
:focus {
outline: 2px solid #10b981;
outline-offset: 2px;
}
/* Reduced motion support */
@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-first responsive design - same as original */
.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-friendly controls */
.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);
}
/* Improved touch targets */
.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 panels */
.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 reliability indicators */
.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 */
.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 */
.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 initialization indicator */
.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 spot markers */
.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;
}
/* Spot creation button */
#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 modals */
#spot-modal, #import-export-modal {
backdrop-filter: blur(8px);
}
/* Spot type selector */
.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 */
.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;
}
/* Import/export buttons in main UI */
.spot-controls {
position: fixed;
bottom: 24px;
left: 12px;
z-index: 30;
}
/* Animations */
@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;
}
/* Stats cards */
.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);
}
/* Responsive text sizing */
@media (max-width: 375px) {
.touch-button {
font-size: 13px;
padding: 10px 12px;
}
.mobile-header h1 {
font-size: 19px;
}
}
/* Landscape mobile adjustments */
@media (max-height: 500px) and (orientation: landscape) {
.mobile-header {
max-height: 25vh;
}
.mobile-controls {
padding: 12px;
}
}
/* Desktop overrides */
@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;
}
}
/* Enhanced search input */
#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;
}
/* Filter buttons */
.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 enhancements */
.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); }
}
/* Progress bar for slider */
#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);
}
/* Enhanced button states */
.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>
<!-- Connection Status -->
<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>
<!-- Module Status -->
<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>
<!-- Main Header Panel -->
<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>
<!-- Data Status Warning -->
<div id="data-status" class="data-warning hidden">
⚠️ Vérification des données météo...
</div>
<!-- Quick Stats -->
<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>
<!-- Date Slider -->
<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>
<!-- Control Panel -->
<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>
<!-- Mobile Overlay -->
<div id="mobile-overlay" class="mobile-overlay"></div>
<!-- Sites Panel (Mobile Slide-in) -->
<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">&times;</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>
<!-- Loading Overlay -->
<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>
<!-- Add Spot Button -->
<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>
<!-- Spot Creation Modal -->
<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 content -->
<form id="spot-form" class="space-y-5">
<!-- Name input -->
<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>
<!-- Type selection -->
<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">
<!-- Will be populated dynamically -->
</div>
</div>
<!-- Description -->
<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>
<!-- Access info -->
<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>
<!-- Weather sensitive -->
<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>
<!-- Best time -->
<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>
<!-- Tags -->
<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>
<!-- Buttons -->
<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>
<!-- Import/Export Modal -->
<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">
<!-- Export -->
<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>
<!-- Import -->
<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>
// Simulated CDN fallback manager
class CDNFallbackManager {
constructor() {
this.cdnStatus = new Map();
}
async initialize() {
console.log('CDN Fallback Manager initialized');
return Promise.resolve();
}
}
// Enhanced Service Worker simulation
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));
}
};
// Weather App Main Class
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 = {};
// Mock data
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.map);
resolve();
});
}
initUI() {
// Initialize spot type grid
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('');
}
// Update stats
this.updateStats();
// Setup date slider
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() {
// Add city markers
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);
});
// Add activity markers
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);
});
// Add custom spots
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() {
// Button event listeners
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');
});
// Close panel buttons
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');
});
// Spot creation
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');
});
// Spot form submission
document.getElementById('spot-form')?.addEventListener('submit', (e) => {
e.preventDefault();
this.createCustomSpot();
});
// Export spots
document.getElementById('export-spots-btn')?.addEventListener('click', () => {
this.exportSpots();
});
// Import spots
document.getElementById('import-spots-btn')?.addEventListener('click', () => {
this.importSpots();
});
// Activity filters
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, // Default to Montpellier
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));
// Close modal and reset form
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;
}
// Check file type
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)) {
// Validate spot structure
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 = ''; // Reset file input
} 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') {
// Create a temporary notification
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);
// Remove after 3 seconds
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
};
}
}
// Initialize application when DOM is ready
document.addEventListener('DOMContentLoaded', async () => {
// Update module status
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...');
// Configuration optimized for mobile/production use
const appConfig = {
isMobile: window.innerWidth < 768,
// Detect mobile device more accurately
weatherOptions: {
timeout: /Mobi|Android/i.test(navigator.userAgent) ? 5000 : 8000, // Reduced timeouts
maxRetries: 1, // Faster startup
cacheDuration: 3600000 // 1 hour
},
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
}
};
// Initialize the weather app
const app = new WeatherApp(appConfig);
// Make app globally available for debugging
window.weatherApp = app;
// Set up global event listeners for app events
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);
});
// Wait for initialization to complete with better timeout handling
const checkInitialization = () => {
if (app.isInitialized) {
updateModuleStatus('✅ Modules ready', true);
// Hide loading overlay immediately when app is ready
const overlay = document.getElementById('overlay-chargement');
if (overlay) {
overlay.style.opacity = '0';
setTimeout(() => overlay.style.display = 'none', 500);
}
// Log performance metrics
const metrics = app.getPerformanceMetrics();
console.log('WeatherApp Performance:', metrics);
// Hide module status after 3 seconds
setTimeout(() => {
if (moduleStatus) {
moduleStatus.style.opacity = '0';
setTimeout(() => moduleStatus.style.display = 'none', 500);
}
}, 3000);
} else {
setTimeout(checkInitialization, 50); // Check more frequently
}
};
// Start checking with additional safety timeout
checkInitialization();
// Safety timeout - force hide loading after 10 seconds regardless
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');
// Show error in UI
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');
}
// Hide loading overlay
document.getElementById('overlay-chargement').style.display = 'none';
}
});
// Handle orientation changes for mobile
window.addEventListener('orientationchange', () => {
setTimeout(() => {
if (window.weatherApp && window.weatherApp.map) {
window.weatherApp.map.invalidateSize();
}
}, 100);
});
// Performance monitoring
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>