website/app/(pages)/book-now/page.tsx

681 lines
30 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useAppTheme } from "@/components/ThemeProvider";
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,
CheckCircle,
Loader2,
} from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { LoginDialog } from "@/components/LoginDialog";
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;
}
export default function BookNowPage() {
const router = useRouter();
const { theme } = useAppTheme();
const isDark = theme === "dark";
const [formData, setFormData] = useState({
firstName: "",
lastName: "",
email: "",
phone: "",
preferredDays: [] as string[],
preferredTimes: [] as string[],
message: "",
});
const [loading, setLoading] = useState(false);
const [booking, setBooking] = useState<Booking | null>(null);
const [error, setError] = useState<string | null>(null);
const [showLoginDialog, setShowLoginDialog] = useState(false);
// Handle submit button click
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Open login dialog instead of submitting directly
setShowLoginDialog(true);
};
const handleLoginSuccess = async () => {
// After successful login, proceed with booking submission
await submitBooking();
};
const submitBooking = async () => {
setLoading(true);
setError(null);
try {
if (formData.preferredDays.length === 0) {
setError("Please select at least one available day.");
setLoading(false);
return;
}
if (formData.preferredTimes.length === 0) {
setError("Please select at least one preferred time.");
setLoading(false);
return;
}
// For now, we'll use the first selected day and first selected time
// This can be adjusted based on your backend requirements
const firstDay = formData.preferredDays[0];
const firstTime = formData.preferredTimes[0];
const timeMap: { [key: string]: string } = {
morning: "09:00",
lunchtime: "12:00",
afternoon: "14:00",
};
const time24 = timeMap[firstTime] || "09:00";
// Get next occurrence of the first selected day
const today = new Date();
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const targetDayIndex = days.indexOf(firstDay);
let daysUntilTarget = (targetDayIndex - today.getDay() + 7) % 7;
if (daysUntilTarget === 0) daysUntilTarget = 7; // Next week if today
const targetDate = new Date(today);
targetDate.setDate(today.getDate() + daysUntilTarget);
const dateString = targetDate.toISOString().split("T")[0];
// Combine date and time into scheduled_at (ISO format)
const dateTimeString = `${dateString}T${time24}:00Z`;
// Prepare request payload
const payload = {
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
phone: formData.phone,
scheduled_at: dateTimeString,
duration: 60, // Default to 60 minutes
preferred_days: formData.preferredDays,
preferred_times: formData.preferredTimes,
notes: formData.message || "",
};
// Simulate API call - Replace with actual API endpoint
const response = await fetch("/api/bookings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).catch(() => {
// Fallback to mock data if API is not available
return null;
});
let bookingData: Booking;
if (response && response.ok) {
const data: BookingsResponse = await response.json();
bookingData = data.bookings[0];
} else {
// Mock response for development - matches the API structure provided
await new Promise((resolve) => setTimeout(resolve, 1000));
bookingData = {
ID: Math.floor(Math.random() * 1000),
CreatedAt: new Date().toISOString(),
UpdatedAt: new Date().toISOString(),
DeletedAt: null,
user_id: 1,
user: {
ID: 1,
CreatedAt: new Date().toISOString(),
UpdatedAt: new Date().toISOString(),
DeletedAt: null,
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
phone: formData.phone,
location: "",
date_of_birth: "0001-01-01T00:00:00Z",
is_admin: false,
bookings: null,
},
scheduled_at: dateTimeString,
duration: 60,
status: "scheduled",
jitsi_room_id: `booking-${Math.floor(Math.random() * 1000)}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
jitsi_room_url: `https://meet.jit.si/booking-${Math.floor(Math.random() * 1000)}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
payment_id: "",
payment_status: "pending",
amount: 52,
notes: formData.message || "Initial consultation session",
};
}
setBooking(bookingData);
setLoading(false);
// Redirect to home after 2 seconds
setTimeout(() => {
router.push("/");
}, 2000);
} catch (err) {
setError("Failed to submit booking. Please try again.");
setLoading(false);
console.error("Booking error:", err);
}
};
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleDayToggle = (day: string) => {
setFormData((prev) => {
const days = prev.preferredDays.includes(day)
? prev.preferredDays.filter((d) => d !== day)
: [...prev.preferredDays, day];
return { ...prev, preferredDays: days };
});
};
const handleTimeToggle = (time: string) => {
setFormData((prev) => {
const times = prev.preferredTimes.includes(time)
? prev.preferredTimes.filter((t) => t !== time)
: [...prev.preferredTimes, time];
return { ...prev, preferredTimes: times };
});
};
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,
});
};
return (
<div className={`min-h-screen ${isDark ? 'bg-gray-900' : 'bg-white'}`}>
{/* Main Content */}
<main className="min-h-screen flex">
{/* Left Side - Image (Fixed) */}
<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'}`}>
<div className="absolute inset-0">
<Image
src="/session.jpg"
alt="Therapy session with diverse clients"
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>
<span className={`font-bold text-lg drop-shadow-lg ${isDark ? 'text-rose-400' : 'text-rose-500'}`}>
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) */}
<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'}`}>
<div className="flex items-start justify-center min-h-full">
<div className="w-full max-w-2xl">
{/* Page Header */}
<div className="pt-4 sm:pt-6 lg:pt-8 px-4 sm:px-6 lg:px-12 pb-4 sm:pb-6">
<Button
variant="ghost"
onClick={() => router.back()}
className={`flex items-center gap-2 mb-3 sm:mb-4 ${isDark ? 'text-white hover:bg-gray-800' : 'text-black hover:bg-gray-100'}`}
>
<ArrowLeft className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="hidden sm:inline text-sm sm:text-base">Back</span>
</Button>
<div>
<h1 className={`text-xl sm:text-2xl font-semibold mb-1 ${isDark ? 'text-white' : 'text-gray-900'}`}>
Book Your Appointment
</h1>
<p className={`text-xs sm:text-sm ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
Fill out the form below and we'll get back to you to confirm your appointment
</p>
</div>
</div>
{/* Booking Form or Success Message */}
<div className="px-4 sm:px-6 lg:px-12 pb-6 sm:pb-8 lg:pb-12">
{booking ? (
<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'}`}>
<div className="text-center space-y-4">
<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'}`} />
</div>
<div>
<h2 className={`text-2xl font-semibold mb-2 ${isDark ? 'text-white' : 'text-gray-900'}`}>
Booking Confirmed!
</h2>
<p className={isDark ? 'text-gray-300' : 'text-gray-600'}>
Your appointment has been successfully booked.
</p>
</div>
<div className={`rounded-lg p-6 space-y-4 text-left ${isDark ? 'bg-gray-700/50' : 'bg-gray-50'}`}>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Booking ID</p>
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>#{booking.ID}</p>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Patient</p>
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>
{booking.user.first_name} {booking.user.last_name}
</p>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Scheduled Time</p>
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>{formatDateTime(booking.scheduled_at)}</p>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Duration</p>
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>{booking.duration} minutes</p>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Status</p>
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${isDark ? 'bg-blue-900/50 text-blue-200' : 'bg-blue-100 text-blue-800'}`}>
{booking.status}
</span>
</div>
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Amount</p>
<p className={`text-base font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>${booking.amount}</p>
</div>
{booking.notes && (
<div>
<p className={`text-sm font-medium mb-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Notes</p>
<p className={`text-base ${isDark ? 'text-white' : 'text-gray-900'}`}>{booking.notes}</p>
</div>
)}
</div>
<div className="pt-4 flex flex-col sm:flex-row gap-3 justify-center">
<Button
onClick={() => {
setBooking(null);
setFormData({
firstName: "",
lastName: "",
email: "",
phone: "",
preferredDays: [],
preferredTimes: [],
message: "",
});
}}
variant="outline"
>
Book Another Appointment
</Button>
<Button
onClick={() => router.push("/")}
className="bg-gradient-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white"
>
Return to Home
</Button>
</div>
</div>
</div>
) : (
<>
<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'}`}>
{error && (
<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>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Personal Information Section */}
<div className="space-y-4">
<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'}`} />
Personal Information
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<label
htmlFor="firstName"
className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
First Name *
</label>
<Input
id="firstName"
type="text"
placeholder="John"
value={formData.firstName}
onChange={(e) =>
handleChange("firstName", e.target.value)
}
required
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'}`}
/>
</div>
<div className="space-y-2">
<label
htmlFor="lastName"
className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
Last Name *
</label>
<Input
id="lastName"
type="text"
placeholder="Doe"
value={formData.lastName}
onChange={(e) =>
handleChange("lastName", e.target.value)
}
required
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'}`}
/>
</div>
</div>
<div className="space-y-2">
<label
htmlFor="email"
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<Mail className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
Email Address *
</label>
<Input
id="email"
type="email"
placeholder="john.doe@example.com"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
required
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'}`}
/>
</div>
<div className="space-y-2">
<label
htmlFor="phone"
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<Phone className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
Phone Number *
</label>
<Input
id="phone"
type="tel"
placeholder="+1 (555) 123-4567"
value={formData.phone}
onChange={(e) => handleChange("phone", e.target.value)}
required
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'}`}
/>
</div>
</div>
{/* Appointment Details Section */}
<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'}`} />
Appointment Details
</h2>
<div className="space-y-4">
<div className="space-y-2">
<label
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<Calendar className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
Available Days *
</label>
<div className="flex flex-wrap gap-3">
{['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'].map((day) => (
<label
key={day}
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border transition-all ${
formData.preferredDays.includes(day)
? isDark
? 'bg-rose-600 border-rose-500 text-white'
: 'bg-rose-500 border-rose-500 text-white'
: isDark
? 'bg-gray-700 border-gray-600 text-gray-300 hover:border-rose-500'
: 'bg-white border-gray-300 text-gray-700 hover:border-rose-500'
}`}
>
<input
type="checkbox"
checked={formData.preferredDays.includes(day)}
onChange={() => handleDayToggle(day)}
className="sr-only"
/>
<span className="text-sm font-medium">{day}</span>
</label>
))}
</div>
</div>
<div className="space-y-2">
<label
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<Clock className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
Preferred Time *
</label>
<div className="flex flex-wrap gap-3">
{[
{ value: 'morning', label: 'Morning' },
{ value: 'lunchtime', label: 'Lunchtime' },
{ value: 'afternoon', label: 'Afternoon' }
].map((time) => (
<label
key={time.value}
className={`flex items-center gap-2 cursor-pointer px-4 py-2 rounded-lg border transition-all ${
formData.preferredTimes.includes(time.value)
? isDark
? 'bg-rose-600 border-rose-500 text-white'
: 'bg-rose-500 border-rose-500 text-white'
: isDark
? 'bg-gray-700 border-gray-600 text-gray-300 hover:border-rose-500'
: 'bg-white border-gray-300 text-gray-700 hover:border-rose-500'
}`}
>
<input
type="checkbox"
checked={formData.preferredTimes.includes(time.value)}
onChange={() => handleTimeToggle(time.value)}
className="sr-only"
/>
<span className="text-sm font-medium">{time.label}</span>
</label>
))}
</div>
</div>
</div>
</div>
{/* Additional Message Section */}
<div className={`space-y-4 pt-6 border-t ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
<label
htmlFor="message"
className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}
>
<MessageSquare className={`w-4 h-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} />
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)}
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'}`}
/>
</div>
{/* Submit Button */}
<div className="pt-6">
<Button
type="submit"
size="lg"
disabled={loading}
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"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
"Request Appointment"
)}
</Button>
<p className={`text-xs text-center mt-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
We'll review your request and get back to you within 24 hours
to confirm your appointment.
</p>
</div>
</form>
</div>
{/* Contact Information */}
<div className="mt-6 text-center">
<p className={isDark ? 'text-gray-300' : 'text-gray-600'}>
Prefer to book by phone?{" "}
<a
href="tel:+17548162311"
className={`font-medium underline ${isDark ? 'text-rose-400 hover:text-rose-300' : 'text-rose-600 hover:text-rose-700'}`}
>
Call us at (754) 816-2311
</a>
</p>
</div>
</>
)}
</div>
</div>
</div>
</div>
</main>
{/* Login Dialog */}
<LoginDialog
open={showLoginDialog}
onOpenChange={setShowLoginDialog}
onLoginSuccess={handleLoginSuccess}
/>
</div>
);
}