import React, { useState, useEffect } from 'react';
import { Heart, Users, MapPin, Clock, Star, Euro, Film, Music, Coffee, Camera, Gamepad2, TreePine, Utensils, Car, Calendar, Sparkles, Moon, Sun, Navigation, Loader2 } from 'lucide-react'; // Importation de Loader2 pour l'animation de chargement
const SortiePlannerApp = () => {
// Déclaration des états React pour gérer les entrées utilisateur et les résultats
const [budget, setBudget] = useState(''); // Budget total
const [nbPersonnes, setNbPersonnes] = useState(1); // Nombre de personnes
const [ambiance, setAmbiance] = useState(''); // Ambiance souhaitée
const [location, setLocation] = useState('Paris, France'); // Localisation actuelle ou sélectionnée
const [currentTime, setCurrentTime] = useState(new Date()); // Heure actuelle pour l'affichage en temps réel
const [selectedDate, setSelectedDate] = useState(''); // Date personnalisée sélectionnée
const [selectedTime, setSelectedTime] = useState(''); // Heure personnalisée sélectionnée
const [isDateTimeCustom, setIsDateTimeCustom] = useState(false); // Indique si la date/heure est personnalisée
const [suggestions, setSuggestions] = useState([]); // Suggestions de programmes
const [showResults, setShowResults] = useState(false); // Afficher les résultats
const [selectedMood, setSelectedMood] = useState(''); // Ambiance sélectionnée (pour la coloration des boutons)
const [isLoading, setIsLoading] = useState(false); // État de chargement pour la planification
const [budgetError, setBudgetError] = useState(''); // Message d'erreur pour le budget
const [selectedAgeGroup, setSelectedAgeGroup] = useState(''); // Filtre par groupe d'âge
const [selectedGroupType, setSelectedGroupType] = useState(''); // Filtre par type de groupe (seul, couple, amis, famille)
// Liste des villes disponibles avec leur code et fuseau horaire (pour référence, non utilisé directement pour la logique de temps ici)
const villes = [
{ nom: 'Paris, France', code: 'paris', timezone: 'Europe/Paris' },
{ nom: 'Lyon, France', code: 'lyon', timezone: 'Europe/Paris' },
{ nom: 'Marseille, France', code: 'marseille', timezone: 'Europe/Paris' },
{ nom: 'Toulouse, France', code: 'toulouse', timezone: 'Europe/Paris' },
{ nom: 'Nice, France', code: 'nice', timezone: 'Europe/Nice' },
{ nom: 'Strasbourg, France', code: 'strasbourg', timezone: 'Europe/Paris' },
{ nom: 'Montpellier, France', code: 'montpellier', timezone: 'Europe/Paris' },
{ nom: 'Bordeaux, France', code: 'bordeaux', timezone: 'Europe/Paris' },
{ nom: 'Nantes, France', code: 'nantes', timezone: 'Europe/Paris' },
{ nom: 'Londres, UK', code: 'london', timezone: 'Europe/London' },
{ nom: 'Barcelona, Espagne', code: 'barcelona', timezone: 'Europe/Madrid' },
{ nom: 'Rome, Italie', code: 'rome', timezone: 'Europe/Rome' },
{ nom: 'Amsterdam, Pays-Bas', code: 'amsterdam', timezone: 'Europe/Amsterdam' },
{ nom: 'Berlin, Allemagne', code: 'berlin', timezone: 'Europe/Berlin' }
];
// Données des activités disponibles, classées par catégorie
const activites = {
cinema: [
{
id: 1,
nom: "UGC Ciné Cité",
type: "Cinéma",
prix: 12.50,
distance: "5 min",
rating: 4.3,
description: "Nouveau film Marvel - Spiderman 4",
horaires: ["18:30", "21:00", "23:15"],
ambiances: ["calme", "romantique", "detente"],
icon: "🎬",
villes: ['Paris', 'Lyon', 'Marseille'],
ageGroups: ["adultes", "ados", "famille"],
groupTypes: ["couple", "amis", "famille", "seul"]
},
{
id: 2,
nom: "Pathé Beaugrenelle",
type: "Cinéma",
prix: 14.00,
distance: "12 min",
rating: 4.5,
description: "Film romantique - Love Actually 2",
horaires: ["19:00", "21:30"],
ambiances: ["romantique", "calme"],
icon: "💕",
villes: ['Paris'],
ageGroups: ["adultes", "ados"],
groupTypes: ["couple", "amis", "seul"]
},
{
id: 30,
nom: "MK2 Nation",
type: "Cinéma Économique",
prix: 8.50,
distance: "8 min",
rating: 4.0,
description: "Films du moment, tarif étudiant",
horaires: ["18:00", "20:30", "22:45"],
ambiances: ["calme", "amis"],
icon: "🎞️",
villes: ['Paris'],
ageGroups: ["adultes", "ados", "etudiants"],
groupTypes: ["amis", "seul"]
},
{
id: 31,
nom: "IMAX Aquaboulevard",
type: "Cinéma Premium",
prix: 22.00,
distance: "20 min",
rating: 4.8,
description: "Expérience IMAX, son Dolby Atmos",
horaires: ["19:30", "22:00"],
ambiances: ["aventure", "fete"],
icon: "🎪",
villes: ['Paris'],
ageGroups: ["adultes", "ados", "famille"],
groupTypes: ["amis", "famille", "couple", "seul"]
}
],
restaurants: [
// ÉCONOMIQUE (5-15€)
{
id: 32,
nom: "McDonald's",
type: "Fast Food",
prix: 8.00,
distance: "3 min",
rating: 3.8,
description: "Menu Big Mac, livraison possible",
horaires: ["08:00-02:00"],
ambiances: ["calme", "amis"],
icon: "🍔",
villes: ['Paris', 'Lyon', 'Marseille', 'Toulouse', 'Nice', 'Strasbourg', 'Montpellier', 'Bordeaux', 'Nantes', 'Londres', 'Barcelona', 'Rome', 'Amsterdam', 'Berlin'],
ageGroups: ["tous"],
groupTypes: ["famille", "amis", "seul", "couple"]
},
{
id: 33,
nom: "Subway République",
type: "Sandwich",
prix: 9.50,
distance: "6 min",
rating: 4.1,
description: "Sandwich 30cm personnalisé + cookie",
horaires: ["09:00-23:00"],
ambiances: ["calme", "amis"],
icon: "🥪",
villes: ['Paris', 'Lyon', 'Marseille', 'Toulouse', 'Nice', 'Strasbourg', 'Montpellier', 'Bordeaux', 'Nantes', 'Londres', 'Barcelona', 'Rome', 'Amsterdam', 'Berlin'],
ageGroups: ["tous"],
groupTypes: ["famille", "amis", "seul", "couple"]
},
{
id: 34,
nom: "Crêperie Breizh",
type: "Crêperie",
prix: 12.00,
distance: "4 min",
rating: 4.4,
description: "Crêpe complète + cidre breton",
horaires: ["12:00-22:00"],
ambiances: ["calme", "romantique"],
icon: "🥞",
villes: ['Paris'],
ageGroups: ["tous"],
groupTypes: ["famille", "couple", "amis", "seul"]
},
{
id: 35,
nom: "Pho Saigon",
type: "Vietnamien",
prix: 13.50,
distance: "9 min",
rating: 4.6,
description: "Soupe Pho authentique, portions généreuses",
horaires: ["11:30-23:00"],
ambiances: ["calme", "amis"],
icon: "🍜",
villes: ['Paris'],
ageGroups: ["adultes", "ados"],
groupTypes: ["amis", "seul", "couple"]
},
// NOUVEAUX RESTAURANTS MARSEILLE
{
id: 62,
nom: "La Cantine du Midi",
type: "Restaurant Provençal",
prix: 18.00,
distance: "10 min",
rating: 4.3,
description: "Cuisine locale, ambiance chaleureuse",
horaires: ["12:00-14:30", "19:00-22:30"],
ambiances: ["calme", "amis"],
icon: "🥘",
villes: ['Marseille'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["amis", "famille", "couple"]
},
{
id: 63,
nom: "Le Cabanon de la Plage",
type: "Restaurant Bord de Mer",
prix: 28.00,
distance: "20 min",
rating: 4.6,
description: "Poissons frais, vue sur la mer, plage",
horaires: ["12:00-15:00", "19:00-23:00"],
ambiances: ["romantique", "detente", "calme"],
icon: "🏖️",
villes: ['Marseille'],
ageGroups: ["tous"],
groupTypes: ["couple", "famille", "amis"]
},
{
id: 64,
nom: "L'Epuisette",
type: "Restaurant Gastronomique Vue Mer",
prix: 90.00,
distance: "15 min",
rating: 4.9,
description: "Vue imprenable sur les rochers, cuisine étoilée",
horaires: ["19:30-22:00"],
ambiances: ["romantique", "luxe", "culture"],
icon: "✨",
villes: ['Marseille'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple"]
},
{
id: 65,
nom: "Chez Fonfon",
type: "Restaurant Bouillabaisse",
prix: 55.00,
distance: "8 min",
rating: 4.7,
description: "La vraie bouillabaisse marseillaise, bord de mer",
horaires: ["12:00-14:00", "19:00-22:00"],
ambiances: ["calme", "culture", "amis"],
icon: "🍲",
villes: ['Marseille'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["amis", "famille"]
},
{
id: 66,
nom: "La Crêperie du Vieux Port",
type: "Crêperie Vue Port",
prix: 15.00,
distance: "5 min",
rating: 4.2,
description: "Crêpes salées et sucrées avec vue sur le Vieux Port",
horaires: ["11:00-23:00"],
ambiances: ["calme", "famille", "amis"],
icon: "⚓",
villes: ['Marseille'],
ageGroups: ["tous"],
groupTypes: ["famille", "couple", "amis", "seul"]
},
{
id: 75,
nom: "Le Glacier du Cours",
type: "Glacier",
prix: 6.00,
distance: "2 min",
rating: 4.5,
description: "Glaces artisanales, parfums variés",
horaires: ["10:00-22:00"],
ambiances: ["calme", "detente"],
icon: "🍦",
villes: ['Paris', 'Marseille', 'Nice'],
ageGroups: ["tous"],
groupTypes: ["famille", "couple", "amis", "seul"]
},
{
id: 76,
nom: "Le Snack Provençal",
type: "Snack / Sandwicherie",
prix: 10.00,
distance: "4 min",
rating: 3.9,
description: "Paninis, salades, et jus frais",
horaires: ["09:00-19:00"],
ambiances: ["rapide", "calme"],
icon: "🥖",
villes: ['Marseille', 'Montpellier'],
ageGroups: ["tous"],
groupTypes: ["seul", "amis", "famille"]
},
// MILIEU DE GAMME (15-35€)
{
id: 36,
nom: "Bistrot du Coin",
type: "Bistrot Français",
prix: 22.00,
distance: "7 min",
rating: 4.3,
description: "Steak-frites maison, ambiance parisienne",
horaires: ["12:00-23:30"],
ambiances: ["calme", "romantique", "amis"],
icon: "🥩",
villes: ['Paris', 'Lyon', 'Toulouse', 'Bordeaux'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["amis", "couple"]
},
{
id: 37,
nom: "Sushi Zen",
type: "Japonais",
prix: 28.00,
distance: "11 min",
rating: 4.5,
description: "Menu sushi 20 pièces + miso",
horaires: ["18:30-23:00"],
ambiances: ["calme", "romantique"],
icon: "🍣",
villes: ['Paris', 'Lyon', 'Marseille'],
ageGroups: ["adultes", "ados"],
groupTypes: ["couple", "amis", "seul"]
},
{
id: 38,
nom: "La Taverne",
type: "Brasserie",
prix: 25.00,
distance: "5 min",
rating: 4.2,
description: "Plat du jour + dessert, terrasse chauffée",
horaires: ["11:00-01:00"],
ambiances: ["amis", "fete"],
icon: "🍺",
villes: ['Paris', 'Strasbourg'],
ageGroups: ["adultes"],
groupTypes: ["amis"]
},
{
id: 39,
nom: "Mama Mia",
type: "Italien",
prix: 19.50,
distance: "8 min",
rating: 4.4,
description: "Pizza artisanale + tiramisu",
horaires: ["19:00-23:30"],
ambiances: ["romantique", "amis"],
icon: "🍕",
villes: ['Paris', 'Rome', 'Nice'],
ageGroups: ["tous"],
groupTypes: ["famille", "couple", "amis"]
},
// HAUT DE GAMME (35-60€)
{
id: 3,
nom: "La Petite Table",
type: "Restaurant Romantique",
prix: 45.00,
distance: "8 min",
rating: 4.7,
description: "Cuisine française, ambiance tamisée",
horaires: ["19:00-23:00"],
ambiances: ["romantique", "calme"],
icon: "🕯️",
villes: ['Paris'],
ageGroups: ["adultes"],
groupTypes: ["couple"]
},
{
id: 40,
nom: "Le Gourmet",
type: "Gastronomique",
prix: 55.00,
distance: "15 min",
rating: 4.8,
description: "Menu 5 services, chef étoilé",
horaires: ["19:30-22:30"],
ambiances: ["romantique", "culture"],
icon: "⭐",
villes: ['Paris'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple"]
},
{
id: 41,
nom: "Steakhouse Premium",
type: "Viande Premium",
prix: 48.00,
distance: "12 min",
rating: 4.6,
description: "Côte de bœuf Wagyu, vin inclus",
horaires: ["18:00-23:00"],
ambiances: ["fete", "amis"],
icon: "🥩",
villes: ['Paris', 'Berlin'],
ageGroups: ["adultes"],
groupTypes: ["amis", "couple"]
},
// LUXE (60€+)
{
id: 4,
nom: "Le Rooftop 360",
type: "Restaurant Panoramique",
prix: 65.00,
distance: "15 min",
rating: 4.8,
description: "Vue sur Paris, cocktails signature",
horaires: ["18:00-02:00"],
ambiances: ["fete", "romantique"],
icon: "🌃",
villes: ['Paris'],
ageGroups: ["adultes"],
groupTypes: ["fete", "romantique"]
},
{
id: 42,
nom: "Le Palace",
type: "Restaurant Étoilé",
prix: 85.00,
distance: "25 min",
rating: 4.9,
description: "2 étoiles Michelin, menu dégustation",
horaires: ["20:00-22:00"],
ambiances: ["romantique", "culture"],
icon: "👑",
villes: ['Paris'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple"]
},
{
id: 43,
nom: "Sky Lounge",
type: "Bar Rooftop",
prix: 75.00,
distance: "18 min",
rating: 4.7,
description: "Cocktails premium, DJ set",
horaires: ["19:00-03:00"],
ambiances: ["fete", "romantique"],
icon: "🍸",
villes: ['Paris'],
ageGroups: ["adultes"],
groupTypes: ["fete", "romantique"]
}
],
divertissement: [
// ÉCONOMIQUE (0-20€)
{
id: 44,
nom: "Parc Montsouris",
type: "Parc",
prix: 0.00,
distance: "15 min",
rating: 4.3,
description: "Balade romantique, lac et verdure",
horaires: ["07:00-20:00"],
ambiances: ["calme", "romantique"],
icon: "🌳",
villes: ['Paris'],
ageGroups: ["tous"],
groupTypes: ["famille", "couple", "amis", "seul"]
},
{
id: 45,
nom: "Café Jeux Ludique",
type: "Bar à Jeux",
prix: 12.00,
distance: "10 min",
rating: 4.2,
description: "200+ jeux de société + boisson",
horaires: ["14:00-02:00"],
ambiances: ["amis", "calme"],
icon: "🎲",
villes: ['Paris', 'Lyon', 'Toulouse'],
ageGroups: ["adultes", "ados"],
groupTypes: ["amis", "couple"]
},
{
id: 46,
nom: "Karaoké Box",
type: "Karaoké",
prix: 15.00,
distance: "8 min",
rating: 4.0,
description: "Cabine privée 2h + cocktail",
horaires: ["19:00-03:00"],
ambiances: ["fete", "amis"],
icon: "🎤",
villes: ['Paris', 'Marseille', 'Nantes'],
ageGroups: ["adultes", "ados"],
groupTypes: ["amis", "famille"]
},
// NOUVEAUX DIVERTISSEMENTS MARSEILLE
{
id: 67,
nom: "Plage des Catalans",
type: "Plage Urbaine",
prix: 0.00,
distance: "7 min",
rating: 4.0,
description: "Plage de sable à proximité du centre, idéale pour la baignade",
horaires: ["08:00-20:00"],
ambiances: ["detente", "amis", "famille"],
icon: "⛱️",
villes: ['Marseille'],
ageGroups: ["tous"],
groupTypes: ["famille", "amis", "couple", "seul"]
},
{
id: 68,
nom: "Calanque de Sormiou",
type: "Calanque / Randonnée",
prix: 5.00, // Coût du parking si voiture
distance: "30 min",
rating: 4.9,
description: "Randonnée et baignade dans un cadre naturel exceptionnel, vue mer",
horaires: ["08:00-18:00"],
ambiances: ["aventure", "calme", "romantique"],
icon: "🏞️",
villes: ['Marseille'],
ageGroups: ["adultes", "ados", "seniors"],
groupTypes: ["couple", "amis", "seul"]
},
{
id: 69,
nom: "La Corniche Kennedy",
type: "Balade Panoramique",
prix: 0.00,
distance: "10 min",
rating: 4.7,
description: "Longue promenade le long de la mer, vues magnifiques",
horaires: ["06:00-23:00"],
ambiances: ["romantique", "calme", "detente"],
icon: "🚶♀️",
villes: ['Marseille'],
ageGroups: ["tous"],
groupTypes: ["couple", "famille", "seul"]
},
{
id: 77,
nom: "Parc Borély",
type: "Parc",
prix: 0.00,
distance: "10 min",
rating: 4.6,
description: "Grand parc avec jardin botanique et lac, idéal pour la détente",
horaires: ["07:00-21:00"],
ambiances: ["calme", "detente", "famille"],
icon: "🌳",
villes: ['Marseille'],
ageGroups: ["tous"],
groupTypes: ["famille", "couple", "amis", "seul"]
},
{
id: 78,
nom: "La Foire du Trône",
type: "Foire / Parc d'attractions",
prix: 10.00, // Entrée + quelques attractions
distance: "25 min",
rating: 4.0,
description: "Grande fête foraine annuelle, attractions pour tous les âges",
horaires: ["14:00-00:00"],
ambiances: ["fete", "aventure", "famille", "amis"],
icon: "🎡",
villes: ['Paris'], // Spécifique à Paris
ageGroups: ["tous"],
groupTypes: ["famille", "amis", "ados"]
},
// MILIEU DE GAMME (20-40€)
{
id: 6,
nom: "Bowling Strike",
type: "Bowling & Arcade",
prix: 18.00,
distance: "7 min",
rating: 4.2,
description: "Piste + chaussures + arcade",
horaires: ["18:00-02:00"],
ambiances: ["fete", "amis", "detente"],
icon: "🎳",
villes: ['Paris', 'Marseille', 'Montpellier'],
ageGroups: ["adultes", "ados", "famille"],
groupTypes: ["amis", "famille", "couple"]
},
{
id: 5,
nom: "Escape Game Mystery",
type: "Jeu d'Évasion",
prix: 28.00,
distance: "10 min",
rating: 4.6,
description: "Salle Sherlock Holmes - 60min",
horaires: ["18:00", "19:30", "21:00"],
ambiances: ["aventure", "amis"],
icon: "🔍",
villes: ['Paris', 'Marseille', 'Nice'],
ageGroups: ["adultes", "ados"],
groupTypes: ["amis", "famille", "couple"]
},
{
id: 47,
nom: "Laser Game Evolution",
type: "Laser Game",
prix: 22.00,
distance: "12 min",
rating: 4.3,
description: "Arène futuriste, parties 20min",
horaires: ["18:00-01:00"],
ambiances: ["aventure", "amis"],
icon: "🔫",
villes: ['Paris', 'Lyon', 'Toulouse', 'Bordeaux', 'Nantes'],
ageGroups: ["ados", "adultes"],
groupTypes: ["amis", "famille"]
},
{
id: 48,
nom: "Comedy Club",
type: "Spectacle Humour",
prix: 35.00,
distance: "14 min",
rating: 4.5,
description: "Stand-up + open mic, 2 boissons",
horaires: ["20:30", "22:30"],
ambiances: ["fete", "amis"],
icon: "😂",
villes: ['Paris', 'Berlin', 'Londres'],
ageGroups: ["adultes"],
groupTypes: ["amis", "couple"]
},
// HAUT DE GAMME (40€+)
{
id: 49,
nom: "Casino de Paris",
type: "Casino",
prix: 45.00,
distance: "20 min",
rating: 4.4,
description: "Poker, machines, restaurant inclus",
horaires: ["20:00-06:00"],
ambiances: ["fete", "aventure"],
icon: "🎰",
villes: ['Paris', 'Lyon', 'Nice'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["amis", "couple", "seul"]
},
{
id: 50,
nom: "Spa Privatif",
type: "Spa Duo",
prix: 80.00,
distance: "16 min",
rating: 4.8,
description: "Jacuzzi privé 2h + champagne",
horaires: ["18:00-23:00"],
ambiances: ["romantique", "calme"],
icon: "🛁",
villes: ['Paris', 'Marseille'],
ageGroups: ["adultes"],
groupTypes: ["couple"]
}
],
culture: [
// ÉCONOMIQUE (0-20€)
{
id: 51,
nom: "Bibliothèque François Mitterrand",
type: "Visite Culturelle",
prix: 0.00,
distance: "22 min",
rating: 4.5,
description: "Architecture moderne, expo gratuite",
horaires: ["09:00-20:00"],
ambiances: ["calme", "culture"],
icon: "📚",
villes: ['Paris'],
ageGroups: ["tous"],
groupTypes: ["seul", "amis", "famille"]
},
{
id: 52,
nom: "Musée Carnavalet",
type: "Musée Histoire",
prix: 8.00,
distance: "18 min",
rating: 4.3,
description: "Histoire de Paris, entrée réduite",
horaires: ["10:00-18:00"],
ambiances: ["culture", "calme"],
icon: "🏛️",
villes: ['Paris'],
ageGroups: ["tous"],
groupTypes: ["famille", "seul", "couple", "amis"]
},
{
id: 7,
nom: "Musée d'Orsay",
type: "Musée",
prix: 16.00,
distance: "20 min",
rating: 4.9,
description: "Exposition Van Gogh temporaire",
horaires: ["18:00", "21:45"], // Modified to reflect specific showing times more clearly
ambiances: ["calme", "romantique", "culture"],
icon: "🎨",
villes: ['Paris'],
ageGroups: ["adultes", "seniors", "ados"],
groupTypes: ["couple", "amis", "seul"]
},
// NOUVEAUX CULTURE MARSEILLE
{
id: 70,
nom: "Le Mucem",
type: "Musée Culturel",
prix: 12.00,
distance: "10 min",
rating: 4.7,
description: "Musée des civilisations de l'Europe et de la Méditerranée, architecture moderne",
horaires: ["10:00-19:00"],
ambiances: ["culture", "calme", "famille"],
icon: "🏛️",
villes: ['Marseille'],
ageGroups: ["tous"],
groupTypes: ["famille", "seul", "couple", "amis"]
},
{
id: 71,
nom: "Notre-Dame de la Garde",
type: "Monument / Vue Panoramique",
prix: 0.00,
distance: "15 min",
rating: 4.9,
description: "Visite de la basilique et vue à 360° sur Marseille et la mer",
horaires: ["07:00-18:30"],
ambiances: ["culture", "calme", "romantique"],
icon: "⛪",
villes: ['Marseille'],
ageGroups: ["tous"],
groupTypes: ["famille", "couple", "seul"]
},
{
id: 72,
nom: "Le Panier (Quartier Historique)",
type: "Visite de Quartier",
prix: 0.00,
distance: "5 min",
rating: 4.5,
description: "Balade dans le plus vieux quartier de Marseille, ruelles pittoresques",
horaires: ["09:00-22:00"],
ambiances: ["culture", "calme", "amis"],
icon: "🏘️",
villes: ['Marseille'],
ageGroups: ["tous"],
groupTypes: ["famille", "amis", "couple", "seul"]
},
// MILIEU DE GAMME (20-50€)
{
id: 53,
nom: "Cabaret Nouvelle Ève",
type: "Cabaret",
prix: 35.00,
distance: "16 min",
rating: 4.4,
description: "Spectacle + coupe de champagne",
horaires: ["21:00", "23:00"],
ambiances: ["romantique", "fete"],
icon: "💃",
villes: ['Paris'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple", "amis"]
},
{
id: 54,
nom: "Opéra Bastille",
type: "Opéra",
prix: 42.00,
distance: "19 min",
rating: 4.7,
description: "Carmen de Bizet, places catégorie 2",
horaires: ["20:00"],
ambiances: ["romantique", "culture"],
icon: "🎭",
villes: ['Paris'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple", "seul", "amis"]
},
// HAUT DE GAMME (50€+)
{
id: 8,
nom: "Théâtre du Châtelet",
type: "Spectacle",
prix: 55.00,
distance: "18 min",
rating: 4.8,
description: "Comédie musicale - Le Roi Lion",
horaires: ["20:00"],
ambiances: ["romantique", "culture", "fete"],
icon: "🎭",
villes: ['Paris'],
ageGroups: ["tous"],
groupTypes: ["famille", "couple", "amis"]
},
{
id: 55,
nom: "Dîner-Croisière Seine",
type: "Croisière",
prix: 85.00,
distance: "25 min",
rating: 4.6,
description: "Repas gastronomique, monuments illuminés",
horaires: ["20:30", "23:00"], // Added specific times
ambiances: ["romantique", "fete"],
icon: "🚢",
villes: ['Paris'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple", "amis"]
}
],
nightlife: [
// ÉCONOMIQUE (10-25€)
{
id: 56,
nom: "Le Pub O'Connell's",
type: "Pub Irlandais",
prix: 15.00,
distance: "6 min",
rating: 4.1,
description: "Bières pression, ambiance décontractée",
horaires: ["17:00-02:00"],
ambiances: ["amis", "calme"],
icon: "🍺",
villes: ['Paris', 'Londres', 'Berlin'],
ageGroups: ["adultes", "ados"],
groupTypes: ["amis", "seul"]
},
{
id: 57,
nom: "Wine Bar Le Vintage",
type: "Bar à Vin",
prix: 20.00,
distance: "9 min",
rating: 4.4,
description: "Dégustation 3 vins + fromages",
horaires: ["18:00-01:00"],
ambiances: ["romantique", "calme"],
icon: "🍷",
villes: ['Paris', 'Bordeaux'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple", "amis", "seul"]
},
{
id: 9,
nom: "Le Jazz Club",
type: "Bar Musical",
prix: 25.00,
distance: "12 min",
rating: 4.4,
description: "Concert jazz live + cocktails",
horaires: ["21:00-03:00"],
ambiances: ["romantique", "calme", "musique"],
icon: "🎷",
villes: ['Paris', 'Berlin'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple", "amis", "seul"]
},
// NOUVELLE VIE NOCTURNE MARSEILLE
{
id: 73,
nom: "Le Rooftop R2",
type: "Boîte de Nuit / Bar Panoramique",
prix: 20.00,
distance: "15 min",
rating: 4.5,
description: "Vue imprenable sur Marseille et le port, soirées DJ",
horaires: ["19:00-04:00"],
ambiances: ["fete", "romantique", "energique"],
icon: "🌃",
villes: ['Marseille'],
ageGroups: ["adultes"],
groupTypes: ["fete", "couple", "amis"]
},
{
id: 74,
nom: "Le Rowing Club",
type: "Bar / Restaurant Vue Mer",
prix: 18.00,
distance: "8 min",
rating: 4.3,
description: "Apéro au bord de l'eau, cocktails et tapas, vue sur le port",
horaires: ["18:00-01:00"],
ambiances: ["calme", "romantique", "amis"],
icon: "🍸",
villes: ['Marseille'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple", "amis"]
},
// MILIEU DE GAMME (25-50€)
{
id: 58,
nom: "Cocktail Bar Hemingway",
type: "Bar Cocktails",
prix: 32.00,
distance: "11 min",
rating: 4.6,
description: "Cocktails signature, ambiance feutrée",
horaires: ["19:00-02:00"],
ambiances: ["romantique", "fete"],
icon: "🍹",
villes: ['Paris', 'Londres'],
ageGroups: ["adultes"],
groupTypes: ["couple", "amis"]
},
{
id: 10,
nom: "Club Fabric",
type: "Boîte de Nuit",
prix: 35.00,
distance: "25 min",
rating: 4.1,
description: "DJ international, 3 dancefloors",
horaires: ["23:00-06:00"],
ambiances: ["fete", "danse", "energique"],
icon: "🪩",
villes: ['Paris', 'Berlin'],
ageGroups: ["adultes", "ados"],
groupTypes: ["amis"]
},
{
id: 59,
nom: "Le Lido",
type: "Cabaret Parisien",
prix: 45.00,
distance: "22 min",
rating: 4.7,
description: "Spectacle + dîner, Champs-Élysées",
horaires: ["21:00", "23:30"],
ambiances: ["romantique", "fete"],
icon: "✨",
villes: ['Paris'],
ageGroups: ["adultes", "seniors"],
groupTypes: ["couple", "amis"]
},
// HAUT DE GAMME (50€+)
{
id: 60,
nom: "VIP Club Exclusive",
type: "Club VIP",
prix: 65.00,
distance: "30 min",
rating: 4.3,
description: "Table VIP, bouteille premium incluse",
horaires: ["23:00-06:00"],
ambiances: ["fete", "luxe"],
icon: "💎",
villes: ['Paris'],
ageGroups: ["adultes"],
groupTypes: ["amis", "couple"]
},
{
id: 61,
nom: "Buddha Bar",
type: "Lounge Premium",
prix: 55.00,
distance: "20 min",
rating: 4.5,
description: "Ambiance zen, cocktails d'exception",
horaires: ["19:00-02:00"],
ambiances: ["romantique", "fete"],
icon: "🧘",
villes: ['Paris', 'Londres'],
ageGroups: ["adultes"],
groupTypes: ["couple", "amis"]
}
]
};
// Définition des ambiances avec leurs icônes et couleurs pour le style Tailwind CSS
const moods = [
{ id: 'calme', label: 'Soirée Calme', icon: Moon, color: 'blue', desc: 'Détente et tranquillité' },
{ id: 'romantique', label: 'Romantique', icon: Heart, color: 'pink', desc: 'Moment à deux' },
{ id: 'fete', label: 'Faire la Fête', icon: Sparkles, color: 'purple', desc: 'Ambiance festive' },
{ id: 'aventure', label: 'Aventure', icon: TreePine, color: 'green', desc: 'Nouvelles expériences' },
{ id: 'culture', label: 'Culturel', icon: Camera, color: 'orange', desc: 'Art et découverte' },
{ id: 'amis', label: 'Entre Amis', icon: Users, color: 'yellow', desc: 'Moments conviviaux' }
];
// Effet de bord pour mettre à jour l'heure actuelle chaque seconde
useEffect(() => {
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
return () => clearInterval(timer); // Nettoyage du timer à la désinitialisation du composant
}, []);
// Fonction pour déterminer la date et l'heure effective à utiliser (actuelle ou personnalisée)
const getEffectiveDateTime = () => {
if (isDateTimeCustom && selectedDate && selectedTime) {
// Si une date et une heure personnalisées sont sélectionnées, les utiliser
return new Date(`${selectedDate}T${selectedTime}`);
}
return currentTime; // Sinon, utiliser l'heure actuelle
};
// Fonction pour vérifier si une activité est disponible en fonction de l'heure effective
const isActivityAvailable = (activite) => {
const now = getEffectiveDateTime();
const currentHourMinute = now.getHours() * 60 + now.getMinutes(); // Convertir l'heure actuelle en minutes depuis minuit
if (!activite.horaires || activite.horaires.length === 0) {
return true; // Supposer que l'activité est disponible si aucun horaire n'est spécifié
}
for (const range of activite.horaires) {
if (range.includes('-')) {
// Gérer les plages horaires (ex: "08:00-02:00")
const [startStr, endStr] = range.split('-');
let [startHour, startMinute] = startStr.split(':').map(Number);
let [endHour, endMinute] = endStr.split(':').map(Number);
const startTotalMinutes = startHour * 60 + startMinute;
let endTotalMinutes = endHour * 60 + endMinute;
// Gérer les plages qui chevauchent minuit (ex: 22:00 - 02:00 signifie 22:00 un jour à 02:00 le lendemain)
if (endTotalMinutes < startTotalMinutes) {
endTotalMinutes += 24 * 60; // Ajouter 24 heures en minutes pour la comparaison
// Si l'heure actuelle est après minuit mais avant l'heure de début de la plage, ajuster l'heure actuelle
if (currentHourMinute < startTotalMinutes) {
const adjustedCurrentTime = currentHourMinute + 24 * 60;
if (adjustedCurrentTime >= startTotalMinutes && adjustedCurrentTime <= endTotalMinutes) {
return true;
}
}
}
if (currentHourMinute >= startTotalMinutes && currentHourMinute <= endTotalMinutes) {
return true;
}
} else {
// Gérer les horaires spécifiques (ex: "18:30")
const [activityHour, activityMinute] = range.split(':').map(Number);
const activityTotalMinutes = activityHour * 60 + activityMinute;
// Considérer l'activité disponible si l'heure actuelle est dans les 2 heures avant ou après l'horaire planifié
const twoHoursInMinutes = 120;
if (currentHourMinute >= activityTotalMinutes - twoHoursInMinutes && currentHourMinute <= activityTotalMinutes + twoHoursInMinutes) {
return true;
}
}
}
return false;
};
// Fonction pour suggérer un type de repas en fonction de l'heure effective
const getMealTypeFromTime = () => {
const time = getEffectiveDateTime();
const hour = time.getHours();
if (hour >= 6 && hour < 11) {
return "Idéal pour un petit-déjeuner ou brunch.";
} else if (hour >= 11 && hour < 14) {
return "Parfait pour un déjeuner ou un repas rapide.";
} else if (hour >= 14 && hour < 18) {
return "Moment pour un goûter, un café ou un verre.";
} else if (hour >= 18 && hour < 22) {
return "Excellente heure pour un dîner ou une sortie en soirée.";
} else {
return "Idéal pour une sortie nocturne ou un dernier verre.";
}
};
// Fonction principale pour planifier la sortie
const planifierSortie = async () => {
setIsLoading(true); // Activer l'état de chargement
setSuggestions([]); // Réinitialiser les suggestions précédentes
setBudgetError(''); // Réinitialiser le message d'erreur du budget
const budgetNum = parseFloat(budget);
if (isNaN(budgetNum) || budgetNum <= 0) {
setBudgetError('Veuillez entrer un budget valide (> 0).');
setShowResults(false);
setIsLoading(false);
return;
}
// Petite pause pour simuler un chargement réseau
await new Promise(resolve => setTimeout(resolve, 1000));
const budgetParPersonne = budgetNum / nbPersonnes;
// Extraire le nom de la ville de la chaîne "Marseille, France"
const currentLocationName = location.split(',')[0];
const toutesActivites = Object.values(activites).flat();
// Filtrer les activités par ville, groupe d'âge et type de groupe
const activitesParCritere = toutesActivites.filter(activite => {
const locationOk = activite.villes && activite.villes.includes(currentLocationName);
const ageOk = !selectedAgeGroup || (activite.ageGroups && activite.ageGroups.includes(selectedAgeGroup));
const groupOk = !selectedGroupType || (activite.groupTypes && activite.groupTypes.includes(selectedGroupType));
return locationOk && ageOk && groupOk;
});
// Filtrer ensuite par budget par personne, ambiance et disponibilité
const activitesFiltrees = activitesParCritere.filter(activite => {
const prixOk = activite.prix <= budgetParPersonne;
const ambianceOk = !ambiance || activite.ambiances.includes(ambiance);
const disponibleOk = isActivityAvailable(activite);
return prixOk && ambianceOk && disponibleOk;
});
// Générer les programmes basés sur les activités filtrées
const programmes = genererProgrammes(activitesFiltrees, budgetNum);
setSuggestions(programmes);
setShowResults(true);
setIsLoading(false); // Désactiver l'état de chargement
};
// Fonction utilitaire pour calculer le coût total et le planning d'un ensemble d'activités
const calculateProgramDetails = (activitiesForProgram, nbPeople, startingTime) => {
let currentActivityTime = new Date(startingTime.getTime());
let totalCalculatedCost = 0;
const programPlanning = [];
activitiesForProgram.forEach(activite => {
const activityCost = activite.prix * nbPeople;
totalCalculatedCost += activityCost;
// Déterminer la durée estimée pour le calcul du temps
let assumedDurationMinutes = 90; // Durée par défaut : 1.5 heures
if (activite.type.includes("Cinéma")) assumedDurationMinutes = 150; // 2.5 heures
else if (activite.type.includes("Restaurant")) assumedDurationMinutes = 90; // 1.5 heures
else if (activite.type.includes("Crêperie")) assumedDurationMinutes = 75; // 1h15
else if (activite.type.includes("Glacier") || activite.type.includes("Snack")) assumedDurationMinutes = 45; // 45min
else if (activite.type.includes("Jeu") || activite.type.includes("Bowling") || activite.type.includes("Laser Game") || activite.type.includes("Karaoké") || activite.type.includes("Spectacle") || activite.type.includes("Foire")) assumedDurationMinutes = 120; // 2 heures (pour foire aussi)
else if (activite.type.includes("Musée") || activite.type.includes("Visite Culturelle") || activite.type.includes("Opéra") || activite.type.includes("Cabaret") || activite.type.includes("Monument")) assumedDurationMinutes = 120; // 2 heures
else if (activite.type.includes("Bar") || activite.type.includes("Club") || activite.type.includes("Pub") || activite.type.includes("Lounge")) assumedDurationMinutes = 120; // 2 heures
else if (activite.type.includes("Spa")) assumedDurationMinutes = 120; // 2 heures
else if (activite.type.includes("Croisière")) assumedDurationMinutes = 150; // 2.5 heures
else if (activite.type.includes("Parc") || activite.type.includes("Plage") || activite.type.includes("Calanque") || activite.type.includes("Balade") || activite.type.includes("Quartier")) assumedDurationMinutes = 120; // 2 heures pour une balade/plage
programPlanning.push({
...activite,
suggestedStartTime: formatTime(currentActivityTime) // Ajoute l'heure de début suggérée
});
// Avancer le temps pour la prochaine activité, en ajoutant un temps de transition (30 minutes)
currentActivityTime = new Date(currentActivityTime.getTime() + (assumedDurationMinutes + 30) * 60 * 1000);
});
// Calcul de la durée totale du programme
const totalDurationMillis = currentActivityTime.getTime() - startingTime.getTime();
const totalHours = Math.floor(totalDurationMillis / (1000 * 60 * 60));
const totalMinutes = Math.round((totalDurationMillis % (1000 * 60 * 60)) / (1000 * 60));
let durationString = '';
if (totalHours > 0) {
durationString += `${totalHours}h`;
}
if (totalMinutes > 0) {
durationString += `${totalMinutes}min`;
}
if (totalHours === 0 && totalMinutes === 0) {
durationString = 'Moins d\'1h';
}
return { totalCalculatedCost, programPlanning, totalDurationString: durationString };
};
// Fonction pour générer des programmes de sortie basés sur le budget total et l'ambiance choisie
const genererProgrammes = (activitesFiltrees, budgetTotal) => {
const programmes = [];
const startingTime = getEffectiveDateTime(); // Heure de début de la planification
// Trier les activités par note (décroissant) puis par prix (croissant)
const sortedActivities = [...activitesFiltrees].sort((a, b) => b.rating - a.rating || a.prix - b.prix);
// Fonction utilitaire pour trouver une activité d'un certain type ou ambiance
const findActivity = (availableActivities, typeKeywords = [], ambianceKeywords = [], excludeIds = new Set(), ageGroup, groupType) => {
// Enhanced filtering based on ageGroup and groupType before considering keywords
let filteredByPersona = availableActivities.filter(act => {
const ageMatch = !ageGroup || (act.ageGroups && (act.ageGroups.includes(ageGroup) || act.ageGroups.includes("tous")));
const groupMatch = !groupType || (act.groupTypes && act.groupTypes.includes(groupType));
return ageMatch && groupMatch && isActivityAvailable(act);
});
// Apply persona-specific preferences (prioritization/deprioritization)
// Note: This logic will prioritize certain types if typeKeywords is empty based on age/group
// If typeKeywords are provided, they will still be respected primarily.
if (ageGroup === "ados" || ageGroup === "enfants") {
filteredByPersona = filteredByPersona.filter(act =>
!["Restaurant Gastronomique", "Opéra", "Spa Duo", "Bar à Vin", "Cabaret"].some(type => act.type.includes(type))
);
if (!typeKeywords.length) typeKeywords.push("Fast Food", "Snack", "Glacier", "Jeu", "Bowling", "Laser Game", "Foire", "Parc", "Cinéma", "Karaoké");
} else if (ageGroup === "adultes" || ageGroup === "seniors") {
filteredByPersona = filteredByPersona.filter(act =>
!["Fast Food", "Laser Game", "Karaoké", "Foire", "Snack"].some(type => act.type.includes(type))
);
if (!typeKeywords.length) typeKeywords.push("Restaurant", "Bistrot", "Gastronomique", "Bar à Vin", "Musée", "Opéra", "Théâtre", "Spa", "Cabaret", "Croisière");
}
if (groupType === "seul") {
filteredByPersona = filteredByPersona.filter(act =>
!["Escape Game", "Bowling", "Laser Game", "Karaoké", "Foire", "Boîte de Nuit", "Cabaret"].some(type => act.type.includes(type)) // Exclude group activities
);
if (!typeKeywords.length) typeKeywords.push("Musée", "Cinéma", "Parc", "Balade", "Café", "Bar", "Restaurant", "Bibliothèque");
} else if (groupType === "couple") {
if (!ambianceKeywords.length) ambianceKeywords.push("romantique", "calme");
if (!typeKeywords.length) typeKeywords.push("Restaurant Romantique", "Spa Duo", "Dîner-Croisière", "Bar à Vin", "Restaurant", "Cinéma");
} else if (groupType === "amis") {
if (!ambianceKeywords.length) ambianceKeywords.push("fete", "amis", "energique");
if (!typeKeywords.length) typeKeywords.push("Bar", "Pub", "Boîte de Nuit", "Jeu", "Bowling", "Laser Game", "Karaoké", "Comedy Club", "Restaurant", "Brasserie");
} else if (groupType === "famille") {
if (!ambianceKeywords.length) ambianceKeywords.push("famille", "detente");
if (!typeKeywords.length) typeKeywords.push("Parc", "Plage", "Cinéma", "Fast Food", "Glacier", "Foire", "Musée", "Restaurant", "Crêperie", "Bowling");
}
// Now apply the original keyword and ambiance filtering on the persona-filtered list
// Prioritize activities that match type keywords first
for (const keyword of typeKeywords) {
const found = filteredByPersona.find(act =>
!excludeIds.has(act.id) &&
act.type.toLowerCase().includes(keyword.toLowerCase()) &&
(!ambiance || act.ambiances.includes(ambiance))
);
if (found) return found;
}
// Then, prioritize activities that match ambiance keywords
for (const keyword of ambianceKeywords) {
const found = filteredByPersona.find(act =>
!excludeIds.has(act.id) &&
act.ambiances.includes(keyword) &&
(!ambiance || act.ambiances.includes(ambiance))
);
if (found) return found;
}
// Fallback: find any suitable activity
return filteredByPersona.find(act =>
!excludeIds.has(act.id) &&
(!ambiance || act.ambiances.includes(ambiance))
);
};
// --- Stratégie 1: Soirée Complète et Diversifiée ---
// Objectif: Proposer un restaurant, une activité de divertissement/culture, et un lieu pour un verre.
let currentBudgetFullEvening = budgetTotal;
const fullEveningActivities = [];
const fullEveningUsedIds = new Set();
// Tenter d'ajouter un restaurant
const restaurant = findActivity(sortedActivities, ["Restaurant", "Crêperie", "Brasserie", "Fast Food", "Snack", "Glacier"], [], fullEveningUsedIds, selectedAgeGroup, selectedGroupType);
if (restaurant && restaurant.prix * nbPersonnes <= currentBudgetFullEvening) {
fullEveningActivities.push(restaurant);
currentBudgetFullEvening -= restaurant.prix * nbPersonnes;
fullEveningUsedIds.add(restaurant.id);
}
// Tenter d'ajouter une activité de divertissement ou culturelle
const entertainmentCulture = findActivity(sortedActivities, ["Cinéma", "Jeu", "Spectacle", "Musée", "Opéra", "Cabaret", "Bowling", "Laser Game", "Karaoké", "Parc", "Plage", "Calanque", "Balade", "Quartier", "Foire"], [], fullEveningUsedIds, selectedAgeGroup, selectedGroupType);
if (entertainmentCulture && entertainmentCulture.prix * nbPersonnes <= currentBudgetFullEvening) {
fullEveningActivities.push(entertainmentCulture);
currentBudgetFullEvening -= entertainmentCulture.prix * nbPersonnes;
fullEveningUsedIds.add(entertainmentCulture.id);
}
// Tenter d'ajouter un lieu pour un verre ou une activité de fin de soirée
const drinksPlace = findActivity(sortedActivities, ["Bar", "Pub", "Lounge", "Boîte de Nuit"], [], fullEveningUsedIds, selectedAgeGroup, selectedGroupType);
if (drinksPlace && drinksPlace.prix * nbPersonnes <= currentBudgetFullEvening) {
fullEveningActivities.push(drinksPlace);
currentBudgetFullEvening -= drinksPlace.prix * nbPersonnes;
fullEveningUsedIds.add(drinksPlace.id);
}
// S'assurer qu'il y a au moins 2 activités significatives pour ce programme
if (fullEveningActivities.length >= 2) {
const { totalCalculatedCost, programPlanning, totalDurationString } = calculateProgramDetails(fullEveningActivities, nbPersonnes, startingTime);
if (totalCalculatedCost <= budgetTotal) {
programmes.push({
id: 'complete-diverse',
titre: "Soirée Complète et Diversifiée",
budget: totalCalculatedCost,
description: "Un programme varié avec dîner, activité et boisson.",
activites: fullEveningActivities,
type: "Diversifiée",
duree: totalDurationString,
recommandation: "Profitez de plusieurs expériences en une soirée.",
planning: programPlanning
});
}
}
// --- Stratégie 2: Soirée Thématique (selon l'ambiance choisie) ---
// Si une ambiance spécifique est choisie, proposer un programme qui la maximise.
if (ambiance) {
let thematicActivities = [];
const thematicUsedIds = new Set();
let currentBudgetThematic = budgetTotal;
// Prioriser les activités qui correspondent le mieux à l'ambiance et aux filtres de persona
const prioritizedThematicActivities = sortedActivities.filter(a =>
a.ambiances.includes(ambiance) &&
(!selectedAgeGroup || (a.ageGroups && (a.ageGroups.includes(selectedAgeGroup) || a.ageGroups.includes("tous")))) &&
(!selectedGroupType || (a.groupTypes && a.groupTypes.includes(selectedGroupType)))
);
for (const activite of prioritizedThematicActivities) {
// Limite le nombre d'activités pour éviter les programmes trop longs, et assure la diversité des types si possible
if (thematicActivities.length >= 3) break;
if (activite.prix * nbPersonnes <= currentBudgetThematic && !thematicUsedIds.has(activite.id)) {
thematicActivities.push(activite);
currentBudgetThematic -= activite.prix * nbPersonnes;
thematicUsedIds.add(activite.id);
}
}
// Ajouter ce programme seulement s'il est unique par rapport aux autres déjà générés
const isThematicProgramDistinct = thematicActivities.length > 0 &&
!programmes.some(p => thematicActivities.every(tA => p.activites.some(pA => pA.id === tA.id))); // Check if all activities are already in another program
if (isThematicProgramDistinct) {
const { totalCalculatedCost, programPlanning, totalDurationString } = calculateProgramDetails(thematicActivities, nbPersonnes, startingTime);
if (totalCalculatedCost <= budgetTotal) {
programmes.push({
id: `thematic-${ambiance}`,
titre: `Soirée ${moods.find(m => m.id === ambiance)?.label || ambiance}`,
budget: totalCalculatedCost,
description: `Un programme spécialement conçu pour une ambiance ${moods.find(m => m.id === ambiance)?.label.toLowerCase() || ambiance}.`,
activites: thematicActivities,
type: "Thématique",
duree: totalDurationString,
recommandation: "Immergez-vous pleinement dans l'ambiance de votre choix.",
planning: programPlanning
});
}
}
}
// --- Stratégie 3: Expérience Unique (Haut de Gamme ou Focus) ---
// Si le budget est suffisant pour une activité coûteuse et très bien notée
const premiumSingleActivity = sortedActivities.find(a =>
a.prix * nbPersonnes <= budgetTotal &&
a.rating >= 4.5 && // Activité bien notée
(a.prix * nbPersonnes >= budgetTotal * 0.5 || sortedActivities.length === 1) && // Consomme une partie significative du budget ou est la seule option
!programmes.some(p => p.activites.some(act => act.id === a.id)) && // Éviter les doublons avec les programmes déjà ajoutés
(!selectedAgeGroup || (a.ageGroups && (a.ageGroups.includes(selectedAgeGroup) || a.ageGroups.includes("tous")))) && // Apply persona filters here too
(!selectedGroupType || (a.groupTypes && a.groupTypes.includes(selectedGroupType)))
);
if (premiumSingleActivity) {
const { totalCalculatedCost, programPlanning, totalDurationString } = calculateProgramDetails([premiumSingleActivity], nbPersonnes, startingTime);
if (totalCalculatedCost <= budgetTotal) {
programmes.push({
id: 'premium-single',
titre: "Expérience Unique (Haut de Gamme)",
budget: totalCalculatedCost,
description: "Une activité phare pour une soirée spéciale.",
activites: [premiumSingleActivity],
type: "Haut de Gamme",
duree: totalDurationString,
recommandation: "Parfait pour une immersion totale dans une seule expérience de qualité.",
planning: programPlanning
});
}
}
// Fallback: Si aucun programme n'a été généré par les stratégies ci-dessus, proposer la meilleure activité unique dans le budget.
if (programmes.length === 0 && activitesFiltrees.length > 0) {
// Find the best activity that also matches persona filters for the fallback
const bestActivity = sortedActivities.find(a =>
(!selectedAgeGroup || (a.ageGroups && (a.ageGroups.includes(selectedAgeGroup) || a.ageGroups.includes("tous")))) &&
(!selectedGroupType || (a.groupTypes && a.groupTypes.includes(selectedGroupType)))
);
if (bestActivity) {
const { totalCalculatedCost, programPlanning, totalDurationString } = calculateProgramDetails([bestActivity], nbPersonnes, startingTime);
programmes.push({
id: 'fallback',
titre: "Suggestion Simple",
budget: totalCalculatedCost,
description: "Voici une activité qui pourrait vous plaire.",
activites: [bestActivity],
type: "Simple",
duree: totalDurationString,
recommandation: "Idéal pour une sortie spontanée.",
planning: programPlanning
});
}
}
// Randomiser l'ordre des programmes générés pour une sensation de fraîcheur à chaque fois
return programmes.sort(() => Math.random() - 0.5).filter(p => p.activites.length > 0);
};
// Fonction pour définir rapidement le budget, le nombre de personnes et l'ambiance
const quickBudgetSet = (amount, personnes, mood) => {
setBudget(amount.toString());
setNbPersonnes(personnes);
setAmbiance(mood);
setSelectedMood(mood);
setBudgetError(''); // Effacer l'erreur de budget si elle existe
};
// Retourne la date actuelle au formatgetFullYear-MM-DD pour l'input date
const getCurrentDateForInput = () => {
const today = new Date();
return today.toISOString().split('T')[0];
};
// Retourne l'heure actuelle au format HH:MM pour l'input time
const getCurrentTimeForInput = () => {
const now = new Date();
return now.toTimeString().slice(0, 5);
};
// Formate une date en chaîne de caractères lisible en français
const formatDateTime = (date) => {
const options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
return date.toLocaleDateString('fr-FR', options);
};
// Récupère la couleur Tailwind associée à une ambiance
const getMoodColor = (moodId) => {
const mood = moods.find(m => m.id === moodId);
return mood ? mood.color : 'gray';
};
// Formate une date pour afficher uniquement l'heure (HH:MM)
const formatTime = (date) => {
return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
};
// Fonction pour réinitialiser tous les champs du formulaire
const resetForm = () => {
setBudget('');
setNbPersonnes(1);
setAmbiance('');
setLocation('Paris, France');
setSelectedDate('');
setSelectedTime('');
setIsDateTimeCustom(false);
setSuggestions([]);
setShowResults(false);
setSelectedMood('');
setIsLoading(false);
setBudgetError('');
setSelectedAgeGroup('');
setSelectedGroupType('');
};
return (
{/* Header de l'application */}
SortiePlanner
{formatTime(currentTime)}
setLocation(e.target.value)}
className="bg-transparent border-none focus:outline-none cursor-pointer"
>
{villes.map(ville => (
{ville.nom}
))}
{/* Section "Quand sortir ?" */}
Quand sortir ?
setIsDateTimeCustom(false)}
className={`flex-1 py-3 px-4 rounded-xl transition-all ${
!isDateTimeCustom
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Maintenant
{formatTime(currentTime)}
setIsDateTimeCustom(true)}
className={`flex-1 py-3 px-4 rounded-xl transition-all ${
isDateTimeCustom
? 'bg-gradient-to-r from-green-500 to-blue-500 text-white shadow-lg'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Programmer
Choisir date/heure
{isDateTimeCustom && (
Date de sortie
setSelectedDate(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
/>
Heure de début
setSelectedTime(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
/>
{selectedDate && selectedTime && (
📅 Sortie programmée pour :
{formatDateTime(new Date(`${selectedDate}T${selectedTime}`))}
)}
)}
Suggestion: {getMealTypeFromTime()}
{/* Section "Budget & Participants" */}
Budget & Participants
Budget total
{
setBudget(e.target.value);
setBudgetError(''); // Effacer l'erreur lors de la saisie
}}
className={`w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white/80 ${
budgetError ? 'border-red-500' : 'border-gray-300'
}`}
/>
{budgetError &&
{budgetError}
}
Personnes
setNbPersonnes(parseInt(e.target.value))}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white/80"
>
1 personne
2 personnes
3 personnes
4+ personnes
quickBudgetSet(50, 2, 'romantique')}
className="px-3 py-3 bg-gradient-to-r from-pink-100 to-red-100 text-pink-700 rounded-xl hover:from-pink-200 hover:to-red-200 transition-all text-sm"
>
50€ • 2p • Romantique
quickBudgetSet(150, 2, 'fete')}
className="px-3 py-3 bg-gradient-to-r from-purple-100 to-blue-100 text-purple-700 rounded-xl hover:from-purple-200 hover:to-blue-200 transition-all text-sm"
>
150€ • 2p • Fête
{/* Section "Quelle ambiance ?" */}
Quelle ambiance ?
{moods.map((mood) => {
const IconComponent = mood.icon; // Composant Lucide React pour l'icône
const isSelected = selectedMood === mood.id; // Vérifier si cette ambiance est sélectionnée
return (
{
setAmbiance(mood.id);
setSelectedMood(mood.id);
}}
className={`group p-4 rounded-2xl transition-all duration-200 ease-in-out transform ${
isSelected
? `bg-gradient-to-r from-${mood.color}-500 to-${mood.color}-600 text-white shadow-lg scale-105`
: `bg-${mood.color}-50 text-${mood.color}-700 hover:bg-${mood.color}-100 hover:scale-105`
}`}
>
{mood.label}
{mood.desc}
);
})}
{/* Nouveaux filtres : Âge et Type de Groupe */}
Qui participe ?
Tranche d'âge
setSelectedAgeGroup(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white/80"
>
Tous les âges
Enfants
Adolescents
Adultes
Seniors
Famille
Type de groupe
setSelectedGroupType(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white/80"
>
Indifférent
Seul(e)
En Couple
Entre Amis
En Famille
{/* Bouton d'action pour planifier la sortie */}
{isLoading ? (
// Spinner de chargement
) : (
)}
Planifier ma sortie {isDateTimeCustom && selectedDate ?
`le ${new Date(selectedDate).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}` :
'maintenant'
} !
{/* Bouton de réinitialisation */}
Réinitialiser
{/* Section des résultats de la planification */}
{showResults && suggestions.length > 0 && (
🎉 Vos programmes personnalisés
📍 {location} • {isDateTimeCustom && selectedDate ?
formatDateTime(getEffectiveDateTime()).split(' à ')[0] :
'Aujourd\'hui'
} {isDateTimeCustom && selectedTime ?
`à ${selectedTime}` :
`à ${formatTime(currentTime)}`
}
{/* Revert to single column display for suggestions */}
{suggestions.map((programme) => (
{programme.titre}
{programme.description}
{/* Affichage du prix total calculé pour le programme */}
{programme.budget.toFixed(2)}€
{programme.type}
{/* Affichage de la durée totale calculée pour le programme */}
{programme.duree}
{nbPersonnes} personne{nbPersonnes > 1 ? 's' : ''}
{/* Section du planning détaillé */}
{programme.planning && programme.planning.length > 0 && (
Planning détaillé
{programme.planning.map((activityInPlan, idx) => (
{activityInPlan.suggestedStartTime}
•
{activityInPlan.icon}
{activityInPlan.nom}
({activityInPlan.description.split(' - ')[0]})
))}
)}
{/* Espacement ajouté pour la section activités */}
{programme.activites.map((activite) => (
{activite.icon}
{activite.nom} ({activite.type})
{activite.description}
{activite.prix.toFixed(2)}€ / pers.
{activite.distance}
{activite.rating}
{activite.horaires && activite.horaires.length > 0 && (
Horaires: {activite.horaires.join(', ')}
)}
))}
💡 Conseil: {programme.recommandation}
Voir l'itinéraire
))}
{/* Fin de la modification: fermeture de la div flex */}
)}
{/* Message si aucune suggestion n'est trouvée */}
{showResults && suggestions.length === 0 && (
😔 Aucune suggestion trouvée !
Essayez d'ajuster votre budget, le nombre de personnes ou l'ambiance recherchée.
Peut-être que l'heure sélectionnée ne correspond pas aux horaires des activités.
)}
);
};
export default SortiePlannerApp;