2025-11-07 19:47:18 +00:00
|
|
|
"use client";
|
|
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
import { useState, useEffect, useMemo, useCallback } from "react";
|
2025-11-07 19:47:18 +00:00
|
|
|
import { Button } from "@/components/ui/button";
|
2025-11-13 11:10:00 +00:00
|
|
|
import { useAppTheme } from "@/components/ThemeProvider";
|
2025-11-07 19:47:18 +00:00
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
|
|
|
|
import {
|
|
|
|
|
Calendar,
|
|
|
|
|
Clock,
|
|
|
|
|
User,
|
|
|
|
|
Mail,
|
|
|
|
|
Phone,
|
|
|
|
|
MessageSquare,
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
Heart,
|
|
|
|
|
CheckCircle2,
|
2025-11-07 20:19:08 +00:00
|
|
|
CheckCircle,
|
|
|
|
|
Loader2,
|
2025-11-23 21:13:18 +00:00
|
|
|
LogOut,
|
2025-11-25 21:04:22 +00:00
|
|
|
CalendarCheck,
|
2025-11-07 19:47:18 +00:00
|
|
|
} from "lucide-react";
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import Image from "next/image";
|
|
|
|
|
import { useRouter } from "next/navigation";
|
2025-11-07 20:19:08 +00:00
|
|
|
import { LoginDialog } from "@/components/LoginDialog";
|
2025-11-25 20:15:37 +00:00
|
|
|
import { SignupDialog } from "@/components/SignupDialog";
|
2025-11-23 21:13:18 +00:00
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
2025-11-23 21:43:13 +00:00
|
|
|
import { useAppointments } from "@/hooks/useAppointments";
|
2025-11-23 21:13:18 +00:00
|
|
|
import { toast } from "sonner";
|
2025-11-23 21:43:13 +00:00
|
|
|
import type { Appointment } from "@/lib/models/appointments";
|
2025-11-07 20:19:08 +00:00
|
|
|
|
|
|
|
|
interface User {
|
|
|
|
|
ID: number;
|
|
|
|
|
CreatedAt?: string;
|
|
|
|
|
UpdatedAt?: string;
|
|
|
|
|
DeletedAt?: string | null;
|
|
|
|
|
first_name: string;
|
|
|
|
|
last_name: string;
|
|
|
|
|
email: string;
|
|
|
|
|
phone: string;
|
|
|
|
|
location: string;
|
|
|
|
|
date_of_birth?: string;
|
|
|
|
|
is_admin?: boolean;
|
|
|
|
|
bookings?: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Booking {
|
|
|
|
|
ID: number;
|
|
|
|
|
CreatedAt: string;
|
|
|
|
|
UpdatedAt: string;
|
|
|
|
|
DeletedAt: string | null;
|
|
|
|
|
user_id: number;
|
|
|
|
|
user: User;
|
|
|
|
|
scheduled_at: string;
|
|
|
|
|
duration: number;
|
|
|
|
|
status: string;
|
|
|
|
|
jitsi_room_id: string;
|
|
|
|
|
jitsi_room_url: string;
|
|
|
|
|
payment_id: string;
|
|
|
|
|
payment_status: string;
|
|
|
|
|
amount: number;
|
|
|
|
|
notes: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface BookingsResponse {
|
|
|
|
|
bookings: Booking[];
|
|
|
|
|
limit: number;
|
|
|
|
|
offset: number;
|
|
|
|
|
total: number;
|
|
|
|
|
}
|
2025-11-07 19:47:18 +00:00
|
|
|
|
|
|
|
|
export default function BookNowPage() {
|
|
|
|
|
const router = useRouter();
|
2025-11-13 11:10:00 +00:00
|
|
|
const { theme } = useAppTheme();
|
|
|
|
|
const isDark = theme === "dark";
|
2025-11-23 21:13:18 +00:00
|
|
|
const { isAuthenticated, logout } = useAuth();
|
2025-11-27 19:18:59 +00:00
|
|
|
const {
|
|
|
|
|
create,
|
|
|
|
|
isCreating,
|
|
|
|
|
weeklyAvailability,
|
|
|
|
|
isLoadingWeeklyAvailability,
|
|
|
|
|
availabilityOverview,
|
|
|
|
|
isLoadingAvailabilityOverview,
|
|
|
|
|
availabilityConfig,
|
|
|
|
|
} = useAppointments();
|
2025-11-07 19:47:18 +00:00
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
|
firstName: "",
|
|
|
|
|
lastName: "",
|
|
|
|
|
email: "",
|
|
|
|
|
phone: "",
|
2025-11-27 19:18:59 +00:00
|
|
|
selectedSlots: [] as Array<{ day: number; time_slot: string }>, // New format
|
2025-11-07 19:47:18 +00:00
|
|
|
message: "",
|
|
|
|
|
});
|
2025-11-07 20:19:08 +00:00
|
|
|
const [booking, setBooking] = useState<Booking | null>(null);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
2025-11-25 20:15:37 +00:00
|
|
|
const [showSignupDialog, setShowSignupDialog] = useState(false);
|
|
|
|
|
const [loginPrefillEmail, setLoginPrefillEmail] = useState<string | undefined>(undefined);
|
2025-11-27 19:18:59 +00:00
|
|
|
|
|
|
|
|
// Helper function to convert day name to day number (0-6)
|
|
|
|
|
const getDayNumber = (dayName: string): number => {
|
|
|
|
|
const dayMap: Record<string, number> = {
|
|
|
|
|
'monday': 0,
|
|
|
|
|
'tuesday': 1,
|
|
|
|
|
'wednesday': 2,
|
|
|
|
|
'thursday': 3,
|
|
|
|
|
'friday': 4,
|
|
|
|
|
'saturday': 5,
|
|
|
|
|
'sunday': 6,
|
2025-11-25 21:04:22 +00:00
|
|
|
};
|
2025-11-27 19:18:59 +00:00
|
|
|
return dayMap[dayName.toLowerCase()] ?? -1;
|
|
|
|
|
};
|
2025-11-25 21:04:22 +00:00
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
// Get available days from availability overview (primary) or weekly availability (fallback)
|
2025-11-25 21:04:22 +00:00
|
|
|
const availableDaysOfWeek = useMemo(() => {
|
2025-11-27 19:18:59 +00:00
|
|
|
// Try availability overview first (preferred)
|
|
|
|
|
if (availabilityOverview && availabilityOverview.available && availabilityOverview.next_available_dates && availabilityOverview.next_available_dates.length > 0) {
|
|
|
|
|
// Group by day name and get unique days with their slots from next_available_dates
|
|
|
|
|
const dayMap = new Map<string, { day: number; dayName: string; availableSlots: Set<string> }>();
|
|
|
|
|
|
|
|
|
|
availabilityOverview.next_available_dates.forEach((dateInfo: any) => {
|
|
|
|
|
if (!dateInfo || !dateInfo.day_name) return;
|
|
|
|
|
|
|
|
|
|
const dayName = String(dateInfo.day_name).trim();
|
|
|
|
|
const dayNum = getDayNumber(dayName);
|
|
|
|
|
|
|
|
|
|
if (dayNum >= 0 && dayNum <= 6 && dateInfo.available_slots && Array.isArray(dateInfo.available_slots) && dateInfo.available_slots.length > 0) {
|
|
|
|
|
const existingDay = dayMap.get(dayName);
|
|
|
|
|
|
|
|
|
|
if (existingDay) {
|
|
|
|
|
// Merge slots if day already exists
|
|
|
|
|
dateInfo.available_slots.forEach((slot: string) => {
|
|
|
|
|
existingDay.availableSlots.add(String(slot).toLowerCase().trim());
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// Create new day entry
|
|
|
|
|
const slotsSet = new Set<string>();
|
|
|
|
|
dateInfo.available_slots.forEach((slot: string) => {
|
|
|
|
|
slotsSet.add(String(slot).toLowerCase().trim());
|
|
|
|
|
});
|
|
|
|
|
dayMap.set(dayName, {
|
|
|
|
|
day: dayNum,
|
|
|
|
|
dayName: dayName,
|
|
|
|
|
availableSlots: slotsSet,
|
|
|
|
|
});
|
2025-12-01 17:35:28 +00:00
|
|
|
}
|
2025-11-27 19:18:59 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Convert Map values to array and sort by day number
|
|
|
|
|
return Array.from(dayMap.values())
|
|
|
|
|
.map(day => ({
|
|
|
|
|
day: day.day,
|
|
|
|
|
dayName: day.dayName,
|
|
|
|
|
availableSlots: Array.from(day.availableSlots),
|
|
|
|
|
}))
|
|
|
|
|
.sort((a, b) => a.day - b.day);
|
2025-11-25 21:04:22 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
// Fallback to weekly availability
|
|
|
|
|
if (weeklyAvailability) {
|
|
|
|
|
// Handle both array format and object with 'week' property
|
|
|
|
|
const weekArray = Array.isArray(weeklyAvailability)
|
|
|
|
|
? weeklyAvailability
|
|
|
|
|
: (weeklyAvailability as any)?.week;
|
|
|
|
|
|
|
|
|
|
if (weekArray && Array.isArray(weekArray)) {
|
|
|
|
|
return weekArray
|
|
|
|
|
.filter(day => {
|
|
|
|
|
const dayNum = Number(day.day);
|
|
|
|
|
return day.is_available &&
|
|
|
|
|
day.available_slots &&
|
|
|
|
|
Array.isArray(day.available_slots) &&
|
|
|
|
|
day.available_slots.length > 0 &&
|
|
|
|
|
!isNaN(dayNum) &&
|
|
|
|
|
dayNum >= 0 &&
|
|
|
|
|
dayNum <= 6;
|
|
|
|
|
})
|
|
|
|
|
.map(day => ({
|
|
|
|
|
day: Number(day.day),
|
|
|
|
|
dayName: day.day_name || 'Unknown',
|
|
|
|
|
availableSlots: day.available_slots || [],
|
|
|
|
|
}))
|
|
|
|
|
.sort((a, b) => a.day - b.day);
|
2025-11-25 21:04:22 +00:00
|
|
|
}
|
2025-11-27 19:18:59 +00:00
|
|
|
}
|
2025-11-25 21:04:22 +00:00
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
return [];
|
|
|
|
|
}, [availabilityOverview, weeklyAvailability]);
|
2025-11-07 19:47:18 +00:00
|
|
|
|
2025-11-23 21:13:18 +00:00
|
|
|
const handleLogout = () => {
|
|
|
|
|
logout();
|
|
|
|
|
toast.success("Logged out successfully");
|
|
|
|
|
router.push("/");
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-13 13:20:04 +00:00
|
|
|
// Handle submit button click
|
2025-11-07 20:19:08 +00:00
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
2025-11-07 19:47:18 +00:00
|
|
|
e.preventDefault();
|
2025-11-23 21:43:13 +00:00
|
|
|
|
|
|
|
|
// Check if user is authenticated
|
|
|
|
|
if (!isAuthenticated) {
|
|
|
|
|
// Open login dialog if not authenticated
|
|
|
|
|
setShowLoginDialog(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If authenticated, proceed with booking
|
|
|
|
|
await submitBooking();
|
2025-11-07 20:19:08 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleLoginSuccess = async () => {
|
2025-11-23 21:43:13 +00:00
|
|
|
// Close login dialog
|
|
|
|
|
setShowLoginDialog(false);
|
2025-11-07 20:19:08 +00:00
|
|
|
// After successful login, proceed with booking submission
|
|
|
|
|
await submitBooking();
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-25 20:15:37 +00:00
|
|
|
const handleSignupSuccess = async () => {
|
|
|
|
|
// Close signup dialog
|
|
|
|
|
setShowSignupDialog(false);
|
|
|
|
|
// After successful signup, proceed with booking submission
|
|
|
|
|
await submitBooking();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSwitchToSignup = () => {
|
|
|
|
|
// Close login dialog and open signup dialog
|
|
|
|
|
setShowLoginDialog(false);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setShowSignupDialog(true);
|
|
|
|
|
}, 100);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSwitchToLogin = (email?: string) => {
|
|
|
|
|
// Close signup dialog and open login dialog with email prefilled
|
|
|
|
|
setShowSignupDialog(false);
|
|
|
|
|
setLoginPrefillEmail(email);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setShowLoginDialog(true);
|
|
|
|
|
}, 100);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-07 20:19:08 +00:00
|
|
|
const submitBooking = async () => {
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-27 19:18:59 +00:00
|
|
|
// Get current slots from formData
|
|
|
|
|
const currentSlots = formData.selectedSlots || [];
|
|
|
|
|
|
|
|
|
|
// Check if slots are selected
|
|
|
|
|
if (!currentSlots || currentSlots.length === 0) {
|
|
|
|
|
setError("Please select at least one day and time slot combination by clicking on the time slot buttons.");
|
2025-11-22 20:18:52 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
// Prepare and validate slots - be very lenient
|
|
|
|
|
const validSlots = currentSlots
|
|
|
|
|
.map(slot => {
|
|
|
|
|
if (!slot) return null;
|
2025-11-26 11:42:31 +00:00
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
// Get day - handle any format
|
|
|
|
|
let dayNum: number;
|
|
|
|
|
if (typeof slot.day === 'number') {
|
|
|
|
|
dayNum = slot.day;
|
|
|
|
|
} else {
|
|
|
|
|
dayNum = parseInt(String(slot.day || 0), 10);
|
2025-11-26 11:42:31 +00:00
|
|
|
}
|
2025-11-27 19:18:59 +00:00
|
|
|
|
|
|
|
|
// Validate day
|
|
|
|
|
if (isNaN(dayNum) || dayNum < 0 || dayNum > 6) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get time_slot - normalize
|
|
|
|
|
const timeSlot = String(slot.time_slot || '').trim().toLowerCase();
|
|
|
|
|
|
|
|
|
|
// Validate time_slot - accept morning, afternoon, evening
|
|
|
|
|
if (!timeSlot || !['morning', 'afternoon', 'evening'].includes(timeSlot)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
day: dayNum,
|
|
|
|
|
time_slot: timeSlot as "morning" | "afternoon" | "evening",
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter((slot): slot is { day: number; time_slot: "morning" | "afternoon" | "evening" } => slot !== null);
|
|
|
|
|
|
|
|
|
|
// Final validation check
|
|
|
|
|
if (!validSlots || validSlots.length === 0) {
|
|
|
|
|
setError("Please select at least one day and time slot combination by clicking on the time slot buttons.");
|
|
|
|
|
return;
|
2025-11-26 11:42:31 +00:00
|
|
|
}
|
2025-11-27 19:18:59 +00:00
|
|
|
|
|
|
|
|
// Validate and limit field lengths to prevent database errors
|
|
|
|
|
const firstName = formData.firstName.trim().substring(0, 100);
|
|
|
|
|
const lastName = formData.lastName.trim().substring(0, 100);
|
|
|
|
|
const email = formData.email.trim().toLowerCase().substring(0, 100);
|
|
|
|
|
const phone = formData.phone ? formData.phone.trim().substring(0, 100) : undefined;
|
|
|
|
|
const reason = formData.message ? formData.message.trim().substring(0, 100) : undefined;
|
|
|
|
|
|
|
|
|
|
// Validate required fields
|
|
|
|
|
if (!firstName || firstName.length === 0) {
|
|
|
|
|
setError("First name is required.");
|
|
|
|
|
return;
|
2025-12-01 17:35:28 +00:00
|
|
|
}
|
2025-11-27 19:18:59 +00:00
|
|
|
if (!lastName || lastName.length === 0) {
|
|
|
|
|
setError("Last name is required.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!email || email.length === 0) {
|
|
|
|
|
setError("Email address is required.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!email.includes('@')) {
|
|
|
|
|
setError("Please enter a valid email address.");
|
2025-11-26 11:42:31 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2025-11-23 21:43:13 +00:00
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
// Prepare payload with validated and limited fields
|
2025-11-07 20:19:08 +00:00
|
|
|
const payload = {
|
2025-11-27 19:18:59 +00:00
|
|
|
first_name: firstName,
|
|
|
|
|
last_name: lastName,
|
|
|
|
|
email: email,
|
|
|
|
|
selected_slots: validSlots,
|
|
|
|
|
...(phone && phone.length > 0 && { phone: phone }),
|
|
|
|
|
...(reason && reason.length > 0 && { reason: reason }),
|
2025-11-07 20:19:08 +00:00
|
|
|
};
|
|
|
|
|
|
2025-11-23 21:43:13 +00:00
|
|
|
// Call the actual API using the hook
|
|
|
|
|
const appointmentData = await create(payload);
|
|
|
|
|
|
|
|
|
|
// Convert API response to Booking format for display
|
2025-11-24 16:04:39 +00:00
|
|
|
// Use a stable ID - if appointmentData.id exists, use it, otherwise use 0
|
|
|
|
|
const appointmentId = appointmentData.id ? parseInt(appointmentData.id, 10) : 0;
|
|
|
|
|
const now = new Date().toISOString();
|
|
|
|
|
|
2025-11-23 21:43:13 +00:00
|
|
|
const bookingData: Booking = {
|
2025-11-24 16:04:39 +00:00
|
|
|
ID: appointmentId || 0,
|
|
|
|
|
CreatedAt: appointmentData.created_at || now,
|
|
|
|
|
UpdatedAt: appointmentData.updated_at || now,
|
2025-11-23 21:43:13 +00:00
|
|
|
DeletedAt: null,
|
|
|
|
|
user_id: 0, // API doesn't return user_id in this response
|
|
|
|
|
user: {
|
|
|
|
|
ID: 0,
|
|
|
|
|
first_name: appointmentData.first_name,
|
|
|
|
|
last_name: appointmentData.last_name,
|
|
|
|
|
email: appointmentData.email,
|
|
|
|
|
phone: appointmentData.phone || "",
|
|
|
|
|
location: "",
|
|
|
|
|
is_admin: false,
|
|
|
|
|
bookings: null,
|
2025-11-07 20:19:08 +00:00
|
|
|
},
|
2025-11-23 21:43:13 +00:00
|
|
|
scheduled_at: appointmentData.scheduled_datetime || "",
|
|
|
|
|
duration: appointmentData.scheduled_duration || 60,
|
|
|
|
|
status: appointmentData.status || "pending_review",
|
|
|
|
|
jitsi_room_id: appointmentData.jitsi_room_id || "",
|
|
|
|
|
jitsi_room_url: appointmentData.jitsi_meet_url || "",
|
|
|
|
|
payment_id: "",
|
|
|
|
|
payment_status: "pending",
|
|
|
|
|
amount: 0,
|
|
|
|
|
notes: appointmentData.reason || "",
|
|
|
|
|
};
|
2025-11-07 20:19:08 +00:00
|
|
|
|
|
|
|
|
setBooking(bookingData);
|
2025-11-23 21:43:13 +00:00
|
|
|
toast.success("Appointment request submitted successfully! We'll review and get back to you soon.");
|
2025-11-07 21:27:14 +00:00
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
// Stay on the booking page to show the receipt - no redirect
|
2025-11-07 20:19:08 +00:00
|
|
|
} catch (err) {
|
2025-11-23 21:43:13 +00:00
|
|
|
const errorMessage = err instanceof Error ? err.message : "Failed to submit booking. Please try again.";
|
|
|
|
|
setError(errorMessage);
|
|
|
|
|
toast.error(errorMessage);
|
2025-11-07 20:19:08 +00:00
|
|
|
}
|
2025-11-07 19:47:18 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleChange = (field: string, value: string) => {
|
|
|
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
// Handle slot selection (day + time slot combination)
|
|
|
|
|
const handleSlotToggle = (day: number, timeSlot: string) => {
|
2025-11-22 20:18:52 +00:00
|
|
|
setFormData((prev) => {
|
2025-11-27 19:18:59 +00:00
|
|
|
const normalizedDay = Number(day);
|
|
|
|
|
const normalizedTimeSlot = String(timeSlot).toLowerCase().trim();
|
|
|
|
|
const currentSlots = prev.selectedSlots || [];
|
|
|
|
|
|
|
|
|
|
// Helper to check if two slots match
|
|
|
|
|
const slotsMatch = (slot1: { day: number; time_slot: string }, slot2: { day: number; time_slot: string }) => {
|
|
|
|
|
return Number(slot1.day) === Number(slot2.day) &&
|
|
|
|
|
String(slot1.time_slot).toLowerCase().trim() === String(slot2.time_slot).toLowerCase().trim();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const targetSlot = { day: normalizedDay, time_slot: normalizedTimeSlot };
|
|
|
|
|
|
|
|
|
|
// Check if this exact slot exists
|
|
|
|
|
const slotExists = currentSlots.some(slot => slotsMatch(slot, targetSlot));
|
|
|
|
|
|
|
|
|
|
if (slotExists) {
|
|
|
|
|
// Remove the slot
|
|
|
|
|
const newSlots = currentSlots.filter(slot => !slotsMatch(slot, targetSlot));
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
selectedSlots: newSlots,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
// Add the slot
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
selectedSlots: [...currentSlots, targetSlot],
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-11-22 20:18:52 +00:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-27 19:18:59 +00:00
|
|
|
// Check if a slot is selected
|
|
|
|
|
const isSlotSelected = (day: number, timeSlot: string): boolean => {
|
|
|
|
|
const normalizedDay = Number(day);
|
|
|
|
|
const normalizedTimeSlot = String(timeSlot).toLowerCase().trim();
|
|
|
|
|
|
|
|
|
|
return (formData.selectedSlots || []).some(
|
|
|
|
|
slot => Number(slot.day) === normalizedDay &&
|
|
|
|
|
String(slot.time_slot).toLowerCase().trim() === normalizedTimeSlot
|
|
|
|
|
);
|
2025-11-22 20:18:52 +00:00
|
|
|
};
|
|
|
|
|
|
2025-11-07 20:19:08 +00:00
|
|
|
const formatDateTime = (dateString: string) => {
|
|
|
|
|
const date = new Date(dateString);
|
|
|
|
|
return date.toLocaleString("en-US", {
|
|
|
|
|
month: "short",
|
|
|
|
|
day: "numeric",
|
|
|
|
|
year: "numeric",
|
|
|
|
|
hour: "numeric",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
hour12: true,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-07 19:47:18 +00:00
|
|
|
return (
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`min-h-screen ${isDark ? 'bg-gray-900' : 'bg-white'}`}>
|
2025-11-07 19:47:18 +00:00
|
|
|
{/* Main Content */}
|
|
|
|
|
<main className="min-h-screen flex">
|
|
|
|
|
{/* Left Side - Image (Fixed) */}
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`hidden lg:block fixed top-0 left-0 h-screen w-1/2 overflow-hidden z-10 bg-gradient-to-br ${isDark ? 'from-gray-900 via-gray-800 to-gray-900' : 'from-rose-100 via-pink-50 to-orange-50'}`}>
|
2025-11-07 19:47:18 +00:00
|
|
|
<div className="absolute inset-0">
|
|
|
|
|
<Image
|
2025-11-22 20:18:52 +00:00
|
|
|
src="/session.jpg"
|
2025-11-21 23:38:16 +00:00
|
|
|
alt="Therapy session with diverse clients"
|
2025-11-07 19:47:18 +00:00
|
|
|
fill
|
|
|
|
|
className="object-cover"
|
|
|
|
|
priority
|
|
|
|
|
sizes="50vw"
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute inset-0 bg-black/50"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Logo at Top */}
|
|
|
|
|
<div className="absolute top-0 left-0 right-0 z-20 flex items-center p-6">
|
|
|
|
|
<Link href="/" className="flex items-center gap-2">
|
|
|
|
|
<div className="bg-gradient-to-r from-rose-500 to-pink-600 p-2 rounded-xl">
|
|
|
|
|
<Heart className="h-5 w-5 text-white fill-white" />
|
|
|
|
|
</div>
|
2025-11-13 11:10:00 +00:00
|
|
|
<span className={`font-bold text-lg drop-shadow-lg ${isDark ? 'text-rose-400' : 'text-rose-500'}`}>
|
2025-11-07 19:47:18 +00:00
|
|
|
Attune Heart Therapy
|
|
|
|
|
</span>
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Overlay Content - Lower Position */}
|
|
|
|
|
<div className="relative z-10 w-full h-full flex items-end justify-center px-12 pb-20">
|
|
|
|
|
<div className="space-y-4 text-center max-w-sm">
|
|
|
|
|
<h2 className="text-xl md:text-2xl font-bold leading-tight text-white drop-shadow-lg">
|
|
|
|
|
Begin Your Journey to Wellness
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="text-sm md:text-base text-white/95 leading-relaxed drop-shadow-md">
|
|
|
|
|
Take the first step towards healing and growth. Our compassionate team is here to support you every step of the way.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{/* Features List */}
|
|
|
|
|
<div className="space-y-2 pt-3">
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<div className="w-7 h-7 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0 border border-white/30">
|
|
|
|
|
<CheckCircle2 className="w-3.5 h-3.5 text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-white/95 text-xs md:text-sm">Safe and confidential environment</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<div className="w-7 h-7 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0 border border-white/30">
|
|
|
|
|
<CheckCircle2 className="w-3.5 h-3.5 text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-white/95 text-xs md:text-sm">Experienced licensed therapists</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<div className="w-7 h-7 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0 border border-white/30">
|
|
|
|
|
<CheckCircle2 className="w-3.5 h-3.5 text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-white/95 text-xs md:text-sm">Personalized treatment plans</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
<div className="w-7 h-7 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0 border border-white/30">
|
|
|
|
|
<CheckCircle2 className="w-3.5 h-3.5 text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-white/95 text-xs md:text-sm">Flexible scheduling options</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Right Side - Form (Scrollable) */}
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`w-full lg:w-1/2 lg:ml-auto fixed top-0 right-0 h-screen overflow-y-auto custom-scrollbar ${isDark ? 'bg-gray-900' : 'bg-white'}`}>
|
2025-11-07 19:47:18 +00:00
|
|
|
<div className="flex items-start justify-center min-h-full">
|
|
|
|
|
<div className="w-full max-w-2xl">
|
|
|
|
|
{/* Page Header */}
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className="pt-4 sm:pt-6 lg:pt-8 px-4 sm:px-6 lg:px-12 pb-4 sm:pb-6">
|
2025-11-07 19:47:18 +00:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => router.back()}
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`flex items-center gap-2 mb-3 sm:mb-4 ${isDark ? 'text-white hover:bg-gray-800' : 'text-black hover:bg-gray-100'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
>
|
2025-11-13 11:10:00 +00:00
|
|
|
<ArrowLeft className="w-4 h-4 sm:w-5 sm:h-5" />
|
|
|
|
|
<span className="hidden sm:inline text-sm sm:text-base">Back</span>
|
2025-11-07 19:47:18 +00:00
|
|
|
</Button>
|
|
|
|
|
<div>
|
2025-11-13 11:10:00 +00:00
|
|
|
<h1 className={`text-xl sm:text-2xl font-semibold mb-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
2025-11-07 19:47:18 +00:00
|
|
|
Book Your Appointment
|
|
|
|
|
</h1>
|
2025-11-13 11:10:00 +00:00
|
|
|
<p className={`text-xs sm:text-sm ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
|
2025-11-07 19:47:18 +00:00
|
|
|
Fill out the form below and we'll get back to you to confirm your appointment
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-07 20:19:08 +00:00
|
|
|
{/* Booking Form or Success Message */}
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className="px-4 sm:px-6 lg:px-12 pb-6 sm:pb-8 lg:pb-12">
|
2025-11-07 20:19:08 +00:00
|
|
|
{booking ? (
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`rounded-xl sm:rounded-2xl shadow-lg p-4 sm:p-6 lg:p-8 border ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
|
2025-11-27 19:18:59 +00:00
|
|
|
<div className="text-center space-y-6">
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`mx-auto w-16 h-16 rounded-full flex items-center justify-center ${isDark ? 'bg-green-900/30' : 'bg-green-100'}`}>
|
|
|
|
|
<CheckCircle className={`w-8 h-8 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
|
2025-11-07 20:19:08 +00:00
|
|
|
</div>
|
|
|
|
|
<div>
|
2025-11-13 11:10:00 +00:00
|
|
|
<h2 className={`text-2xl font-semibold mb-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
2025-11-27 19:18:59 +00:00
|
|
|
Booking Request Submitted!
|
2025-11-07 20:19:08 +00:00
|
|
|
</h2>
|
2025-11-27 19:18:59 +00:00
|
|
|
<p className={`text-base ${isDark ? 'text-gray-300' : 'text-gray-600'}`}>
|
|
|
|
|
Your appointment request has been received.
|
2025-11-07 20:19:08 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`rounded-lg p-6 space-y-4 text-left ${isDark ? 'bg-gray-700/50' : 'bg-gray-50'}`}>
|
2025-11-07 20:19:08 +00:00
|
|
|
<div>
|
2025-11-27 19:18:59 +00:00
|
|
|
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Name</p>
|
|
|
|
|
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
2025-11-07 20:19:08 +00:00
|
|
|
{booking.user.first_name} {booking.user.last_name}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
2025-11-27 19:18:59 +00:00
|
|
|
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Email</p>
|
|
|
|
|
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
|
|
|
{booking.user.email}
|
|
|
|
|
</p>
|
2025-11-07 20:19:08 +00:00
|
|
|
</div>
|
2025-11-27 19:18:59 +00:00
|
|
|
{booking.user.phone && (
|
2025-12-01 17:35:28 +00:00
|
|
|
<div>
|
2025-11-27 19:18:59 +00:00
|
|
|
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Phone</p>
|
|
|
|
|
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
|
|
|
{booking.user.phone}
|
|
|
|
|
</p>
|
2025-11-07 20:19:08 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-11-27 19:18:59 +00:00
|
|
|
<div className={`rounded-lg p-4 ${isDark ? 'bg-blue-900/20 border border-blue-800/50' : 'bg-blue-50 border border-blue-200'}`}>
|
|
|
|
|
<p className={`text-sm ${isDark ? 'text-blue-300' : 'text-blue-800'}`}>
|
|
|
|
|
You will be contacted shortly to confirm your appointment.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="pt-4 flex justify-center">
|
2025-11-07 20:19:08 +00:00
|
|
|
<Button
|
2025-11-27 19:18:59 +00:00
|
|
|
onClick={() => router.back()}
|
2025-11-07 20:19:08 +00:00
|
|
|
className="bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
|
|
|
|
|
>
|
2025-11-27 19:18:59 +00:00
|
|
|
Go Back
|
2025-11-07 20:19:08 +00:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`rounded-xl sm:rounded-2xl shadow-lg p-4 sm:p-6 lg:p-8 border ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}`}>
|
2025-11-07 20:19:08 +00:00
|
|
|
{error && (
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`mb-6 p-4 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}>
|
|
|
|
|
<p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p>
|
2025-11-07 20:19:08 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
2025-11-07 19:47:18 +00:00
|
|
|
{/* Personal Information Section */}
|
|
|
|
|
<div className="space-y-4">
|
2025-11-13 11:10:00 +00:00
|
|
|
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
|
|
|
<User className={`w-5 h-5 ${isDark ? 'text-rose-400' : 'text-rose-600'}`} />
|
2025-11-07 19:47:18 +00:00
|
|
|
Personal Information
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="firstName"
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
>
|
|
|
|
|
First Name *
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="firstName"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="John"
|
|
|
|
|
value={formData.firstName}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
handleChange("firstName", e.target.value)
|
|
|
|
|
}
|
2025-11-27 19:18:59 +00:00
|
|
|
maxLength={100}
|
2025-11-07 19:47:18 +00:00
|
|
|
required
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="lastName"
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
>
|
|
|
|
|
Last Name *
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="lastName"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Doe"
|
|
|
|
|
value={formData.lastName}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
handleChange("lastName", e.target.value)
|
|
|
|
|
}
|
2025-11-27 19:18:59 +00:00
|
|
|
maxLength={100}
|
2025-11-07 19:47:18 +00:00
|
|
|
required
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="email"
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
>
|
2025-11-13 11:10:00 +00:00
|
|
|
<Mail className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
|
2025-11-07 19:47:18 +00:00
|
|
|
Email Address *
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="email"
|
|
|
|
|
type="email"
|
|
|
|
|
placeholder="john.doe@example.com"
|
|
|
|
|
value={formData.email}
|
|
|
|
|
onChange={(e) => handleChange("email", e.target.value)}
|
2025-11-27 19:18:59 +00:00
|
|
|
maxLength={100}
|
2025-11-07 19:47:18 +00:00
|
|
|
required
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="phone"
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
>
|
2025-11-13 11:10:00 +00:00
|
|
|
<Phone className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
|
2025-11-07 19:47:18 +00:00
|
|
|
Phone Number *
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="phone"
|
|
|
|
|
type="tel"
|
|
|
|
|
placeholder="+1 (555) 123-4567"
|
|
|
|
|
value={formData.phone}
|
|
|
|
|
onChange={(e) => handleChange("phone", e.target.value)}
|
2025-11-27 19:18:59 +00:00
|
|
|
maxLength={100}
|
2025-11-07 19:47:18 +00:00
|
|
|
required
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-500'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Appointment Details Section */}
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`space-y-4 pt-6 border-t ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
|
|
|
|
|
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
|
|
|
|
<Calendar className={`w-5 h-5 ${isDark ? 'text-rose-400' : 'text-rose-600'}`} />
|
2025-11-07 19:47:18 +00:00
|
|
|
Appointment Details
|
|
|
|
|
</h2>
|
|
|
|
|
|
2025-11-25 21:04:22 +00:00
|
|
|
<div className="space-y-4">
|
2025-11-07 19:47:18 +00:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
>
|
2025-11-13 11:10:00 +00:00
|
|
|
<Calendar className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
|
2025-11-27 19:18:59 +00:00
|
|
|
Available Days & Times *
|
2025-11-07 19:47:18 +00:00
|
|
|
</label>
|
2025-11-27 19:18:59 +00:00
|
|
|
{(isLoadingWeeklyAvailability || isLoadingAvailabilityOverview) ? (
|
2025-11-25 21:04:22 +00:00
|
|
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
|
|
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
2025-11-27 19:18:59 +00:00
|
|
|
Loading availability...
|
2025-11-25 21:04:22 +00:00
|
|
|
</div>
|
|
|
|
|
) : availableDaysOfWeek.length === 0 ? (
|
|
|
|
|
<p className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
|
|
|
|
|
No available days at the moment. Please check back later.
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
2025-11-27 19:18:59 +00:00
|
|
|
<p className={`text-xs ${isDark ? 'text-gray-400' : 'text-gray-500'} mb-3`}>
|
|
|
|
|
Select one or more day-time combinations that work for you
|
|
|
|
|
</p>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{availableDaysOfWeek.map((dayInfo, dayIndex) => {
|
|
|
|
|
// Ensure day is always a valid number (already validated in useMemo)
|
|
|
|
|
const currentDay = typeof dayInfo.day === 'number' && !isNaN(dayInfo.day)
|
|
|
|
|
? dayInfo.day
|
|
|
|
|
: dayIndex; // Fallback to index if invalid
|
|
|
|
|
|
|
|
|
|
// Skip if day is still invalid
|
|
|
|
|
if (isNaN(currentDay) || currentDay < 0 || currentDay > 6) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={`day-wrapper-${currentDay}-${dayIndex}`} className="space-y-2">
|
|
|
|
|
<h4 className={`text-sm font-semibold ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
|
|
|
|
|
{dayInfo.dayName || `Day ${currentDay}`}
|
|
|
|
|
</h4>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
{dayInfo.availableSlots.map((timeSlot: string, slotIndex: number) => {
|
|
|
|
|
if (!timeSlot) return null;
|
|
|
|
|
|
|
|
|
|
const timeSlotLabels: Record<string, string> = {
|
|
|
|
|
morning: "Morning",
|
|
|
|
|
afternoon: "Lunchtime",
|
|
|
|
|
evening: "Evening",
|
|
|
|
|
};
|
|
|
|
|
// Normalize time slot for consistent comparison
|
|
|
|
|
const normalizedTimeSlot = String(timeSlot).toLowerCase().trim();
|
|
|
|
|
// Create unique key combining day, time slot, and index to ensure uniqueness
|
|
|
|
|
const slotKey = `day-${currentDay}-slot-${normalizedTimeSlot}-${slotIndex}`;
|
|
|
|
|
// Check if THIS specific day-time combination is selected
|
|
|
|
|
const isSelected = isSlotSelected(currentDay, normalizedTimeSlot);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={slotKey}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
// Pass the specific day and time slot for this button
|
|
|
|
|
handleSlotToggle(currentDay, normalizedTimeSlot);
|
|
|
|
|
}}
|
|
|
|
|
aria-pressed={isSelected}
|
|
|
|
|
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border-2 transition-all focus:outline-none focus:ring-2 focus:ring-rose-500 ${
|
|
|
|
|
isSelected
|
2025-11-22 20:18:52 +00:00
|
|
|
? isDark
|
2025-11-27 19:18:59 +00:00
|
|
|
? 'bg-rose-600 border-rose-500 text-white hover:bg-rose-700'
|
|
|
|
|
: 'bg-rose-500 border-rose-500 text-white hover:bg-rose-600'
|
2025-11-22 20:18:52 +00:00
|
|
|
: isDark
|
2025-11-27 19:18:59 +00:00
|
|
|
? 'bg-gray-700 border-gray-600 text-gray-300 hover:border-rose-500 hover:bg-gray-650'
|
|
|
|
|
: 'bg-white border-gray-300 text-gray-700 hover:border-rose-500 hover:bg-rose-50'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<span className="text-sm font-medium">
|
|
|
|
|
{timeSlotLabels[normalizedTimeSlot] || timeSlot}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-11-25 21:04:22 +00:00
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-11-07 19:47:18 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Additional Message Section */}
|
2025-11-13 11:10:00 +00:00
|
|
|
<div className={`space-y-4 pt-6 border-t ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
|
2025-11-07 19:47:18 +00:00
|
|
|
<label
|
|
|
|
|
htmlFor="message"
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
>
|
2025-11-13 11:10:00 +00:00
|
|
|
<MessageSquare className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
|
2025-11-07 19:47:18 +00:00
|
|
|
Additional Message (Optional)
|
|
|
|
|
</label>
|
|
|
|
|
<textarea
|
|
|
|
|
id="message"
|
|
|
|
|
rows={4}
|
|
|
|
|
placeholder="Tell us about any specific concerns or preferences..."
|
|
|
|
|
value={formData.message}
|
|
|
|
|
onChange={(e) => handleChange("message", e.target.value)}
|
2025-11-27 19:18:59 +00:00
|
|
|
maxLength={100}
|
2025-11-13 11:10:00 +00:00
|
|
|
className={`w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-rose-500 focus-visible:border-rose-500 disabled:cursor-not-allowed disabled:opacity-50 ${isDark ? 'border-gray-600 bg-gray-700 text-white placeholder:text-gray-400 focus-visible:ring-rose-400 focus-visible:border-rose-400' : 'border-gray-300 bg-white text-gray-900 placeholder:text-gray-500'}`}
|
2025-11-07 19:47:18 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Submit Button */}
|
|
|
|
|
<div className="pt-6">
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
size="lg"
|
2025-11-27 19:18:59 +00:00
|
|
|
disabled={isCreating || availableDaysOfWeek.length === 0 || formData.selectedSlots.length === 0}
|
2025-11-07 20:19:08 +00:00
|
|
|
className="w-full bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all h-12 text-base font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
|
2025-11-07 19:47:18 +00:00
|
|
|
>
|
2025-11-23 21:43:13 +00:00
|
|
|
{isCreating ? (
|
2025-11-07 20:19:08 +00:00
|
|
|
<>
|
|
|
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
|
|
|
Submitting...
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
2025-11-22 21:29:35 +00:00
|
|
|
"Request Appointment"
|
2025-11-07 20:19:08 +00:00
|
|
|
)}
|
2025-11-07 19:47:18 +00:00
|
|
|
</Button>
|
2025-11-13 11:10:00 +00:00
|
|
|
<p className={`text-xs text-center mt-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
|
2025-11-07 19:47:18 +00:00
|
|
|
We'll review your request and get back to you within 24 hours
|
|
|
|
|
to confirm your appointment.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
2025-11-07 20:19:08 +00:00
|
|
|
</div>
|
2025-11-07 19:47:18 +00:00
|
|
|
|
2025-11-23 21:13:18 +00:00
|
|
|
{/* Logout Button - Only show when authenticated */}
|
|
|
|
|
{isAuthenticated && (
|
|
|
|
|
<div className="mt-6 flex justify-center">
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleLogout}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="bg-red-600 hover:bg-red-700 text-white border-red-600 hover:border-red-700"
|
|
|
|
|
>
|
|
|
|
|
<LogOut className="w-4 h-4 mr-2" />
|
|
|
|
|
Logout
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-07 20:19:08 +00:00
|
|
|
</>
|
|
|
|
|
)}
|
2025-11-07 19:47:18 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
2025-11-07 20:19:08 +00:00
|
|
|
|
|
|
|
|
{/* Login Dialog */}
|
|
|
|
|
<LoginDialog
|
|
|
|
|
open={showLoginDialog}
|
2025-11-25 20:15:37 +00:00
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
setShowLoginDialog(open);
|
|
|
|
|
if (!open) {
|
|
|
|
|
setLoginPrefillEmail(undefined);
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-11-07 20:19:08 +00:00
|
|
|
onLoginSuccess={handleLoginSuccess}
|
2025-11-25 20:15:37 +00:00
|
|
|
onSwitchToSignup={handleSwitchToSignup}
|
|
|
|
|
prefillEmail={loginPrefillEmail}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Signup Dialog */}
|
|
|
|
|
<SignupDialog
|
|
|
|
|
open={showSignupDialog}
|
|
|
|
|
onOpenChange={setShowSignupDialog}
|
|
|
|
|
onSignupSuccess={handleSignupSuccess}
|
|
|
|
|
onSwitchToLogin={handleSwitchToLogin}
|
2025-11-07 20:19:08 +00:00
|
|
|
/>
|
2025-11-07 19:47:18 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|