Implement booking form functionality with loading state and error handling. Add user and booking interfaces, and display confirmation message upon successful submission. Enhance form submission process with time conversion and mock API response for development.

This commit is contained in:
iamkiddy 2025-11-07 20:19:08 +00:00
parent 77ecc28df1
commit 561d2ee2b5
5 changed files with 630 additions and 22 deletions

View File

@ -20,10 +20,53 @@ import {
ArrowLeft, ArrowLeft,
Heart, Heart,
CheckCircle2, CheckCircle2,
CheckCircle,
Loader2,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; 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() { export default function BookNowPage() {
const router = useRouter(); const router = useRouter();
@ -37,18 +80,128 @@ export default function BookNowPage() {
preferredTime: "", preferredTime: "",
message: "", 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);
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// Handle form submission // Open login dialog instead of submitting directly
console.log("Form submitted:", formData); setShowLoginDialog(true);
// You can add navigation or API call here };
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) => { const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value })); 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 ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-white">
{/* Main Content */} {/* Main Content */}
@ -144,9 +297,94 @@ export default function BookNowPage() {
</div> </div>
</div> </div>
{/* Booking Form */} {/* Booking Form or Success Message */}
<div className="px-6 sm:px-8 lg:px-12 pb-6 sm:pb-8 lg:pb-12"> <div className="px-6 sm:px-8 lg:px-12 pb-6 sm:pb-8 lg:pb-12">
{booking ? (
<div className="bg-white rounded-2xl shadow-lg p-6 sm:p-8 border border-gray-200"> <div className="bg-white rounded-2xl shadow-lg p-6 sm:p-8 border border-gray-200">
<div className="text-center space-y-4">
<div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<div>
<h2 className="text-2xl font-semibold text-gray-900 mb-2">
Booking Confirmed!
</h2>
<p className="text-gray-600">
Your appointment has been successfully booked.
</p>
</div>
<div className="bg-gray-50 rounded-lg p-6 space-y-4 text-left">
<div>
<p className="text-sm font-medium text-gray-500 mb-1">Booking ID</p>
<p className="text-base font-semibold text-gray-900">#{booking.ID}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500 mb-1">Patient</p>
<p className="text-base text-gray-900">
{booking.user.first_name} {booking.user.last_name}
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500 mb-1">Scheduled Time</p>
<p className="text-base text-gray-900">{formatDateTime(booking.scheduled_at)}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500 mb-1">Duration</p>
<p className="text-base text-gray-900">{booking.duration} minutes</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500 mb-1">Status</p>
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
{booking.status}
</span>
</div>
<div>
<p className="text-sm font-medium text-gray-500 mb-1">Amount</p>
<p className="text-base font-semibold text-gray-900">${booking.amount}</p>
</div>
{booking.notes && (
<div>
<p className="text-sm font-medium text-gray-500 mb-1">Notes</p>
<p className="text-base 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: "",
appointmentType: "",
preferredDate: "",
preferredTime: "",
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="bg-white rounded-2xl shadow-lg p-6 sm:p-8 border border-gray-200">
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Personal Information Section */} {/* Personal Information Section */}
<div className="space-y-4"> <div className="space-y-4">
@ -357,9 +595,17 @@ export default function BookNowPage() {
<Button <Button
type="submit" type="submit"
size="lg" size="lg"
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={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"
> >
Submit Booking Request {loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
"Submit Booking Request"
)}
</Button> </Button>
<p className="text-xs text-gray-500 text-center mt-4"> <p className="text-xs text-gray-500 text-center mt-4">
We'll review your request and get back to you within 24 hours We'll review your request and get back to you within 24 hours
@ -381,11 +627,20 @@ export default function BookNowPage() {
</a> </a>
</p> </p>
</div> </div>
</>
)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
{/* Login Dialog */}
<LoginDialog
open={showLoginDialog}
onOpenChange={setShowLoginDialog}
onLoginSuccess={handleLoginSuccess}
/>
</div> </div>
); );
} }

171
components/LoginDialog.tsx Normal file
View File

@ -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<string | null>(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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-white">
<DialogHeader>
<DialogTitle className="text-3xl font-bold bg-linear-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent">
Welcome back
</DialogTitle>
<DialogDescription className="text-gray-600">
Please log in to complete your booking
</DialogDescription>
</DialogHeader>
{/* Login Form */}
<form className="space-y-6 mt-4" onSubmit={handleLogin}>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-800">{error}</p>
</div>
)}
{/* Email Field */}
<div className="space-y-2">
<label htmlFor="login-email" className="text-sm font-medium text-black">
Email address
</label>
<Input
id="login-email"
type="email"
placeholder="Email address"
value={loginData.email}
onChange={(e) => setLoginData({ ...loginData, email: e.target.value })}
className="h-12 bg-white border-gray-300"
required
/>
</div>
{/* Password Field */}
<div className="space-y-2">
<label htmlFor="login-password" className="text-sm font-medium text-black">
Your password
</label>
<div className="relative">
<Input
id="login-password"
type={showPassword ? "text" : "password"}
placeholder="Your password"
value={loginData.password}
onChange={(e) => setLoginData({ ...loginData, password: e.target.value })}
className="h-12 bg-white border-gray-300 pr-12"
required
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 text-gray-500 hover:text-gray-700"
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</Button>
</div>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={loginLoading}
className="w-full h-12 text-base font-semibold bg-linear-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 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loginLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Logging in...
</>
) : (
"Log in"
)}
</Button>
{/* Remember Me & Forgot Password */}
<div className="flex items-center justify-between text-sm">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-rose-600 focus:ring-2 focus:ring-rose-500 cursor-pointer"
/>
<span className="text-black">Remember me</span>
</label>
<Link
href="/forgot-password"
className="text-blue-600 hover:text-blue-700 font-medium"
onClick={(e) => {
e.preventDefault();
onOpenChange(false);
}}
>
Forgot password?
</Link>
</div>
{/* Sign Up Prompt */}
<p className="text-sm text-gray-600 text-center">
New to Attune Heart Therapy?{" "}
<Link href="/signup" className="text-blue-600 underline font-medium">
Sign up
</Link>
</p>
</form>
</DialogContent>
</Dialog>
);
}

143
components/ui/dialog.tsx Normal file
View File

@ -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<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -12,6 +12,7 @@
"node": ">=20.9.0" "node": ">=20.9.0"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",

View File

@ -8,6 +8,9 @@ importers:
.: .:
dependencies: 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': '@radix-ui/react-dropdown-menu':
specifier: ^2.1.16 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) 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': '@types/react':
optional: true 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': '@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies: peerDependencies:
@ -2801,6 +2817,28 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.2 '@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)': '@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies: dependencies:
react: 19.2.0 react: 19.2.0