# Social Connections & Appointment Booking Extension ## Enhanced Database Schema ### 1. Extended User Model (backend/src/models/user.ts) ```typescript import { pgTable, serial, varchar, text, timestamp, pgEnum, boolean } from 'drizzle-orm/pg-core' import { InferSelectModel, InferInsertModel } from 'drizzle-orm' export const userRoleEnum = pgEnum('user_role', ['standard', 'pro', 'dev']) export const professionalTypeEnum = pgEnum('professional_type', [ 'doctor', 'dentist', 'therapist', 'consultant', 'lawyer', 'accountant', 'coach', 'tutor', 'other' ]) export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).notNull().unique(), passwordHash: text('password_hash').notNull(), firstName: varchar('first_name', { length: 100 }), lastName: varchar('last_name', { length: 100 }), role: userRoleEnum('role').default('standard').notNull(), // Professional Profile Fields isProfessional: boolean('is_professional').default(false).notNull(), professionalType: professionalTypeEnum('professional_type'), businessName: varchar('business_name', { length: 255 }), bio: text('bio'), specializations: text('specializations'), // JSON array as text phoneNumber: varchar('phone_number', { length: 20 }), address: text('address'), // Social & Discovery isPublicProfile: boolean('is_public_profile').default(false).notNull(), allowAppointmentBooking: boolean('allow_appointment_booking').default(false).notNull(), profileImageUrl: varchar('profile_image_url', { length: 500 }), // Existing fields isActive: varchar('is_active', { length: 10 }).default('true').notNull(), lastLoginAt: timestamp('last_login_at'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) export type User = InferSelectModel export type NewUser = InferInsertModel ``` ### 2. Professional Availability Model (backend/src/models/availability.ts) ```typescript import { pgTable, serial, integer, varchar, time, boolean, json } from 'drizzle-orm/pg-core' import { users } from './user.js' import { InferSelectModel, InferInsertModel } from 'drizzle-orm' export const dayOfWeekEnum = pgEnum('day_of_week', [ 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' ]) export const availability = pgTable('availability', { id: serial('id').primaryKey(), professionalId: integer('professional_id').references(() => users.id).notNull(), dayOfWeek: dayOfWeekEnum('day_of_week').notNull(), startTime: time('start_time').notNull(), endTime: time('end_time').notNull(), isAvailable: boolean('is_available').default(true).notNull(), slotDurationMinutes: integer('slot_duration_minutes').default(30).notNull(), breakBetweenSlots: integer('break_between_slots').default(0).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) export type Availability = InferSelectModel export type NewAvailability = InferInsertModel ``` ### 3. Appointment Model (backend/src/models/appointment.ts) ```typescript import { pgTable, serial, integer, varchar, text, timestamp, pgEnum, decimal } from 'drizzle-orm/pg-core' import { users } from './user.js' import { InferSelectModel, InferInsertModel } from 'drizzle-orm' export const appointmentStatusEnum = pgEnum('appointment_status', [ 'pending', 'confirmed', 'cancelled', 'completed', 'no_show' ]) export const appointments = pgTable('appointments', { id: serial('id').primaryKey(), professionalId: integer('professional_id').references(() => users.id).notNull(), clientId: integer('client_id').references(() => users.id).notNull(), title: varchar('title', { length: 255 }).notNull(), description: text('description'), status: appointmentStatusEnum('status').default('pending').notNull(), scheduledDate: timestamp('scheduled_date').notNull(), duration: integer('duration').notNull(), // minutes // Financial price: decimal('price', { precision: 10, scale: 2 }), currency: varchar('currency', { length: 3 }).default('USD'), isPaid: boolean('is_paid').default(false).notNull(), // Contact info clientEmail: varchar('client_email', { length: 255 }), clientPhone: varchar('client_phone', { length: 20 }), // Notes professionalNotes: text('professional_notes'), clientNotes: text('client_notes'), // Timestamps confirmedAt: timestamp('confirmed_at'), completedAt: timestamp('completed_at'), cancelledAt: timestamp('cancelled_at'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) export type Appointment = InferSelectModel export type NewAppointment = InferInsertModel ``` ### 4. Social Connections Model (backend/src/models/connection.ts) ```typescript import { pgTable, serial, integer, varchar, text, timestamp, pgEnum } from 'drizzle-orm/pg-core' import { users } from './user.js' import { InferSelectModel, InferInsertModel } from 'drizzle-orm' export const connectionStatusEnum = pgEnum('connection_status', [ 'pending', 'accepted', 'blocked' ]) export const connections = pgTable('connections', { id: serial('id').primaryKey(), requesterId: integer('requester_id').references(() => users.id).notNull(), receiverId: integer('receiver_id').references(() => users.id).notNull(), status: connectionStatusEnum('status').default('pending').notNull(), message: text('message'), // Optional message when sending connection request createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) export type Connection = InferSelectModel export type NewConnection = InferInsertModel ``` ### 5. Professional Reviews Model (backend/src/models/review.ts) ```typescript import { pgTable, serial, integer, varchar, text, timestamp } from 'drizzle-orm/pg-core' import { users } from './user.js' import { appointments } from './appointment.js' import { InferSelectModel, InferInsertModel } from 'drizzle-orm' export const reviews = pgTable('reviews', { id: serial('id').primaryKey(), professionalId: integer('professional_id').references(() => users.id).notNull(), clientId: integer('client_id').references(() => users.id).notNull(), appointmentId: integer('appointment_id').references(() => appointments.id), rating: integer('rating').notNull(), // 1-5 stars title: varchar('title', { length: 255 }), comment: text('comment'), isAnonymous: boolean('is_anonymous').default(false).notNull(), isPublic: boolean('is_public').default(true).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) export type Review = InferSelectModel export type NewReview = InferInsertModel ``` ## Backend API Extensions ### 1. Professional Profile Controller (backend/src/controllers/professionalController.ts) ```typescript import { Request, Response } from 'express' import { getDatabase } from '../config/database.js' import { users, availability, appointments, reviews } from '../models/index.js' import { eq, and, gte, desc, avg, count } from 'drizzle-orm' import { AuthenticatedRequest } from '../middleware/auth.js' import { asyncHandler, createError } from '../middleware/errorHandler.js' export const getProfessionalProfile = asyncHandler(async (req: Request, res: Response) => { const { professionalId } = req.params const db = getDatabase() const [professional] = await db .select({ id: users.id, firstName: users.firstName, lastName: users.lastName, businessName: users.businessName, professionalType: users.professionalType, bio: users.bio, specializations: users.specializations, phoneNumber: users.phoneNumber, address: users.address, profileImageUrl: users.profileImageUrl, allowAppointmentBooking: users.allowAppointmentBooking, }) .from(users) .where(and( eq(users.id, parseInt(professionalId)), eq(users.isProfessional, true), eq(users.isPublicProfile, true) )) .limit(1) if (!professional) { throw createError('Professional not found or profile not public', 404) } // Get availability const professionalAvailability = await db .select() .from(availability) .where(eq(availability.professionalId, parseInt(professionalId))) // Get reviews stats const [reviewStats] = await db .select({ averageRating: avg(reviews.rating), totalReviews: count(reviews.id) }) .from(reviews) .where(and( eq(reviews.professionalId, parseInt(professionalId)), eq(reviews.isPublic, true) )) // Get recent reviews const recentReviews = await db .select({ id: reviews.id, rating: reviews.rating, title: reviews.title, comment: reviews.comment, isAnonymous: reviews.isAnonymous, createdAt: reviews.createdAt, clientName: users.firstName }) .from(reviews) .leftJoin(users, eq(reviews.clientId, users.id)) .where(and( eq(reviews.professionalId, parseInt(professionalId)), eq(reviews.isPublic, true) )) .orderBy(desc(reviews.createdAt)) .limit(5) res.json({ success: true, professional: { ...professional, specializations: professional.specializations ? JSON.parse(professional.specializations) : [] }, availability: professionalAvailability, stats: { averageRating: reviewStats.averageRating ? Number(reviewStats.averageRating).toFixed(1) : null, totalReviews: reviewStats.totalReviews || 0 }, recentReviews: recentReviews.map(review => ({ ...review, clientName: review.isAnonymous ? 'Anonymous' : review.clientName })) }) }) export const searchProfessionals = asyncHandler(async (req: Request, res: Response) => { const { type, location, specialization, page = 1, limit = 10 } = req.query const db = getDatabase() let query = db .select({ id: users.id, firstName: users.firstName, lastName: users.lastName, businessName: users.businessName, professionalType: users.professionalType, bio: users.bio, specializations: users.specializations, address: users.address, profileImageUrl: users.profileImageUrl, }) .from(users) .where(and( eq(users.isProfessional, true), eq(users.isPublicProfile, true), eq(users.allowAppointmentBooking, true) )) // Add filters if (type) { query = query.where(eq(users.professionalType, type as string)) } const professionals = await query .limit(Number(limit)) .offset((Number(page) - 1) * Number(limit)) // Get ratings for each professional const professionalsWithRatings = await Promise.all( professionals.map(async (prof) => { const [stats] = await db .select({ averageRating: avg(reviews.rating), totalReviews: count(reviews.id) }) .from(reviews) .where(eq(reviews.professionalId, prof.id)) return { ...prof, specializations: prof.specializations ? JSON.parse(prof.specializations) : [], averageRating: stats.averageRating ? Number(stats.averageRating).toFixed(1) : null, totalReviews: stats.totalReviews || 0 } }) ) res.json({ success: true, professionals: professionalsWithRatings, pagination: { page: Number(page), limit: Number(limit), total: professionals.length } }) }) export const updateProfessionalProfile = asyncHandler(async (req: AuthenticatedRequest, res: Response) => { const userId = req.user!.id const { isProfessional, professionalType, businessName, bio, specializations, phoneNumber, address, isPublicProfile, allowAppointmentBooking } = req.body const db = getDatabase() const [updatedUser] = await db .update(users) .set({ isProfessional, professionalType, businessName, bio, specializations: specializations ? JSON.stringify(specializations) : null, phoneNumber, address, isPublicProfile, allowAppointmentBooking, updatedAt: new Date() }) .where(eq(users.id, userId)) .returning() res.json({ success: true, message: 'Professional profile updated successfully', user: updatedUser }) }) ``` ### 2. Appointment Controller (backend/src/controllers/appointmentController.ts) ```typescript import { Request, Response } from 'express' import { getDatabase } from '../config/database.js' import { appointments, users, availability } from '../models/index.js' import { eq, and, gte, lte, or } from 'drizzle-orm' import { AuthenticatedRequest } from '../middleware/auth.js' import { asyncHandler, createError } from '../middleware/errorHandler.js' export const getAvailableSlots = asyncHandler(async (req: Request, res: Response) => { const { professionalId, date } = req.query const db = getDatabase() if (!professionalId || !date) { throw createError('Professional ID and date are required', 400) } const requestedDate = new Date(date as string) const dayOfWeek = requestedDate.toLocaleLowerCase().substring(0, 3) + 'day' // Get professional's availability for the day const professionalAvailability = await db .select() .from(availability) .where(and( eq(availability.professionalId, parseInt(professionalId as string)), eq(availability.dayOfWeek, dayOfWeek as any), eq(availability.isAvailable, true) )) if (professionalAvailability.length === 0) { return res.json({ success: true, slots: [], message: 'No availability for this day' }) } // Get existing appointments for the date const startOfDay = new Date(requestedDate) startOfDay.setHours(0, 0, 0, 0) const endOfDay = new Date(requestedDate) endOfDay.setHours(23, 59, 59, 999) const existingAppointments = await db .select() .from(appointments) .where(and( eq(appointments.professionalId, parseInt(professionalId as string)), gte(appointments.scheduledDate, startOfDay), lte(appointments.scheduledDate, endOfDay), or( eq(appointments.status, 'confirmed'), eq(appointments.status, 'pending') ) )) // Generate available slots const availableSlots: Array<{ time: string, available: boolean }> = [] for (const avail of professionalAvailability) { const startTime = new Date(`${date} ${avail.startTime}`) const endTime = new Date(`${date} ${avail.endTime}`) const slotDuration = avail.slotDurationMinutes || 30 const breakDuration = avail.breakBetweenSlots || 0 let currentTime = new Date(startTime) while (currentTime < endTime) { const slotEnd = new Date(currentTime.getTime() + slotDuration * 60000) if (slotEnd <= endTime) { const isBooked = existingAppointments.some(apt => { const aptStart = new Date(apt.scheduledDate) const aptEnd = new Date(aptStart.getTime() + apt.duration * 60000) return (currentTime >= aptStart && currentTime < aptEnd) || (slotEnd > aptStart && slotEnd <= aptEnd) }) availableSlots.push({ time: currentTime.toTimeString().substring(0, 5), available: !isBooked }) } currentTime = new Date(currentTime.getTime() + (slotDuration + breakDuration) * 60000) } } res.json({ success: true, date: date, slots: availableSlots }) }) export const bookAppointment = asyncHandler(async (req: AuthenticatedRequest, res: Response) => { const { professionalId, scheduledDate, duration, title, description, clientEmail, clientPhone, clientNotes } = req.body const clientId = req.user!.id const db = getDatabase() // Check if the slot is still available const existingAppointment = await db .select() .from(appointments) .where(and( eq(appointments.professionalId, professionalId), eq(appointments.scheduledDate, new Date(scheduledDate)), or( eq(appointments.status, 'confirmed'), eq(appointments.status, 'pending') ) )) .limit(1) if (existingAppointment.length > 0) { throw createError('This time slot is no longer available', 409) } // Create appointment const [newAppointment] = await db .insert(appointments) .values({ professionalId, clientId, scheduledDate: new Date(scheduledDate), duration: duration || 30, title, description, clientEmail, clientPhone, clientNotes, status: 'pending' }) .returning() // Get professional and client info for response const [professional] = await db .select({ firstName: users.firstName, lastName: users.lastName, businessName: users.businessName, email: users.email }) .from(users) .where(eq(users.id, professionalId)) .limit(1) res.status(201).json({ success: true, message: 'Appointment booked successfully', appointment: newAppointment, professional }) }) export const getAppointments = asyncHandler(async (req: AuthenticatedRequest, res: Response) => { const userId = req.user!.id const { type = 'all', status, startDate, endDate } = req.query const db = getDatabase() let query = db .select({ id: appointments.id, title: appointments.title, description: appointments.description, status: appointments.status, scheduledDate: appointments.scheduledDate, duration: appointments.duration, price: appointments.price, isPaid: appointments.isPaid, professionalNotes: appointments.professionalNotes, clientNotes: appointments.clientNotes, createdAt: appointments.createdAt, // Professional info professionalFirstName: users.firstName, professionalLastName: users.lastName, professionalBusinessName: users.businessName, professionalEmail: users.email, professionalPhone: users.phoneNumber, }) .from(appointments) .leftJoin(users, eq(appointments.professionalId, users.id)) // Filter by user type (as client or professional) if (type === 'client') { query = query.where(eq(appointments.clientId, userId)) } else if (type === 'professional') { query = query.where(eq(appointments.professionalId, userId)) } else { query = query.where(or( eq(appointments.clientId, userId), eq(appointments.professionalId, userId) )) } // Add additional filters if (status) { query = query.where(eq(appointments.status, status as any)) } if (startDate) { query = query.where(gte(appointments.scheduledDate, new Date(startDate as string))) } if (endDate) { query = query.where(lte(appointments.scheduledDate, new Date(endDate as string))) } const userAppointments = await query.orderBy(appointments.scheduledDate) res.json({ success: true, appointments: userAppointments }) }) export const updateAppointmentStatus = asyncHandler(async (req: AuthenticatedRequest, res: Response) => { const { appointmentId } = req.params const { status, professionalNotes } = req.body const userId = req.user!.id const db = getDatabase() // Check if user is the professional for this appointment const [appointment] = await db .select() .from(appointments) .where(eq(appointments.id, parseInt(appointmentId))) .limit(1) if (!appointment) { throw createError('Appointment not found', 404) } if (appointment.professionalId !== userId) { throw createError('Not authorized to update this appointment', 403) } const updateData: any = { status, updatedAt: new Date() } if (professionalNotes) { updateData.professionalNotes = professionalNotes } if (status === 'confirmed') { updateData.confirmedAt = new Date() } else if (status === 'completed') { updateData.completedAt = new Date() } else if (status === 'cancelled') { updateData.cancelledAt = new Date() } const [updatedAppointment] = await db .update(appointments) .set(updateData) .where(eq(appointments.id, parseInt(appointmentId))) .returning() res.json({ success: true, message: 'Appointment status updated successfully', appointment: updatedAppointment }) }) ``` ## Frontend Extensions ### 1. Professional Profile Page (frontend/src/pages/ProfessionalProfilePage.jsx) ```jsx import React, { useState, useEffect } from 'react' import { useParams } from 'react-router-dom' import { Calendar, Clock, Star, MapPin, Phone, Mail, User } from 'lucide-react' import { professionalService } from '../services/professionalService' import { appointmentService } from '../services/appointmentService' import LoadingSpinner from '../components/common/LoadingSpinner' import AppointmentBookingModal from '../components/appointments/AppointmentBookingModal' const ProfessionalProfilePage = () => { const { professionalId } = useParams() const [professional, setProfessional] = useState(null) const [loading, setLoading] = useState(true) const [showBookingModal, setShowBookingModal] = useState(false) const [selectedDate, setSelectedDate] = useState('') const [selectedTime, setSelectedTime] = useState('') const [availableSlots, setAvailableSlots] = useState([]) useEffect(() => { if (professionalId) { fetchProfessionalProfile() } }, [professionalId]) const fetchProfessionalProfile = async () => { try { const response = await professionalService.getProfessionalProfile(professionalId) if (response.success) { setProfessional(response.professional) } } catch (error) { console.error('Failed to fetch professional profile:', error) } finally { setLoading(false) } } const fetchAvailableSlots = async (date) => { try { const response = await appointmentService.getAvailableSlots(professionalId, date) if (response.success) { setAvailableSlots(response.slots) } } catch (error) { console.error('Failed to fetch available slots:', error) } } const handleDateSelect = (date) => { setSelectedDate(date) fetchAvailableSlots(date) } const handleTimeSelect = (time) => { setSelectedTime(time) setShowBookingModal(true) } if (loading) { return (
) } if (!professional) { return (

Professional Not Found

The requested professional profile could not be found.

) } return (
{/* Professional Header */}
{professional.profileImageUrl ? ( {`${professional.firstName} ) : (
)}

{professional.businessName || `${professional.firstName} ${professional.lastName}`}

{professional.professionalType}

{/* Rating */}
{[...Array(5)].map((_, i) => ( ))}
{professional.stats?.averageRating || 'No ratings'} ({professional.stats?.totalReviews || 0} reviews)
{/* Specializations */} {professional.specializations && professional.specializations.length > 0 && (
{professional.specializations.map((spec, index) => ( {spec} ))}
)} {/* Contact Info */}
{professional.address && (
{professional.address}
)} {professional.phoneNumber && (
{professional.phoneNumber}
)}
{/* Left Column - About & Reviews */}
{/* About Section */} {professional.bio && (

About

{professional.bio}

)} {/* Reviews Section */}

Recent Reviews

{professional.recentReviews && professional.recentReviews.length > 0 ? (
{professional.recentReviews.map((review) => (
{[...Array(5)].map((_, i) => ( ))}
by {review.clientName} • {new Date(review.createdAt).toLocaleDateString()}
{review.title && (

{review.title}

)} {review.comment && (

{review.comment}

)}
))}
) : (

No reviews yet

)}
{/* Right Column - Availability */}

Availability

{professional.availability && professional.availability.length > 0 ? (
{professional.availability.map((avail) => (
{avail.dayOfWeek} {avail.startTime} - {avail.endTime}
))}
) : (

No availability set

)}
{/* Quick Stats */}

Quick Stats

Average Rating {professional.stats?.averageRating || 'N/A'}
Total Reviews {professional.stats?.totalReviews || 0}
Professional Type {professional.professionalType}
{/* Appointment Booking Modal */} {showBookingModal && ( setShowBookingModal(false)} onBooked={() => { setShowBookingModal(false) // Could show success message or redirect }} /> )}
) } export default ProfessionalProfilePage ``` ### 2. Appointment Booking Modal (frontend/src/components/appointments/AppointmentBookingModal.jsx) ```jsx import React, { useState, useEffect } from 'react' import { X, Calendar, Clock, User, Mail, Phone, MessageSquare } from 'lucide-react' import { appointmentService } from '../../services/appointmentService' import LoadingSpinner from '../common/LoadingSpinner' const AppointmentBookingModal = ({ professionalId, professional, onClose, onBooked }) => { const [step, setStep] = useState(1) // 1: Date/Time, 2: Details, 3: Confirmation const [selectedDate, setSelectedDate] = useState('') const [selectedTime, setSelectedTime] = useState('') const [availableSlots, setAvailableSlots] = useState([]) const [loading, setLoading] = useState(false) const [bookingData, setBookingData] = useState({ title: '', description: '', clientEmail: '', clientPhone: '', clientNotes: '', duration: 30 }) const [errors, setErrors] = useState({}) // Generate next 30 days for date selection const getAvailableDates = () => { const dates = [] const today = new Date() for (let i = 1; i <= 30; i++) { const date = new Date(today) date.setDate(today.getDate() + i) // Skip weekends if professional doesn't work weekends const dayOfWeek = date.toLocaleDateString('en', { weekday: 'long' }).toLowerCase() const hasAvailability = professional.availability?.some( avail => avail.dayOfWeek === dayOfWeek && avail.isAvailable ) if (hasAvailability) { dates.push({ date: date.toISOString().split('T')[0], display: date.toLocaleDateString('en', { month: 'short', day: 'numeric', weekday: 'short' }) }) } } return dates } const fetchAvailableSlots = async (date) => { setLoading(true) try { const response = await appointmentService.getAvailableSlots(professionalId, date) if (response.success) { setAvailableSlots(response.slots || []) } } catch (error) { console.error('Failed to fetch available slots:', error) setAvailableSlots([]) } finally { setLoading(false) } } const handleDateSelect = (date) => { setSelectedDate(date) setSelectedTime('') fetchAvailableSlots(date) } const handleTimeSelect = (time) => { setSelectedTime(time) } const handleInputChange = (field, value) => { setBookingData(prev => ({ ...prev, [field]: value })) // Clear error for this field if (errors[field]) { setErrors(prev => ({ ...prev, [field]: '' })) } } const validateStep2 = () => { const newErrors = {} if (!bookingData.title.trim()) { newErrors.title = 'Appointment title is required' } if (!bookingData.clientEmail.trim()) { newErrors.clientEmail = 'Email is required' } else if (!/\S+@\S+\.\S+/.test(bookingData.clientEmail)) { newErrors.clientEmail = 'Valid email is required' } if (!bookingData.clientPhone.trim()) { newErrors.clientPhone = 'Phone number is required' } setErrors(newErrors) return Object.keys(newErrors).length === 0 } const handleBookAppointment = async () => { setLoading(true) try { const appointmentDateTime = new Date(`${selectedDate}T${selectedTime}:00`) const response = await appointmentService.bookAppointment({ professionalId: parseInt(professionalId), scheduledDate: appointmentDateTime.toISOString(), duration: bookingData.duration, title: bookingData.title, description: bookingData.description, clientEmail: bookingData.clientEmail, clientPhone: bookingData.clientPhone, clientNotes: bookingData.clientNotes }) if (response.success) { setStep(3) onBooked(response.appointment) } } catch (error) { console.error('Failed to book appointment:', error) alert('Failed to book appointment. Please try again.') } finally { setLoading(false) } } const nextStep = () => { if (step === 1 && selectedDate && selectedTime) { setStep(2) } else if (step === 2 && validateStep2()) { handleBookAppointment() } } const availableDates = getAvailableDates() return (
{/* Header */}

Book Appointment

{/* Content */}
{/* Professional Info */}

{professional.businessName || `${professional.firstName} ${professional.lastName}`}

{professional.professionalType}

{/* Step 1: Date and Time Selection */} {step === 1 && (

Select Date

{availableDates.map((dateOption) => ( ))}
{selectedDate && (

Select Time

{loading ? (
) : (
{availableSlots .filter(slot => slot.available) .map((slot) => ( ))}
)} {availableSlots.length === 0 && !loading && (

No available slots for this date

)}
)}
)} {/* Step 2: Appointment Details */} {step === 2 && (

Selected: {new Date(selectedDate).toLocaleDateString('en', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} at {selectedTime}

handleInputChange('title', e.target.value)} className={`input-field ${errors.title ? 'border-red-500' : ''}`} placeholder="e.g., Regular Check-up, Consultation" /> {errors.title &&

{errors.title}

}