Prestations de service

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)}
{/* Section "Quand sortir ?" */}

Quand sortir ?

{isDateTimeCustom && (
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" />
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

{ 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}

}
{/* 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 ( ); })}
{/* Nouveaux filtres : Âge et Type de Groupe */}

Qui participe ?

{/* Bouton d'action pour planifier la sortie */} {/* Bouton de réinitialisation */} {/* 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}

))}
{/* 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;

Prestations de service

Basique

Nous proposons une gamme de services spécialisés adaptés à vos besoins spécifiques.

Professionnel

Nous proposons une gamme de services spécialisés adaptés à vos besoins spécifiques.

Business

Nous proposons une gamme de services spécialisés adaptés à vos besoins spécifiques.

Entreprise

Nous proposons une gamme de services spécialisés adaptés à vos besoins spécifiques.