const OptionsConfig = ({ title, icon, configKey, addPrompts, color }) => (
setAdminSection('menu')} />

Gestion des options

Ajoutez, modifiez ou supprimez les {title.toLowerCase()}

{adminConfig[configKey].map((option, index) => (
{ if (e.target.value && e.target.value.trim()) { setAdminConfig(prev => ({ ...prev, [configKey]: prev[configKey].map((opt, i) => i === index ? { ...opt, label: e.target.value.trim() } : opt ) })); } }} className={`w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-${color}-500 text-lg`} />
{option.value}
))}
{adminConfig[configKey].length === 0 && (
{icon}

Aucune option configurée

import React, { useState } from 'react'; import { Calendar, Clock, User, Mail, Phone, CheckCircle, ChevronLeft, ChevronRight } from 'lucide-react'; const CalendlyClone = () => { const [currentStep, setCurrentStep] = useState(1); const [selectedDate, setSelectedDate] = useState(null); const [selectedTime, setSelectedTime] = useState(null); const [showAdmin, setShowAdmin] = useState(false); const [adminConfig, setAdminConfig] = useState({ // Configuration générale companyName: 'Consultation Immobilière', description: 'Réservez votre rendez-vous avec notre courtier immobilier', primaryColor: '#2563eb', secondaryColor: '#16a34a', // Configuration des horaires timeSlots: [ '08:00', '08:30', '09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30', '17:00', '17:30', '18:00', '18:30', '19:00' ], availabilityDays: 60, includeWeekends: true, // Types de rendez-vous personnalisés appointmentTypes: [ { value: 'achat', label: 'Achat d\'une propriété' }, { value: 'vente', label: 'Vente d\'une propriété' }, { value: 'evaluation', label: 'Évaluation gratuite' }, { value: 'investissement', label: 'Investissement immobilier' }, { value: 'consultation', label: 'Consultation générale' } ], // Types de propriétés propertyTypes: [ { value: 'maison', label: 'Maison' }, { value: 'condo', label: 'Condominium' }, { value: 'duplex', label: 'Duplex/Triplex' }, { value: 'commercial', label: 'Commercial' }, { value: 'terrain', label: 'Terrain' } ], // Budgets budgetRanges: [ { value: '0-200000', label: 'Moins de 200 000 $' }, { value: '200000-400000', label: '200 000 $ - 400 000 $' }, { value: '400000-600000', label: '400 000 $ - 600 000 $' }, { value: '600000-800000', label: '600 000 $ - 800 000 $' }, { value: '800000+', label: 'Plus de 800 000 $' } ], // Échéanciers timeframes: [ { value: 'immediatement', label: 'Immédiatement' }, { value: '1-3mois', label: '1-3 mois' }, { value: '3-6mois', label: '3-6 mois' }, { value: '6-12mois', label: '6-12 mois' }, { value: 'plus12mois', label: 'Plus de 12 mois' } ], // Messages personnalisés confirmationMessage: 'Votre consultation est confirmée. Vous recevrez un email de confirmation avec tous les détails.', followUpMessage: 'Notre courtier vous contactera 24h avant le rendez-vous pour confirmer et vous donner les dernières informations.', // Champs requis requiredFields: { name: true, email: true, phone: true, appointmentType: true, propertyType: false, budget: false, timeframe: false, message: false } }); const [bookingForm, setBookingForm] = useState({ name: '', email: '', phone: '', appointmentType: '', propertyType: '', budget: '', timeframe: '', message: '' }); const [bookedSlots, setBookedSlots] = useState([]); const [currentMonth, setCurrentMonth] = useState(new Date()); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); // Créneaux horaires depuis la configuration admin const timeSlots = adminConfig.timeSlots; const formatDate = (date) => { return date.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); }; const isSlotBooked = (date, time) => { const dateStr = date.toDateString(); return bookedSlots.some(slot => slot.date === dateStr && slot.time === time); }; const handleDateSelect = (date) => { setSelectedDate(date); setCurrentStep(2); }; const handleTimeSelect = (time) => { setSelectedTime(time); setCurrentStep(3); }; const handleFormChange = (e) => { const { name, value } = e.target; let formattedValue = value; // Formatage automatique du téléphone if (name === 'phone') { // Supprimer tous les caractères non numériques const phoneNumber = value.replace(/\D/g, ''); // Limiter à 10 chiffres if (phoneNumber.length <= 10) { // Formater: 123-123-1234 if (phoneNumber.length >= 6) { formattedValue = `${phoneNumber.slice(0, 3)}-${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6)}`; } else if (phoneNumber.length >= 3) { formattedValue = `${phoneNumber.slice(0, 3)}-${phoneNumber.slice(3)}`; } else { formattedValue = phoneNumber; } } else { return; // Ne pas permettre plus de 10 chiffres } } setBookingForm({ ...bookingForm, [name]: formattedValue }); // Marquer le champ comme touché setTouched({ ...touched, [name]: true }); // Validation en temps réel validateField(name, formattedValue); }; const validateField = (name, value) => { const newErrors = { ...errors }; switch (name) { case 'name': if (!value.trim()) { newErrors.name = 'Le nom est obligatoire'; } else if (value.trim().length < 2) { newErrors.name = 'Le nom doit contenir au moins 2 caractères'; } else { delete newErrors.name; } break; case 'email': const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!value.trim()) { newErrors.email = 'L\'email est obligatoire'; } else if (!emailRegex.test(value)) { newErrors.email = 'Format d\'email invalide'; } else { delete newErrors.email; } break; case 'phone': const phoneRegex = /^\d{3}-\d{3}-\d{4}$/; if (!value.trim()) { newErrors.phone = 'Le téléphone est obligatoire'; } else if (!phoneRegex.test(value)) { newErrors.phone = 'Format: 123-123-1234'; } else { delete newErrors.phone; } break; case 'appointmentType': if (!value) { newErrors.appointmentType = 'Veuillez sélectionner un type de rendez-vous'; } else { delete newErrors.appointmentType; } break; default: break; } setErrors(newErrors); }; const validateAllFields = () => { const requiredFields = Object.keys(adminConfig.requiredFields).filter( field => adminConfig.requiredFields[field] ); let isValid = true; const newErrors = {}; requiredFields.forEach(field => { validateField(field, bookingForm[field]); if (errors[field] || !bookingForm[field]) { isValid = false; if (!bookingForm[field]) { newErrors[field] = 'Ce champ est obligatoire'; } } }); setErrors({ ...errors, ...newErrors }); setTouched(requiredFields.reduce((acc, field) => ({ ...acc, [field]: true }), {})); return isValid && Object.keys(errors).length === 0; }; const handleBooking = () => { if (!validateAllFields()) { alert('Veuillez corriger les erreurs dans le formulaire'); return; } const newBooking = { date: selectedDate.toDateString(), time: selectedTime, ...bookingForm }; setBookedSlots([...bookedSlots, newBooking]); setCurrentStep(4); }; const resetBooking = () => { setCurrentStep(1); setSelectedDate(null); setSelectedTime(null); setBookingForm({ name: '', email: '', phone: '', appointmentType: '', propertyType: '', budget: '', timeframe: '', message: '' }); setErrors({}); setTouched({}); setAdminSection('menu'); // Reset admin to menu }; const goBack = () => { if (currentStep > 1) { setCurrentStep(currentStep - 1); } }; // Fonctions pour le calendrier const getDaysInMonth = (date) => { return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); }; const getFirstDayOfMonth = (date) => { const firstDay = new Date(date.getFullYear(), date.getMonth(), 1).getDay(); return firstDay === 0 ? 7 : firstDay; // Lundi = 1, Dimanche = 7 }; const isDateAvailable = (date) => { const today = new Date(); today.setHours(0, 0, 0, 0); const checkDate = new Date(date); checkDate.setHours(0, 0, 0, 0); // Vérifier si c'est un weekend et si les weekends sont autorisés if (!adminConfig.includeWeekends) { const dayOfWeek = checkDate.getDay(); if (dayOfWeek === 0 || dayOfWeek === 6) { // Dimanche = 0, Samedi = 6 return false; } } // Disponible si c'est après aujourd'hui et dans la période configurée const maxDate = new Date(today); maxDate.setDate(today.getDate() + adminConfig.availabilityDays); return checkDate > today && checkDate <= maxDate; }; const previousMonth = () => { setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)); }; const nextMonth = () => { setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1)); }; const renderCalendar = () => { const daysInMonth = getDaysInMonth(currentMonth); const firstDay = getFirstDayOfMonth(currentMonth); const days = []; // Jours vides au début for (let i = 1; i < firstDay; i++) { days.push(

); } // Jours du mois for (let day = 1; day <= daysInMonth; day++) { const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day); const isAvailable = isDateAvailable(date); const isSelected = selectedDate && selectedDate.toDateString() === date.toDateString(); days.push( ); } return days; }; // Fonctions d'administration const handleAdminConfigChange = (section, field, value) => { setAdminConfig(prev => ({ ...prev, [section]: { ...prev[section], [field]: value } })); }; const handleSimpleConfigChange = (field, value) => { setAdminConfig(prev => ({ ...prev, [field]: value })); }; const addTimeSlot = () => { const newTime = prompt('Ajouter un créneau (format HH:MM):'); if (newTime && /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/.test(newTime)) { setAdminConfig(prev => ({ ...prev, timeSlots: [...prev.timeSlots, newTime].sort() })); } }; const removeTimeSlot = (timeToRemove) => { setAdminConfig(prev => ({ ...prev, timeSlots: prev.timeSlots.filter(time => time !== timeToRemove) })); }; const addOption = (configKey, newOption) => { if (newOption.value && newOption.label) { setAdminConfig(prev => ({ ...prev, [configKey]: [...prev[configKey], newOption] })); } }; const removeOption = (configKey, valueToRemove) => { setAdminConfig(prev => ({ ...prev, [configKey]: prev[configKey].filter(option => option.value !== valueToRemove) })); }; const StepIndicator = () => (
{[1, 2, 3, 4].map((step) => (
{step < currentStep ? '✓' : step}
{step < 4 && (
)}
))}
); const AdminPanel = () => (
{/* Header admin avec les mêmes couleurs */}

🔧 Panneau d'Administration

Configuration complète de votre système de réservation

{/* Configuration générale */}

🏢 Configuration Générale

handleSimpleConfigChange('companyName', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" placeholder="Nom affiché en en-tête" />
handleSimpleConfigChange('description', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" placeholder="Sous-titre de l'application" />
handleSimpleConfigChange('primaryColor', e.target.value)} className="w-full h-10 border border-gray-300 rounded-lg cursor-pointer" />
handleSimpleConfigChange('secondaryColor', e.target.value)} className="w-full h-10 border border-gray-300 rounded-lg cursor-pointer" />
{/* Configuration des disponibilités */}

📅 Disponibilités et Horaires

handleSimpleConfigChange('availabilityDays', parseInt(e.target.value))} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" />

Combien de jours à l'avance les clients peuvent réserver

{adminConfig.timeSlots.map(time => (
{time}
))}

Format: HH:MM (ex: 14:30)

{/* Types de rendez-vous */}

📋 Types de Rendez-vous

{adminConfig.appointmentTypes.map((type, index) => (
editOption('appointmentTypes', index, e.target.value)} className="flex-1 px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-green-500" /> {type.value}
))}
{/* Types de propriétés */}

🏠 Types de Propriétés

{adminConfig.propertyTypes.map((type, index) => (
editOption('propertyTypes', index, e.target.value)} className="flex-1 px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-purple-500" /> {type.value}
))}
{/* Tranches de budget */}

💰 Tranches de Budget

{adminConfig.budgetRanges.map((budget, index) => (
editOption('budgetRanges', index, e.target.value)} className="flex-1 px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-yellow-500" /> {budget.value}
))}
{/* Échéanciers */}

⏰ Échéanciers

{adminConfig.timeframes.map((timeframe, index) => (
editOption('timeframes', index, e.target.value)} className="flex-1 px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-indigo-500" /> {timeframe.value}
))}
{/* Champs requis */}

✅ Champs Obligatoires

{Object.keys(adminConfig.requiredFields).map(field => ( ))}
{/* Messages personnalisés */}

💬 Messages Personnalisés