diff --git a/app/book-now/page.tsx b/app/book-now/page.tsx index 26adfb5..20d7a11 100644 --- a/app/book-now/page.tsx +++ b/app/book-now/page.tsx @@ -20,10 +20,53 @@ import { 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(); @@ -37,18 +80,128 @@ export default function BookNowPage() { preferredTime: "", message: "", }); + const [loading, setLoading] = useState(false); + const [booking, setBooking] = useState(null); + const [error, setError] = useState(null); + const [showLoginDialog, setShowLoginDialog] = useState(false); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Handle form submission - console.log("Form submitted:", formData); - // You can add navigation or API call here + // 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 { + // Convert time to 24-hour format for ISO string + const time24 = formData.preferredTime.includes("PM") + ? formData.preferredTime.replace("PM", "").trim().split(":").map((v, i) => + i === 0 ? (parseInt(v) === 12 ? 12 : parseInt(v) + 12) : v + ).join(":") + : formData.preferredTime.replace("AM", "").trim().split(":").map((v, i) => + i === 0 ? (parseInt(v) === 12 ? "00" : v.padStart(2, "0")) : v + ).join(":"); + + // Combine date and time into scheduled_at (ISO format) + const dateTimeString = `${formData.preferredDate}T${time24}:00Z`; + + // Prepare request payload + const payload = { + first_name: formData.firstName, + last_name: formData.lastName, + email: formData.email, + phone: formData.phone, + appointment_type: formData.appointmentType, + scheduled_at: dateTimeString, + duration: 60, // Default to 60 minutes + 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); + } 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 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 (
{/* Main Content */} @@ -144,10 +297,95 @@ export default function BookNowPage() {
- {/* Booking Form */} + {/* Booking Form or Success Message */}
-
-
+ {booking ? ( +
+
+
+ +
+
+

+ Booking Confirmed! +

+

+ Your appointment has been successfully booked. +

+
+
+
+

Booking ID

+

#{booking.ID}

+
+
+

Patient

+

+ {booking.user.first_name} {booking.user.last_name} +

+
+
+

Scheduled Time

+

{formatDateTime(booking.scheduled_at)}

+
+
+

Duration

+

{booking.duration} minutes

+
+
+

Status

+ + {booking.status} + +
+
+

Amount

+

${booking.amount}

+
+ {booking.notes && ( +
+

Notes

+

{booking.notes}

+
+ )} +
+
+ + +
+
+
+ ) : ( + <> +
+ {error && ( +
+

{error}

+
+ )} + {/* Personal Information Section */}

@@ -357,9 +595,17 @@ export default function BookNowPage() {

We'll review your request and get back to you within 24 hours @@ -367,25 +613,34 @@ export default function BookNowPage() {

-
+
- {/* Contact Information */} -
-

- Prefer to book by phone?{" "} - - Call us at (954) 807-3027 - -

-
+ {/* Contact Information */} +
+

+ Prefer to book by phone?{" "} + + Call us at (954) 807-3027 + +

+
+ + )}
+ + {/* Login Dialog */} + ); } diff --git a/components/LoginDialog.tsx b/components/LoginDialog.tsx new file mode 100644 index 0000000..69094ea --- /dev/null +++ b/components/LoginDialog.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Eye, EyeOff, Loader2 } from "lucide-react"; +import Link from "next/link"; + +interface LoginDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onLoginSuccess: () => void; +} + +export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogProps) { + const [loginData, setLoginData] = useState({ + email: "", + password: "", + }); + const [showPassword, setShowPassword] = useState(false); + const [rememberMe, setRememberMe] = useState(false); + const [loginLoading, setLoginLoading] = useState(false); + const [error, setError] = useState(null); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoginLoading(true); + setError(null); + + try { + // Simulate login API call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // After successful login, close dialog and call success callback + setShowPassword(false); + setLoginLoading(false); + onOpenChange(false); + onLoginSuccess(); + } catch (err) { + setError("Login failed. Please try again."); + setLoginLoading(false); + } + }; + + return ( + + + + + Welcome back + + + Please log in to complete your booking + + + + {/* Login Form */} +
+ {error && ( +
+

{error}

+
+ )} + + {/* Email Field */} +
+ + setLoginData({ ...loginData, email: e.target.value })} + className="h-12 bg-white border-gray-300" + required + /> +
+ + {/* Password Field */} +
+ +
+ setLoginData({ ...loginData, password: e.target.value })} + className="h-12 bg-white border-gray-300 pr-12" + required + /> + +
+
+ + {/* Submit Button */} + + + {/* Remember Me & Forgot Password */} +
+ + { + e.preventDefault(); + onOpenChange(false); + }} + > + Forgot password? + +
+ + {/* Sign Up Prompt */} +

+ New to Attune Heart Therapy?{" "} + + Sign up + +

+
+
+
+ ); +} + diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..d9ccec9 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/package.json b/package.json index 3339ed1..5a9db76 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "node": ">=20.9.0" }, "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80c05ee..c053baa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-dropdown-menu': specifier: ^2.1.16 version: 2.1.16(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -504,6 +507,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-direction@1.1.1': resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} peerDependencies: @@ -2801,6 +2817,28 @@ snapshots: optionalDependencies: '@types/react': 19.2.2 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + aria-hidden: 1.2.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.0)': dependencies: react: 19.2.0