Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Appointment Scheduler</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<script src="https://unpkg.com/react@18/umd/react.development.js"></script> | |
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> | |
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
<style type="text/css"> | |
.fade-in { | |
animation: fadeIn 0.3s ease-in-out; | |
} | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
.time-slot { | |
transition: all 0.2s ease; | |
} | |
.time-slot:hover { | |
transform: scale(1.05); | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50"> | |
<div id="root"></div> | |
<script type="text/babel"> | |
const { useState, useEffect } = React; | |
const services = [ | |
{ id: 1, name: "Haircut", duration: 30, price: 30 }, | |
{ id: 2, name: "Beard Trim", duration: 15, price: 15 }, | |
{ id: 3, name: "Haircut + Beard", duration: 45, price: 40 }, | |
{ id: 4, name: "Hot Towel Shave", duration: 30, price: 35 }, | |
{ id: 5, name: "Hair Coloring", duration: 60, price: 60 } | |
]; | |
const timeSlots = [ | |
"09:00 AM", "09:30 AM", "10:00 AM", "10:30 AM", "11:00 AM", "11:30 AM", | |
"12:00 PM", "12:30 PM", "01:00 PM", "01:30 PM", "02:00 PM", "02:30 PM", | |
"03:00 PM", "03:30 PM", "04:00 PM", "04:30 PM", "05:00 PM", "05:30 PM" | |
]; | |
const professionals = [ | |
{ id: 1, name: "John Doe", specialty: "Haircuts & Styling" }, | |
{ id: 2, name: "Jane Smith", specialty: "Coloring & Treatments" }, | |
{ id: 3, name: "Mike Johnson", specialty: "Beard Grooming" } | |
]; | |
function App() { | |
const [step, setStep] = useState(1); | |
const [selectedService, setSelectedService] = useState(null); | |
const [selectedDate, setSelectedDate] = useState(null); | |
const [selectedTime, setSelectedTime] = useState(null); | |
const [selectedProfessional, setSelectedProfessional] = useState(null); | |
const [name, setName] = useState(""); | |
const [email, setEmail] = useState(""); | |
const [phone, setPhone] = useState(""); | |
const [notes, setNotes] = useState(""); | |
const [bookedSlots, setBookedSlots] = useState([]); | |
const [appointments, setAppointments] = useState([]); | |
// Generate dates for the next 7 days | |
const dates = Array.from({ length: 7 }, (_, i) => { | |
const date = new Date(); | |
date.setDate(date.getDate() + i); | |
return date; | |
}); | |
// Simulate fetching booked slots from an API | |
useEffect(() => { | |
const dummyBookedSlots = [ | |
{ date: dates[0].toDateString(), time: "09:00 AM", professional: 1 }, | |
{ date: dates[0].toDateString(), time: "10:00 AM", professional: 2 }, | |
{ date: dates[1].toDateString(), time: "02:30 PM", professional: 1 }, | |
]; | |
setBookedSlots(dummyBookedSlots); | |
}, []); | |
const isSlotAvailable = (time, professionalId) => { | |
if (!selectedDate) return true; | |
const isBooked = bookedSlots.some( | |
slot => | |
slot.date === selectedDate.toDateString() && | |
slot.time === time && | |
slot.professional === professionalId | |
); | |
return !isBooked; | |
}; | |
const handleSubmit = (e) => { | |
e.preventDefault(); | |
const newAppointment = { | |
id: Date.now(), | |
service: selectedService.name, | |
date: selectedDate.toDateString(), | |
time: selectedTime, | |
professional: professionals.find(p => p.id === selectedProfessional).name, | |
customer: { name, email, phone }, | |
notes, | |
status: "Confirmed" | |
}; | |
setAppointments([...appointments, newAppointment]); | |
alert("Appointment booked successfully!"); | |
resetForm(); | |
}; | |
const resetForm = () => { | |
setStep(1); | |
setSelectedService(null); | |
setSelectedDate(null); | |
setSelectedTime(null); | |
setSelectedProfessional(null); | |
setName(""); | |
setEmail(""); | |
setPhone(""); | |
setNotes(""); | |
}; | |
const formatDate = (date) => { | |
return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); | |
}; | |
return ( | |
<div className="min-h-screen py-8 px-4"> | |
<div className="max-w-4xl mx-auto bg-white rounded-xl shadow-md overflow-hidden"> | |
<div className="p-8"> | |
<h1 className="text-3xl font-bold text-center text-gray-800 mb-2">Book Your Appointment</h1> | |
<p className="text-center text-gray-600 mb-8">Schedule your visit in just a few clicks</p> | |
{/* Progress Steps */} | |
<div className="flex items-center justify-between mb-8 relative"> | |
<div className="absolute w-full h-1 bg-gray-200 top-1/2 transform -translate-y-1/2 z-0"></div> | |
{[1, 2, 3, 4].map((s) => ( | |
<div key={s} className="flex flex-col items-center relative z-10"> | |
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${step >= s ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600'} font-semibold`}> | |
{s} | |
</div> | |
<div className={`text-xs mt-2 font-medium ${step >= s ? 'text-blue-600' : 'text-gray-500'}`}> | |
{s === 1 && 'Service'} | |
{s === 2 && 'Schedule'} | |
{s === 3 && 'Professional'} | |
{s === 4 && 'Details'} | |
</div> | |
</div> | |
))} | |
</div> | |
{/* Step 1: Select Service */} | |
{step === 1 && ( | |
<div className="fade-in"> | |
<h2 className="text-xl font-semibold text-gray-800 mb-4">Select a Service</h2> | |
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
{services.map((service) => ( | |
<div | |
key={service.id} | |
className={`p-4 border rounded-lg cursor-pointer transition-all ${selectedService?.id === service.id ? 'border-blue#500 bg-blue-50' : 'border-gray-200 hover:border-blue-300'}`} | |
onClick={() => setSelectedService(service)} | |
> | |
<div className="font-semibold text-gray-800">{service.name}</div> | |
<div className="flex justify-between items-center mt-2"> | |
<span className="text-sm text-gray-600">{service.duration} min</span> | |
<span className="text-sm font-medium text-blue-600">${service.price}</span> | |
</div> | |
</div> | |
))} | |
</div> | |
<div className="mt-6 flex justify-end"> | |
<button | |
onClick={() => setStep(2)} | |
disabled={!selectedService} | |
className={`px-6 py-2 rounded-md ${selectedService ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-gray-300 text-gray-500 cursor-not-allowed'} transition-colors`} | |
> | |
Next | |
</button> | |
</div> | |
</div> | |
)} | |
{/* Step 2: Select Date & Time */} | |
{step === 2 && ( | |
<div className="fade-in"> | |
<h2 className="text-xl font-semibold text-gray-800 mb-4">Choose Date and Time</h2> | |
<div className="mb-6"> | |
<h3 className="text-lg font-medium text-gray-700 mb-3">Available Dates</h3> | |
<div className="flex overflow-x-auto pb-2 gap-3"> | |
{dates.map((date, index) => ( | |
<div | |
key={index} | |
className={`flex-shrink-0 w-24 py-3 px-1 rounded-lg text-center cursor-pointer ${selectedDate?.toDateString() === date.toDateString() ? 'bg-blue-100 border border-blue-500' : 'bg-gray-50 border border-gray-200 hover:bg-gray-100'}`} | |
onClick={() => setSelectedDate(date)} | |
> | |
<div className="font-medium text-gray-700">{formatDate(date)}</div> | |
</div> | |
))} | |
</div> | |
</div> | |
{selectedDate && ( | |
<div className="mt-6"> | |
<h3 className="text-lg font-medium text-gray-700 mb-3">Available Time Slots</h3> | |
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3"> | |
{timeSlots.map((time, index) => ( | |
<div | |
key={index} | |
className={`time-slot py-2 px-1 rounded-md text-center cursor-pointer ${selectedTime === time ? 'bg-blue-600 text-white' : isSlotAvailable(time, selectedProfessional || 1) ? 'bg-gray-100 hover:bg-blue-100 text-gray-800' : 'bg-gray-200 text-gray-400 cursor-not-allowed'}`} | |
onClick={() => isSlotAvailable(time, selectedProfessional || 1) && setSelectedTime(time)} | |
> | |
{time} | |
</div> | |
))} | |
</div> | |
</div> | |
)} | |
<div className="mt-8 flex justify-between"> | |
<button | |
onClick={() => setStep(1)} | |
className="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors" | |
> | |
Back | |
</button> | |
<button | |
onClick={() => setStep(3)} | |
disabled={!selectedDate || !selectedTime} | |
className={`px-6 py-2 rounded-md ${selectedDate && selectedTime ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-gray-300 text-gray-500 cursor-not-allowed'} transition-colors`} | |
> | |
Next | |
</button> | |
</div> | |
</div> | |
)} | |
{/* Step 3: Select Professional */} | |
{step === 3 && ( | |
<div className="fade-in"> | |
<h2 className="text-xl font-semibold text-gray-800 mb-4">Choose Your Professional</h2> | |
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
{professionals.map((professional) => ( | |
<div | |
key={professional.id} | |
className={`p-4 border rounded-lg cursor-pointer transition-all ${selectedProfessional === professional.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-blue-300'}`} | |
onClick={() => setSelectedProfessional(professional.id)} | |
> | |
<div className="w-16 h-16 rounded-full bg-gray-300 flex items-center justify-center text-2xl text-gray-500 mb-3 mx-auto"> | |
<i className="fas fa-user"></i> | |
</div> | |
<div className="text-center"> | |
<div className="font-semibold text-gray-800">{professional.name}</div> | |
<div className="text-sm text-gray-600 mt-1">{professional.specialty}</div> | |
{selectedProfessional === professional.id && ( | |
<div className="mt-2 text-sm text-green-600"> | |
<i className="fas fa-check-circle mr-1"></i> Selected | |
</div> | |
)} | |
</div> | |
</div> | |
))} | |
</div> | |
<div className="mt-8 flex justify-between"> | |
<button | |
onClick={() => setStep(2)} | |
className="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors" | |
> | |
Back | |
</button> | |
<button | |
onClick={() => setStep(4)} | |
disabled={!selectedProfessional} | |
className={`px-6 py-2 rounded-md ${selectedProfessional ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-gray-300 text-gray-500 cursor-not-allowed'} transition-colors`} | |
> | |
Next | |
</button> | |
</div> | |
</div> | |
)} | |
{/* Step 4: Customer Details */} | |
{step === 4 && ( | |
<form onSubmit={handleSubmit} className="fade-in"> | |
<h2 className="text-xl font-semibold text-gray-800 mb-4">Your Information</h2> | |
<div className="mb-6 p-4 bg-gray-50 rounded-lg"> | |
<h3 className="font-medium text-gray-700 mb-3">Appointment Summary</h3> | |
<div className="space-y-2"> | |
<div className="flex justify-between"> | |
<span className="text-gray-600">Service:</span> | |
<span className="font-medium">{selectedService.name}</span> | |
</div> | |
<div className="flex justify-between"> | |
<span className="text-gray-600">Date:</span> | |
<span className="font-medium">{selectedDate && formatDate(selectedDate)}</span> | |
</div> | |
<div className="flex justify-between"> | |
<span className="text-gray-600">Time:</span> | |
<span className="font-medium">{selectedTime}</span> | |
</div> | |
<div className="flex justify-between"> | |
<span className="text-gray-600">Professional:</span> | |
<span className="font-medium">{professionals.find(p => p.id === selectedProfessional).name}</span> | |
</div> | |
<div className="flex justify-between pt-2 mt-2 border-t border-gray-200"> | |
<span className="text-gray-600">Total:</span> | |
<span className="font-bold text-blue-600">${selectedService.price}</span> | |
</div> | |
</div> | |
</div> | |
<div className="space-y-4"> | |
<div> | |
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">Full Name *</label> | |
<input | |
type="text" | |
id="name" | |
value={name} | |
onChange={(e) => setName(e.target.value)} | |
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" | |
required | |
/> | |
</div> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<div> | |
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">Email *</label> | |
<input | |
type="email" | |
id="email" | |
value={email} | |
onChange={(e) => setEmail(e.target.value)} | |
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" | |
required | |
/> | |
</div> | |
<div> | |
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-1">Phone *</label> | |
<input | |
type="tel" | |
id="phone" | |
value={phone} | |
onChange={(e) => setPhone(e.target.value)} | |
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" | |
required | |
/> | |
</div> | |
</div> | |
<div> | |
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">Special Requests</label> | |
<textarea | |
id="notes" | |
value={notes} | |
onChange={(e) => setNotes(e.target.value)} | |
rows="3" | |
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" | |
/> | |
</div> | |
</div> | |
<div className="mt-8 flex justify-between"> | |
<button | |
onClick={() => setStep(3)} | |
type="button" | |
className="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors" | |
> | |
Back | |
</button> | |
<button | |
type="submit" | |
className="px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors flex items-center" | |
> | |
<i className="fas fa-calendar-check mr-2"></i> Confirm Booking | |
</button> | |
</div> | |
</form> | |
)} | |
{appointments.length > 0 && ( | |
<div className="mt-12"> | |
<h2 className="text-xl font-semibold text-gray-800 mb-4">Your Appointments</h2> | |
<div className="space-y-3"> | |
{appointments.map((appointment) => ( | |
<div key={appointment.id} className="p-4 border border-gray-200 rounded-lg"> | |
<div className="flex justify-between items-start"> | |
<div> | |
<div className="font-medium">{appointment.service}</div> | |
<div className="text-sm text-gray-600"> | |
{appointment.date} at {appointment.time} with {appointment.professional} | |
</div> | |
</div> | |
<div className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full flex items-center"> | |
<i className="fas fa-check-circle mr-1"></i> {appointment.status} | |
</div> | |
</div> | |
<div className="mt-2 pt-2 border-t border-gray-100"> | |
<div className="text-sm"><span className="font-medium">Customer:</span> {appointment.customer.name}</div> | |
</div> | |
</div> | |
))} | |
</div> | |
</div> | |
)} | |
</div> | |
</div> | |
</div> | |
); | |
} | |
ReactDOM.render(<App />, document.getElementById('root')); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=jasondos/appontment" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
</html> |