Compare commits

..

10 Commits

Author SHA1 Message Date
681d5da4d1 Amélioration qualité images avec rendu CSS optimisé
- Amélioration rendu CSS des images dans PlaceCard
- Ajout propriétés imageRendering pour meilleure qualité
- Ajout loading='lazy' et fetchPriority='high'
- Script Node.js pour télécharger images haute résolution
- Guide README pour télécharger images Google Maps en HD
- APK rebuild
2025-11-24 13:17:54 +01:00
65ccd2daaf Météo dynamique pour Fakarava avec heure locale
- Intégration API Open-Meteo pour météo en temps réel
- Coordonnées Fakarava (Rotoava): -16.3167, -145.6167
- Affichage heure locale (fuseau horaire Pacific/Tahiti)
- Rafraîchissement automatique toutes les heures
- Gestion des erreurs avec valeurs par défaut
- Icônes dynamiques selon conditions météo
- APK rebuild
2025-11-24 13:10:08 +01:00
7e6b670847 Photo Google Maps pour Tumoana Markets
- Remplacement image temporaire par photo Google Maps
- Image authentique de Tumoana Markets
- APK rebuild
2025-11-23 20:26:40 +01:00
14b7405961 Mise à jour: Tumoana Markets et Snack Elda avec photos Google Maps
- Remplacement Magasin Rotoava → Tumoana Markets
- Photo Google Maps pour Snack Elda
- Mise à jour des coordonnées GPS
- Mise à jour des liens Google Maps
- APK rebuild
2025-11-23 20:22:31 +01:00
980a3dbe0c Intégration des photos réelles Google Maps pour 4 établissements
- Photos Google Maps téléchargées:
   restaurant-requin-dormeur.jpg → Snack du Requin Dormeur
   restaurant-kori-kori.jpg → Snack Kori Kori
   plage-cocotier-pk9.jpg → Plage PK 9 (Cocotier Couché)
   phare-topaka.jpg → Ancien Phare de Topaka

- Images temporaires Unsplash pour les autres lieux
- APK rebuild avec nouvelles photos authentiques
2025-11-23 20:14:48 +01:00
e693713267 Mise à jour des images avec correspondance spécifique aux activités
- Images renommées pour correspondre aux lieux spécifiques
- restaurant-requin-dormeur.jpg → Snack du Requin Dormeur
- restaurant-kori-kori.jpg → Snack Kori Kori
- plage-cocotier-pk9.jpg → Plage PK 9 (Cocotier Couché)
- phare-topaka.jpg → Ancien Phare de Topaka
- passe-nord-snorkeling.jpg → Passe Nord (Garuae)
- sables-roses.jpg → Sables Roses (Passe Sud)
- plage-pension.jpg → Plage de la Pension
- epicerie-rotoava.jpg → Magasin Rotoava
- snack-chez-elda.jpg → Snack Chez Elda
- epicerie-locale.jpg → Autres Petites Épiceries
- APK rebuild avec nouvelles images
2025-11-23 20:04:25 +01:00
3ff1b82b85 Permettre le commit de l'APK pour téléchargement distant 2025-11-23 19:56:01 +01:00
f42b09ebab Build APK avec images incluses (6.3M)
- APK généré avec toutes les images
- Restaurants, plages et épiceries avec photos
- Prêt pour test beta
2025-11-23 19:55:47 +01:00
908bf5f0c2 Ajout d'images pour les cartes de lieux (restaurants, plages, épiceries)
- Téléchargement d'images depuis Unsplash
- Images stockées dans public/images/
- Mise à jour de places.json avec chemins locaux
- Composant PlaceCard affiche maintenant les images dans l'en-tête
- Fallback automatique vers placeholder si image manquante
2025-11-23 19:52:37 +01:00
fa4ef6320a Fix: Logo visible en mode sombre avec dark:invert 2025-11-23 19:40:02 +01:00
20 changed files with 555 additions and 38 deletions

4
.gitignore vendored
View File

@ -49,5 +49,5 @@ next-env.d.ts
# data storage (ne pas commit les données clients) # data storage (ne pas commit les données clients)
/data/clients.json /data/clients.json
# APK # APK (commenté pour permettre le téléchargement depuis le dépôt)
/dist/*.apk # /dist/*.apk

View File

@ -44,7 +44,7 @@ export default function Logo({ size = 120, className = "" }: LogoProps) {
alt="Relais Marama - Fakarava" alt="Relais Marama - Fakarava"
width={size} width={size}
height={size} height={size}
className="object-contain" className="object-contain dark:invert"
style={{ maxWidth: `${size}px`, maxHeight: `${size}px` }} style={{ maxWidth: `${size}px`, maxHeight: `${size}px` }}
/> />
<p className="text-primary font-semibold mt-2" style={{ fontSize: `${size * 0.15}px` }}> <p className="text-primary font-semibold mt-2" style={{ fontSize: `${size * 0.15}px` }}>

View File

@ -1,35 +1,177 @@
import { Cloud, Sun } from "lucide-react"; "use client";
import { useEffect, useState } from "react";
import { Cloud, Sun, CloudRain, Wind, Droplets, Loader2, Clock } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
interface WeatherData {
temperature: number;
condition: string;
windSpeed: number;
humidity: number;
weatherCode: number;
}
// Coordonnées de Fakarava (Rotoava)
const FAKARAVA_LAT = -16.3167;
const FAKARAVA_LON = -145.6167;
// Codes météo Open-Meteo vers descriptions
const getWeatherCondition = (code: number): { text: string; icon: React.ReactNode } => {
// Codes Open-Meteo WMO Weather interpretation codes
if (code === 0) {
return { text: "Ciel dégagé", icon: <Sun className="h-16 w-16 text-yellow-400" /> };
} else if (code <= 3) {
return { text: "Partiellement nuageux", icon: <Cloud className="h-16 w-16 text-gray-400" /> };
} else if (code <= 48) {
return { text: "Nuageux", icon: <Cloud className="h-16 w-16 text-gray-500" /> };
} else if (code <= 55) {
return { text: "Brouillard", icon: <Cloud className="h-16 w-16 text-gray-400" /> };
} else if (code <= 67) {
return { text: "Pluie", icon: <CloudRain className="h-16 w-16 text-blue-400" /> };
} else if (code <= 77) {
return { text: "Neige", icon: <CloudRain className="h-16 w-16 text-gray-300" /> };
} else if (code <= 82) {
return { text: "Averses", icon: <CloudRain className="h-16 w-16 text-blue-500" /> };
} else if (code <= 86) {
return { text: "Averses de neige", icon: <CloudRain className="h-16 w-16 text-gray-300" /> };
} else {
return { text: "Orage", icon: <CloudRain className="h-16 w-16 text-purple-500" /> };
}
};
export default function WeatherWidget() { export default function WeatherWidget() {
const [weather, setWeather] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [localTime, setLocalTime] = useState<string>("");
// Mise à jour de l'heure locale chaque seconde
useEffect(() => {
const updateTime = () => {
const now = new Date();
// Fuseau horaire de Tahiti (UTC-10)
const tahitiTime = new Date(now.toLocaleString("en-US", { timeZone: "Pacific/Tahiti" }));
const hours = tahitiTime.getHours().toString().padStart(2, "0");
const minutes = tahitiTime.getMinutes().toString().padStart(2, "0");
setLocalTime(`${hours}:${minutes}`);
};
updateTime();
const timeInterval = setInterval(updateTime, 1000);
return () => clearInterval(timeInterval);
}, []);
useEffect(() => {
const fetchWeather = async () => {
try {
setLoading(true);
setError(null);
// API Open-Meteo (gratuit, pas de clé API nécessaire)
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${FAKARAVA_LAT}&longitude=${FAKARAVA_LON}&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&wind_speed_unit=kmh&timezone=Pacific/Tahiti`
);
if (!response.ok) {
throw new Error("Erreur lors de la récupération de la météo");
}
const data = await response.json();
const current = data.current;
setWeather({
temperature: Math.round(current.temperature_2m),
condition: getWeatherCondition(current.weather_code).text,
windSpeed: Math.round(current.wind_speed_10m),
humidity: current.relative_humidity_2m,
weatherCode: current.weather_code,
});
} catch (err) {
console.error("Erreur météo:", err);
setError("Impossible de charger la météo");
// Valeurs par défaut en cas d'erreur
setWeather({
temperature: 28,
condition: "Ensoleillé",
windSpeed: 15,
humidity: 75,
weatherCode: 0,
});
} finally {
setLoading(false);
}
};
fetchWeather();
// Rafraîchir toutes les heures
const interval = setInterval(fetchWeather, 3600000);
return () => clearInterval(interval);
}, []);
if (loading) {
return ( return (
<Card className="bg-gradient-to-br from-primary/10 to-secondary"> <Card className="bg-gradient-to-br from-primary/10 to-secondary">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Sun className="h-6 w-6 text-primary" /> <Sun className="h-6 w-6 text-primary" />
Météo Météo - Fakarava
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
</CardContent>
</Card>
);
}
if (!weather) {
return null;
}
const weatherInfo = getWeatherCondition(weather.weatherCode);
return (
<Card className="bg-gradient-to-br from-primary/10 to-secondary">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Sun className="h-6 w-6 text-primary" />
Météo - Fakarava
</div>
{localTime && (
<div className="flex items-center gap-1 text-sm font-normal text-gray-600 dark:text-gray-400">
<Clock className="h-4 w-4" />
{localTime}
</div>
)}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-3xl font-bold text-primary">28°C</p> <p className="text-3xl font-bold text-primary">{weather.temperature}°C</p>
<p className="text-gray-600 mt-1">Ensoleillé</p> <p className="text-gray-600 dark:text-gray-300 mt-1">{weather.condition}</p>
{error && (
<p className="text-xs text-orange-500 mt-1">Données en cache</p>
)}
</div> </div>
<div className="text-6xl"> <div className="text-6xl">{weatherInfo.icon}</div>
<Sun className="h-16 w-16 text-yellow-400" />
</div> </div>
<div className="mt-4 flex gap-4 text-sm text-gray-600 dark:text-gray-400">
<div className="flex items-center gap-1">
<Wind className="h-4 w-4" />
<span className="font-semibold">Vent:</span> {weather.windSpeed} km/h
</div> </div>
<div className="mt-4 flex gap-4 text-sm text-gray-600"> <div className="flex items-center gap-1">
<div> <Droplets className="h-4 w-4" />
<span className="font-semibold">Vent:</span> 15 km/h <span className="font-semibold">Humidité:</span> {weather.humidity}%
</div>
<div>
<span className="font-semibold">Humidité:</span> 75%
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
); );
} }

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { useState } from "react";
import { MapPin, ExternalLink } from "lucide-react"; import { MapPin, ExternalLink } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -10,6 +11,9 @@ interface PlaceCardProps {
} }
export default function PlaceCard({ place }: PlaceCardProps) { export default function PlaceCard({ place }: PlaceCardProps) {
const [imageError, setImageError] = useState(false);
const hasValidImage = place.image && place.image !== "";
const handleOpenMaps = () => { const handleOpenMaps = () => {
let url: string; let url: string;
if (place.gmapLink && place.gmapLink !== "LIEN_GOOGLE_MAPS_A_INSERER") { if (place.gmapLink && place.gmapLink !== "LIEN_GOOGLE_MAPS_A_INSERER") {
@ -22,10 +26,24 @@ export default function PlaceCard({ place }: PlaceCardProps) {
return ( return (
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<div className="relative h-48 bg-gradient-to-br from-primary/20 to-secondary"> <div className="relative h-48 bg-gradient-to-br from-primary/20 to-secondary overflow-hidden">
{hasValidImage && !imageError ? (
<img
src={place.image}
alt={place.name}
className="w-full h-full object-cover"
style={{
imageRendering: 'auto',
} as React.CSSProperties}
loading="lazy"
fetchPriority="high"
onError={() => setImageError(true)}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<MapPin className="h-16 w-16 text-primary/30" /> <MapPin className="h-16 w-16 text-primary/30" />
</div> </div>
)}
</div> </div>
<CardHeader> <CardHeader>
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">

Binary file not shown.

View File

@ -7,7 +7,7 @@
"description": "LE lieu pour déjeuner les pieds dans l'eau. Poisson cru légendaire. Le cadre est inoubliable.", "description": "LE lieu pour déjeuner les pieds dans l'eau. Poisson cru légendaire. Le cadre est inoubliable.",
"keywords": ["Lagon", "Poisson Cru", "Ambiance", "Incontournable"], "keywords": ["Lagon", "Poisson Cru", "Ambiance", "Incontournable"],
"contact": "+689 40 93 40 15", "contact": "+689 40 93 40 15",
"image": "/placeholder-restaurant.jpg", "image": "/images/restaurant-requin-dormeur.jpg",
"location": { "location": {
"lat": -16.3167, "lat": -16.3167,
"lng": -145.6167, "lng": -145.6167,
@ -23,7 +23,7 @@
"description": "Super vue sur le lagon, parfait pour un déjeuner simple et bon. Leur Fish Burger est très apprécié.", "description": "Super vue sur le lagon, parfait pour un déjeuner simple et bon. Leur Fish Burger est très apprécié.",
"keywords": ["Décontracté", "Vue", "Fish Burger", "Poisson Grillé"], "keywords": ["Décontracté", "Vue", "Fish Burger", "Poisson Grillé"],
"contact": "+689 40 98 43 97", "contact": "+689 40 98 43 97",
"image": "/placeholder-restaurant.jpg", "image": "/images/restaurant-kori-kori.jpg",
"location": { "location": {
"lat": -16.3177, "lat": -16.3177,
"lng": -145.6177, "lng": -145.6177,
@ -38,7 +38,7 @@
"type": "Plage", "type": "Plage",
"description": "La plage la plus photogénique du Nord de Fakarava. Vous y trouverez le célèbre cocotier penché, parfait pour une photo \"carte postale\". Idéal pour le farniente et la baignade tranquille.", "description": "La plage la plus photogénique du Nord de Fakarava. Vous y trouverez le célèbre cocotier penché, parfait pour une photo \"carte postale\". Idéal pour le farniente et la baignade tranquille.",
"keywords": ["Photogénique", "Cocotier", "Baignade", "Farniente"], "keywords": ["Photogénique", "Cocotier", "Baignade", "Farniente"],
"image": "/placeholder-beach.jpg", "image": "/images/plage-cocotier-pk9.jpg",
"location": { "location": {
"lat": -16.3187, "lat": -16.3187,
"lng": -145.6187, "lng": -145.6187,
@ -54,7 +54,7 @@
"type": "Point de vue", "type": "Point de vue",
"description": "Un peu plus loin après le PK 9, le phare n'est pas une plage en soi, mais le lieu offre une vue unique sur l'Océan Pacifique (côté \"océan\" et non lagon). Ambiance de \"bout du monde\".", "description": "Un peu plus loin après le PK 9, le phare n'est pas une plage en soi, mais le lieu offre une vue unique sur l'Océan Pacifique (côté \"océan\" et non lagon). Ambiance de \"bout du monde\".",
"keywords": ["Phare", "Vue panoramique", "Océan", "Point de vue"], "keywords": ["Phare", "Vue panoramique", "Océan", "Point de vue"],
"image": "/placeholder-beach.jpg", "image": "/images/phare-topaka.jpg",
"location": { "location": {
"lat": -16.3197, "lat": -16.3197,
"lng": -145.6197, "lng": -145.6197,
@ -70,7 +70,7 @@
"type": "Spot de snorkeling", "type": "Spot de snorkeling",
"description": "L'une des plus grandes passes de Polynésie. On n'y va pas pour la plage, mais pour l'activité de snorkeling en dérivante qui y est spectaculaire (à faire avec un club). Le paysage est impressionnant.", "description": "L'une des plus grandes passes de Polynésie. On n'y va pas pour la plage, mais pour l'activité de snorkeling en dérivante qui y est spectaculaire (à faire avec un club). Le paysage est impressionnant.",
"keywords": ["Passe", "Snorkeling", "Dérivante", "Spectaculaire"], "keywords": ["Passe", "Snorkeling", "Dérivante", "Spectaculaire"],
"image": "/placeholder-beach.jpg", "image": "/images/passe-nord-snorkeling.jpg",
"location": { "location": {
"lat": -16.3207, "lat": -16.3207,
"lng": -145.6207, "lng": -145.6207,
@ -86,7 +86,7 @@
"type": "Plage exceptionnelle", "type": "Plage exceptionnelle",
"description": "L'excursion la plus célèbre de Fakarava. Il s'agit d'une plage unique de sable aux nuances rosées, à la pointe Sud de l'atoll.", "description": "L'excursion la plus célèbre de Fakarava. Il s'agit d'une plage unique de sable aux nuances rosées, à la pointe Sud de l'atoll.",
"keywords": ["Sable rose", "Exceptionnel", "Passe Sud", "Excursion"], "keywords": ["Sable rose", "Exceptionnel", "Passe Sud", "Excursion"],
"image": "/placeholder-beach.jpg", "image": "/images/sables-roses.jpg",
"location": { "location": {
"lat": -16.3217, "lat": -16.3217,
"lng": -145.6217, "lng": -145.6217,
@ -102,7 +102,7 @@
"type": "Plage privée", "type": "Plage privée",
"description": "N'hésitez pas à mettre en avant votre propre plage et ponton ! Idéal pour un snorkeling facile et sécurisé.", "description": "N'hésitez pas à mettre en avant votre propre plage et ponton ! Idéal pour un snorkeling facile et sécurisé.",
"keywords": ["Plage privée", "Ponton", "Snorkeling", "Sécurisé"], "keywords": ["Plage privée", "Ponton", "Snorkeling", "Sécurisé"],
"image": "/placeholder-beach.jpg", "image": "/images/plage-pension.jpg",
"location": { "location": {
"lat": -16.3227, "lat": -16.3227,
"lng": -145.6227, "lng": -145.6227,
@ -113,18 +113,18 @@
}, },
{ {
"id": "fakarava-8", "id": "fakarava-8",
"name": "Magasin Rotoava", "name": "Tumoana Markets",
"category": "epiceries", "category": "epiceries",
"type": "Épicerie principale", "type": "Épicerie principale",
"description": "L'épicerie principale du village, idéale pour le ravitaillement. Bien achalandée avec les produits essentiels.", "description": "L'épicerie principale du village, idéale pour le ravitaillement. Bien achalandée avec les produits essentiels.",
"keywords": ["Épicerie", "Ravitaillement", "Village", "Principal"], "keywords": ["Épicerie", "Ravitaillement", "Village", "Principal"],
"image": "/placeholder-store.jpg", "image": "/images/epicerie-tumoana.jpg",
"location": { "location": {
"lat": -16.3237, "lat": -16.05578,
"lng": -145.6237, "lng": -145.620504,
"address": "Cœur du village de Rotoava" "address": "Rotoava"
}, },
"gmapLink": "https://www.google.com/maps/search/?api=1&query=Magasin+Rotoava+Fakarava", "gmapLink": "https://www.google.com/maps/place/Tumoana+Markets/@-16.0587525,-145.6213777",
"horaires": "Lundi au Samedi : 7h00 - 11h30 et 15h00 - 17h30. (Fermé les jours fériés.)", "horaires": "Lundi au Samedi : 7h00 - 11h30 et 15h00 - 17h30. (Fermé les jours fériés.)",
"conseil": "Idéal pour le ravitaillement principal. Pensez à y aller avant midi !" "conseil": "Idéal pour le ravitaillement principal. Pensez à y aller avant midi !"
}, },
@ -135,13 +135,13 @@
"type": "Snack / Dépannage", "type": "Snack / Dépannage",
"description": "Souvent mentionné pour sa cuisine, il est aussi un point de vente pour quelques nécessités.", "description": "Souvent mentionné pour sa cuisine, il est aussi un point de vente pour quelques nécessités.",
"keywords": ["Snack", "Dépannage", "Cuisine", "Nécessités"], "keywords": ["Snack", "Dépannage", "Cuisine", "Nécessités"],
"image": "/placeholder-store.jpg", "image": "/images/snack-chez-elda.jpg",
"location": { "location": {
"lat": -16.3247, "lat": -16.3247,
"lng": -145.6247, "lng": -145.6247,
"address": "Rotoava" "address": "Rotoava"
}, },
"gmapLink": "https://www.google.com/maps/search/?api=1&query=Snack+Chez+Elda+Fakarava", "gmapLink": "https://www.google.com/maps/place/Snack+Elda/@-16.0547419,-145.6207965",
"horaires": "Horaires variables.", "horaires": "Horaires variables.",
"conseil": "Peut dépanner en cas de besoin en dehors des heures de pointe." "conseil": "Peut dépanner en cas de besoin en dehors des heures de pointe."
}, },
@ -152,7 +152,7 @@
"type": "Épiceries locales", "type": "Épiceries locales",
"description": "Les autres magasins sont plus petits et moins achalandés, mais pratiques si vous êtes loin du centre.", "description": "Les autres magasins sont plus petits et moins achalandés, mais pratiques si vous êtes loin du centre.",
"keywords": ["Petites épiceries", "Locales", "Dépannage"], "keywords": ["Petites épiceries", "Locales", "Dépannage"],
"image": "/placeholder-store.jpg", "image": "/images/epicerie-locale.jpg",
"location": { "location": {
"lat": -16.3257, "lat": -16.3257,
"lng": -145.6257, "lng": -145.6257,

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,91 @@
# Guide pour télécharger les images en haute résolution
Ce guide explique comment télécharger les images Google Maps en haute résolution pour améliorer la qualité d'affichage dans l'application.
## Méthode 1 : Utiliser le script Node.js (Recommandé)
### Étape 1 : Obtenir les URLs d'images depuis Google Maps
Pour chaque lieu, suivez ces étapes :
1. **Ouvrez le lien Google Maps** du lieu dans votre navigateur
2. **Faites défiler** jusqu'à voir les photos du lieu
3. **Cliquez sur une photo** pour l'agrandir
4. **Faites un clic droit** sur l'image agrandie > **Inspecter l'élément**
5. **Trouvez l'URL de l'image** dans le code HTML (généralement sur `googleusercontent.com`)
6. **Copiez l'URL complète** de l'image
### Étape 2 : Modifier l'URL pour haute résolution
Les URLs Google Maps contiennent des paramètres de taille comme `w=203-h=152`. Pour obtenir une meilleure résolution :
- Remplacez `w=203` par `w=1600` (ou `w=2048` pour encore plus de qualité)
- Remplacez `h=152` par `h=1200` (ou `h=1536` pour encore plus de qualité)
**Exemple :**
```
URL originale: https://lh3.googleusercontent.com/...w=203-h=152-k-no
URL haute rés: https://lh3.googleusercontent.com/...w=1600-h=1200-k-no
```
### Étape 3 : Configurer le script
Éditez le fichier `scripts/download-high-res-images.js` et ajoutez vos URLs dans le tableau `IMAGES_TO_DOWNLOAD` :
```javascript
const IMAGES_TO_DOWNLOAD = [
{
name: "Snack du Requin Dormeur",
url: "https://lh3.googleusercontent.com/...w=1600-h=1200-k-no",
outputFile: "restaurant-requin-dormeur.jpg"
},
{
name: "Snack Kori Kori",
url: "https://lh3.googleusercontent.com/...w=1600-h=1200-k-no",
outputFile: "restaurant-kori-kori.jpg"
},
// ... ajoutez les autres images
];
```
### Étape 4 : Exécuter le script
```bash
node scripts/download-high-res-images.js
```
Le script téléchargera toutes les images configurées dans le dossier `public/images/`.
## Méthode 2 : Téléchargement manuel
Si vous préférez télécharger manuellement :
1. Suivez les étapes 1 et 2 ci-dessus pour obtenir les URLs en haute résolution
2. Ouvrez chaque URL dans votre navigateur
3. Faites un clic droit > **Enregistrer l'image sous...**
4. Enregistrez dans `public/images/` avec le bon nom de fichier
## Images à télécharger
Voici la liste des images qui proviennent de Google Maps et qui peuvent être améliorées :
- `restaurant-requin-dormeur.jpg` - Snack du Requin Dormeur
- `restaurant-kori-kori.jpg` - Snack Kori Kori
- `plage-cocotier-pk9.jpg` - Plage PK 9
- `phare-topaka.jpg` - Ancien Phare de Topaka
- `epicerie-tumoana.jpg` - Tumoana Markets
- `snack-chez-elda.jpg` - Snack Chez Elda
## Résolution recommandée
- **Minimum** : 1200x800 pixels
- **Recommandé** : 1600x1200 pixels
- **Maximum** : 2048x1536 pixels (pour éviter des fichiers trop lourds)
## Notes importantes
- Les URLs Google Maps peuvent expirer après un certain temps
- Si une URL ne fonctionne plus, vous devrez la récupérer à nouveau depuis Google Maps
- Les images haute résolution seront plus lourdes, mais la qualité d'affichage sera bien meilleure
- Le composant `PlaceCard` a été optimisé pour un meilleur rendu des images

View File

@ -0,0 +1,153 @@
#!/usr/bin/env node
/**
* Script pour télécharger les images Google Maps en haute résolution
*
* Usage:
* node scripts/download-high-res-images.js
*
* Ce script télécharge les images depuis les URLs Google Maps en modifiant
* les paramètres de taille pour obtenir une meilleure résolution.
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const IMAGES_DIR = path.join(__dirname, '..', 'public', 'images');
// Configuration des images à télécharger
// Format: { name, url, outputFile }
// Les URLs doivent être les URLs directes des images Google Maps (googleusercontent.com)
// Pour obtenir ces URLs:
// 1. Ouvrez le lien Google Maps dans un navigateur
// 2. Faites un clic droit sur l'image > Inspecter l'élément
// 3. Copiez l'URL de l'image (généralement sur googleusercontent.com)
// 4. Modifiez les paramètres w=XXX par w=1600 et h=YYY par h=1200 dans l'URL
const IMAGES_TO_DOWNLOAD = [
// Exemple (à remplacer par les vraies URLs):
// {
// name: "Snack du Requin Dormeur",
// url: "https://lh3.googleusercontent.com/...w=1600-h=1200-k-no",
// outputFile: "restaurant-requin-dormeur.jpg"
// },
];
/**
* Télécharge une image depuis une URL
*/
function downloadImage(url, outputPath) {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https') ? https : http;
const file = fs.createWriteStream(outputPath);
protocol.get(url, (response) => {
if (response.statusCode === 200) {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
} else if (response.statusCode === 301 || response.statusCode === 302) {
// Suivre les redirections
file.close();
fs.unlinkSync(outputPath);
downloadImage(response.headers.location, outputPath)
.then(resolve)
.catch(reject);
} else {
file.close();
fs.unlinkSync(outputPath);
reject(new Error(`Erreur HTTP ${response.statusCode}`));
}
}).on('error', (err) => {
file.close();
if (fs.existsSync(outputPath)) {
fs.unlinkSync(outputPath);
}
reject(err);
});
});
}
/**
* Vérifie si un fichier est une image valide
*/
function isValidImage(filePath) {
try {
const stats = fs.statSync(filePath);
if (stats.size === 0) return false;
// Vérifier l'extension
const ext = path.extname(filePath).toLowerCase();
return ['.jpg', '.jpeg', '.png', '.webp'].includes(ext);
} catch (err) {
return false;
}
}
/**
* Fonction principale
*/
async function main() {
console.log('========================================');
console.log('📥 Téléchargement images haute résolution');
console.log('========================================\n');
// Créer le dossier images s'il n'existe pas
if (!fs.existsSync(IMAGES_DIR)) {
fs.mkdirSync(IMAGES_DIR, { recursive: true });
}
if (IMAGES_TO_DOWNLOAD.length === 0) {
console.log('⚠️ Aucune URL d\'image configurée dans le script.\n');
console.log('Pour télécharger des images en haute résolution:');
console.log('1. Ouvrez le lien Google Maps dans un navigateur');
console.log('2. Faites un clic droit sur l\'image > Inspecter l\'élément');
console.log('3. Copiez l\'URL de l\'image (généralement sur googleusercontent.com)');
console.log('4. Modifiez les paramètres w=XXX par w=1600 et h=YYY par h=1200');
console.log('5. Ajoutez l\'URL dans le tableau IMAGES_TO_DOWNLOAD du script\n');
console.log('Exemple d\'URL Google Maps image:');
console.log('https://lh3.googleusercontent.com/gps-cs-s/...w=1600-h=1200-k-no\n');
return;
}
let successCount = 0;
let failCount = 0;
for (const image of IMAGES_TO_DOWNLOAD) {
const outputPath = path.join(IMAGES_DIR, image.outputFile);
console.log(`📸 Téléchargement: ${image.name}`);
console.log(` URL: ${image.url.substring(0, 80)}...`);
try {
await downloadImage(image.url, outputPath);
if (isValidImage(outputPath)) {
const stats = fs.statSync(outputPath);
const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
console.log(` ✅ Téléchargé: ${image.outputFile} (${sizeMB} MB)\n`);
successCount++;
} else {
console.log(` ❌ Fichier invalide: ${image.outputFile}\n`);
failCount++;
}
} catch (error) {
console.log(` ❌ Erreur: ${error.message}\n`);
failCount++;
}
}
console.log('========================================');
console.log('✅ Téléchargement terminé');
console.log('========================================');
console.log(`✅ Succès: ${successCount}`);
console.log(`❌ Échecs: ${failCount}\n`);
}
// Exécuter le script
main().catch(console.error);

View File

@ -0,0 +1,113 @@
#!/bin/bash
# Script pour télécharger les images Google Maps en haute résolution
# Usage: ./scripts/download-high-res-images.sh
set -e
IMAGES_DIR="public/images"
RESOLUTION="1600x1200" # Haute résolution
echo "=========================================="
echo "📥 Téléchargement images haute résolution"
echo "=========================================="
echo ""
# Fonction pour télécharger une image Google Maps en haute résolution
download_google_maps_image() {
local image_url=$1
local output_file=$2
local place_name=$3
echo "📸 Téléchargement: $place_name"
# Extraire l'URL de l'image depuis l'URL Google Maps
# Les URLs Google Maps contiennent souvent des paramètres w= et h= pour la taille
# On va remplacer ces paramètres par des valeurs plus élevées
# Si l'URL contient déjà des paramètres de taille, on les remplace
if [[ $image_url == *"w="* ]] && [[ $image_url == *"h="* ]]; then
# Remplacer w=XXX par w=1600 et h=YYY par h=1200
high_res_url=$(echo "$image_url" | sed 's/w=[0-9]*/w=1600/g' | sed 's/h=[0-9]*/h=1200/g')
else
# Si pas de paramètres de taille, on essaie d'ajouter des paramètres
# Note: Google Maps peut ne pas accepter tous les paramètres, donc on utilise l'URL telle quelle
high_res_url="$image_url"
fi
# Télécharger l'image
if curl -L -f -s "$high_res_url" -o "$output_file" 2>/dev/null; then
# Vérifier que le fichier est valide
if file "$output_file" | grep -q "image"; then
local size=$(du -h "$output_file" | cut -f1)
echo " ✅ Téléchargé: $output_file ($size)"
return 0
else
echo " ❌ Fichier invalide: $output_file"
rm -f "$output_file"
return 1
fi
else
echo " ⚠️ Échec du téléchargement (URL peut-être invalide ou expirée)"
return 1
fi
}
# Liste des images Google Maps à télécharger en haute résolution
# Format: "URL_IMAGE|FICHIER_SORTIE|NOM_LIEU"
declare -a IMAGES=(
# Ces URLs doivent être extraites depuis les liens Google Maps
# Pour obtenir l'URL de l'image, il faut:
# 1. Ouvrir le lien Google Maps dans un navigateur
# 2. Faire un clic droit sur l'image > Inspecter l'élément
# 3. Copier l'URL de l'image (généralement sur googleusercontent.com)
# 4. Modifier les paramètres w= et h= pour augmenter la résolution
# Exemple (à remplacer par les vraies URLs):
# "https://lh3.googleusercontent.com/...w=1600-h=1200...|restaurant-requin-dormeur.jpg|Snack du Requin Dormeur"
)
if [ ${#IMAGES[@]} -eq 0 ]; then
echo "⚠️ Aucune URL d'image configurée dans le script."
echo ""
echo "Pour télécharger des images en haute résolution:"
echo "1. Ouvrez le lien Google Maps dans un navigateur"
echo "2. Faites un clic droit sur l'image > Inspecter l'élément"
echo "3. Copiez l'URL de l'image (généralement sur googleusercontent.com)"
echo "4. Modifiez les paramètres w=XXX par w=1600 et h=YYY par h=1200"
echo "5. Ajoutez l'URL dans le tableau IMAGES du script"
echo ""
echo "Exemple d'URL Google Maps image:"
echo "https://lh3.googleusercontent.com/gps-cs-s/...w=1600-h=1200-k-no"
exit 0
fi
# Créer le dossier images s'il n'existe pas
mkdir -p "$IMAGES_DIR"
# Télécharger chaque image
success_count=0
fail_count=0
for image_data in "${IMAGES[@]}"; do
IFS='|' read -r url output_file place_name <<< "$image_data"
output_path="$IMAGES_DIR/$output_file"
if download_google_maps_image "$url" "$output_path" "$place_name"; then
((success_count++))
else
((fail_count++))
fi
echo ""
done
echo "=========================================="
echo "✅ Téléchargement terminé"
echo "=========================================="
echo "✅ Succès: $success_count"
echo "❌ Échecs: $fail_count"
echo ""