From cef17ea895ca2de59cfd0ce715c205fdbbb8a5d8 Mon Sep 17 00:00:00 2001 From: iamkiddy Date: Wed, 26 Nov 2025 11:42:31 +0000 Subject: [PATCH] Add React DatePicker and styles, update package dependencies, and enhance booking validation --- app/(admin)/_components/header.tsx | 2 +- app/(admin)/admin/booking/[id]/page.tsx | 24 +- app/(admin)/admin/booking/page.tsx | 21 +- app/(pages)/book-now/page.tsx | 55 +++-- app/(user)/user/dashboard/page.tsx | 8 +- app/(user)/user/settings/page.tsx | 287 ++++++++++++++++-------- app/globals.css | 93 ++++++++ components/DatePicker.tsx | 125 +++++------ components/ForgotPasswordDialog.tsx | 1 + components/ui/popover.tsx | 2 +- lib/actions/appointments.ts | 173 +++++++++++++- lib/actions/auth.ts | 2 +- package.json | 2 + pnpm-lock.yaml | 50 +++++ 14 files changed, 630 insertions(+), 215 deletions(-) diff --git a/app/(admin)/_components/header.tsx b/app/(admin)/_components/header.tsx index 0475a10..0073cd9 100644 --- a/app/(admin)/_components/header.tsx +++ b/app/(admin)/_components/header.tsx @@ -89,7 +89,7 @@ export function Header() { +

Loading appointment details...

@@ -213,7 +213,7 @@ export default function AppointmentDetailPage() { if (!appointment) { return ( -
+

Appointment not found

@@ -283,15 +283,15 @@ export default function UserDashboard() {

- Request Appointment + No Upcoming Appointments

- No upcoming appointments. Book an appointment to get started. + You don't have any scheduled appointments yet. Book an appointment to get started.

diff --git a/app/(user)/user/settings/page.tsx b/app/(user)/user/settings/page.tsx index 6d0906d..976e3af 100644 --- a/app/(user)/user/settings/page.tsx +++ b/app/(user)/user/settings/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; @@ -13,19 +13,26 @@ import { Lock, Eye, EyeOff, + Loader2, } from "lucide-react"; import Link from "next/link"; import { Navbar } from "@/components/Navbar"; import { useAppTheme } from "@/components/ThemeProvider"; +import { getProfile, updateProfile } from "@/lib/actions/auth"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; export default function SettingsPage() { const { theme } = useAppTheme(); const isDark = theme === "dark"; + const { user } = useAuth(); const [loading, setLoading] = useState(false); + const [fetching, setFetching] = useState(true); const [formData, setFormData] = useState({ - fullName: "John Doe", - email: "john.doe@example.com", - phone: "+1 (555) 123-4567", + firstName: "", + lastName: "", + email: "", + phone: "", }); const [passwordData, setPasswordData] = useState({ currentPassword: "", @@ -38,6 +45,30 @@ export default function SettingsPage() { confirm: false, }); + // Fetch profile data on mount + useEffect(() => { + const fetchProfile = async () => { + setFetching(true); + try { + const profile = await getProfile(); + setFormData({ + firstName: profile.first_name || "", + lastName: profile.last_name || "", + email: profile.email || "", + phone: profile.phone_number || "", + }); + } catch (error) { + console.error("Failed to fetch profile:", error); + const errorMessage = error instanceof Error ? error.message : "Failed to load profile"; + toast.error(errorMessage); + } finally { + setFetching(false); + } + }; + + fetchProfile(); + }, []); + const handleInputChange = (field: string, value: string) => { setFormData((prev) => ({ ...prev, @@ -60,35 +91,61 @@ export default function SettingsPage() { }; const handleSave = async () => { + if (!formData.firstName || !formData.lastName) { + toast.error("First name and last name are required"); + return; + } + setLoading(true); - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 1000)); - setLoading(false); - // In a real app, you would show a success message here + try { + await updateProfile({ + first_name: formData.firstName, + last_name: formData.lastName, + phone_number: formData.phone || undefined, + }); + toast.success("Profile updated successfully"); + } catch (error) { + console.error("Failed to update profile:", error); + const errorMessage = error instanceof Error ? error.message : "Failed to update profile"; + toast.error(errorMessage); + } finally { + setLoading(false); + } }; const handlePasswordSave = async () => { + if (!passwordData.currentPassword) { + toast.error("Please enter your current password"); + return; + } if (passwordData.newPassword !== passwordData.confirmPassword) { - // In a real app, you would show an error message here - alert("New passwords do not match"); + toast.error("New passwords do not match"); return; } if (passwordData.newPassword.length < 8) { - // In a real app, you would show an error message here - alert("Password must be at least 8 characters long"); + toast.error("Password must be at least 8 characters long"); return; } + setLoading(true); - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 1000)); - setLoading(false); - // Reset password fields - setPasswordData({ - currentPassword: "", - newPassword: "", - confirmPassword: "", - }); - // In a real app, you would show a success message here + try { + // Note: The API might not have a change password endpoint for authenticated users + // This would need to be implemented on the backend + // For now, we'll show a message that this feature is coming soon + toast.error("Password change feature is not yet available. Please use the forgot password flow."); + // Reset password fields + setPasswordData({ + currentPassword: "", + newPassword: "", + confirmPassword: "", + }); + } catch (error) { + console.error("Failed to update password:", error); + const errorMessage = error instanceof Error ? error.message : "Failed to update password"; + toast.error(errorMessage); + } finally { + setLoading(false); + } }; return ( @@ -114,83 +171,109 @@ export default function SettingsPage() {

-
-
-
- {/* Profile Information */} - - -
- - Profile Information -
- - Update your personal information and contact details - -
- -
- -
- - handleInputChange("fullName", e.target.value)} - className={`pl-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} - placeholder="Enter your full name" - /> + {fetching ? ( +
+ +
+ ) : ( +
+
+ {/* Profile Information */} + + +
+ + Profile Information +
+ + Update your personal information and contact details + +
+ +
+ +
+
+ handleInputChange("firstName", e.target.value)} + className={`${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} + placeholder="First name" + /> +
+
+ handleInputChange("lastName", e.target.value)} + className={`${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} + placeholder="Last name" + /> +
+
-
-
- -
- - handleInputChange("email", e.target.value)} - className={`pl-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} - placeholder="Enter your email" - /> +
+ +
+ + +
+

+ Email address cannot be changed +

-
-
- -
- - handleInputChange("phone", e.target.value)} - className={`pl-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} - placeholder="Enter your phone number" - /> +
+ +
+ + handleInputChange("phone", e.target.value)} + className={`pl-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} + placeholder="Enter your phone number" + /> +
-
- - + +
+ +
+ + {/* Change Password */} @@ -214,7 +297,7 @@ export default function SettingsPage() { type={showPasswords.current ? "text" : "password"} value={passwordData.currentPassword} onChange={(e) => handlePasswordChange("currentPassword", e.target.value)} - className={`pl-10 pr-10 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} + className={`pl-10 pr-10 h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`} placeholder="Enter your current password" />
+
-
+ )}
); diff --git a/app/globals.css b/app/globals.css index 82baded..2cd982b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -117,3 +117,96 @@ scrollbar-color: rgb(244 63 94) rgb(255 228 230); } } + +/* React DatePicker Styles */ +.react-datepicker { + font-family: inherit; + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); +} + +.react-datepicker__header { + background-color: hsl(var(--background)); + border-bottom: 1px solid hsl(var(--border)); + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; + padding-top: 0.75rem; +} + +.react-datepicker__current-month { + color: hsl(var(--foreground)); + font-weight: 600; + font-size: 0.875rem; +} + +.react-datepicker__day-name { + color: hsl(var(--muted-foreground)); + font-weight: 500; + font-size: 0.75rem; +} + +.react-datepicker__day { + color: hsl(var(--foreground)); + border-radius: 0.375rem; +} + +.react-datepicker__day:hover { + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); + border-radius: 0.375rem; +} + +.react-datepicker__day--selected, +.react-datepicker__day--keyboard-selected { + background-color: rgb(225 29 72); + color: white; + border-radius: 0.375rem; +} + +.react-datepicker__day--selected:hover, +.react-datepicker__day--keyboard-selected:hover { + background-color: rgb(190 24 93); +} + +.react-datepicker__day--disabled { + color: hsl(var(--muted-foreground)); + opacity: 0.5; + cursor: not-allowed; +} + +.react-datepicker__navigation { + top: 0.75rem; +} + +.react-datepicker__navigation-icon::before { + border-color: hsl(var(--foreground)); +} + +.react-datepicker__navigation:hover *::before { + border-color: rgb(225 29 72); +} + +html.dark .react-datepicker { + background-color: #1f2937; + border-color: #374151; +} + +html.dark .react-datepicker__header { + background-color: #1f2937; + border-color: #374151; +} + +html.dark .react-datepicker__current-month { + color: #f9fafb; +} + +html.dark .react-datepicker__day { + color: #f9fafb; +} + +html.dark .react-datepicker__day:hover { + background-color: #374151; + color: #f9fafb; +} diff --git a/components/DatePicker.tsx b/components/DatePicker.tsx index 233e1cf..b82c587 100644 --- a/components/DatePicker.tsx +++ b/components/DatePicker.tsx @@ -1,22 +1,12 @@ 'use client'; import * as React from 'react'; - import { Calendar as CalendarIcon } from 'lucide-react'; - import { format } from 'date-fns'; - import { cn } from '@/lib/utils'; - import { Button } from '@/components/ui/button'; - -import { Calendar } from '@/components/ui/calendar'; - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; +import DatePickerLib from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; interface DatePickerProps { date: Date | undefined; @@ -25,7 +15,25 @@ interface DatePickerProps { } export function DatePicker({ date, setDate, label }: DatePickerProps) { - const [open, setOpen] = React.useState(false); + const [isOpen, setIsOpen] = React.useState(false); + const wrapperRef = React.useRef(null); + + // Close calendar when clicking outside + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); return (
@@ -34,65 +42,38 @@ export function DatePicker({ date, setDate, label }: DatePickerProps) { {label} )} - - - - - - { - setDate(selectedDate); - setOpen(false); - }} - initialFocus - classNames={{ - months: "space-y-4", - month: "space-y-4", - caption: "flex justify-center pt-3 pb-5 relative items-center border-b border-gray-200 dark:border-gray-700 mb-4", - caption_label: "text-lg font-bold text-gray-800 dark:text-gray-100", - nav: "flex items-center justify-between absolute inset-0", - nav_button: cn( - "h-9 w-9 rounded-full bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 hover:bg-rose-50 dark:hover:bg-gray-600 hover:border-rose-300 dark:hover:border-rose-500 p-0 transition-all shadow-sm" - ), - nav_button_previous: "absolute left-0", - nav_button_next: "absolute right-0", - table: "w-full border-collapse space-y-3", - head_row: "flex mb-3", - head_cell: "text-gray-600 dark:text-gray-400 rounded-md w-11 font-semibold text-xs", - row: "flex w-full mt-2", - cell: cn( - "relative p-0 text-center text-sm focus-within:relative focus-within:z-20", - "[&>button]:h-11 [&>button]:w-11 [&>button]:p-0 [&>button]:font-semibold [&>button]:cursor-pointer [&>button]:rounded-full [&>button]:transition-all" - ), - day: cn( - "h-11 w-11 p-0 font-semibold aria-selected:opacity-100 hover:bg-rose-500 hover:text-white rounded-full transition-all cursor-pointer", - "hover:scale-110 active:scale-95 hover:shadow-md" - ), - day_selected: - "bg-rose-600 text-white hover:bg-rose-700 hover:text-white focus:bg-rose-600 focus:text-white font-bold shadow-xl scale-110 ring-4 ring-rose-200 dark:ring-rose-800", - day_today: "bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 font-bold border-2 border-blue-300 dark:border-blue-600", - day_outside: "text-gray-300 dark:text-gray-600 opacity-50", - day_disabled: "text-gray-200 dark:text-gray-700 opacity-30 cursor-not-allowed", - day_range_middle: - "aria-selected:bg-rose-100 dark:aria-selected:bg-rose-900/30 aria-selected:text-rose-700 dark:aria-selected:text-rose-300", - day_hidden: "invisible", - }} - /> - - +
+ + {isOpen && ( +
+ { + setDate(selectedDate || undefined); + if (selectedDate) { + setIsOpen(false); + } + }} + minDate={new Date()} + inline + calendarClassName="!border-0" + wrapperClassName="w-full" + /> +
+ )} +
); } - diff --git a/components/ForgotPasswordDialog.tsx b/components/ForgotPasswordDialog.tsx index fee45f4..75047a8 100644 --- a/components/ForgotPasswordDialog.tsx +++ b/components/ForgotPasswordDialog.tsx @@ -461,3 +461,4 @@ export function ForgotPasswordDialog({ open, onOpenChange, onSuccess }: ForgotPa + diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx index 5cd4fbe..5e814ab 100644 --- a/components/ui/popover.tsx +++ b/components/ui/popover.tsx @@ -30,7 +30,7 @@ function PopoverContent({ align={align} sideOffset={sideOffset} className={cn( - "bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-gray-200 p-4 shadow-md outline-hidden", + "bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[100] w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-gray-200 p-4 shadow-md outline-hidden", className )} {...props} diff --git a/lib/actions/appointments.ts b/lib/actions/appointments.ts index a8ad586..b094928 100644 --- a/lib/actions/appointments.ts +++ b/lib/actions/appointments.ts @@ -51,16 +51,124 @@ export async function createAppointment( throw new Error("Authentication required. Please log in to book an appointment."); } + // Validate required fields + if (!input.first_name || !input.last_name || !input.email) { + throw new Error("First name, last name, and email are required"); + } + if (!input.preferred_dates || input.preferred_dates.length === 0) { + throw new Error("At least one preferred date is required"); + } + if (!input.preferred_time_slots || input.preferred_time_slots.length === 0) { + throw new Error("At least one preferred time slot is required"); + } + + // Validate date format (YYYY-MM-DD) + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + for (const date of input.preferred_dates) { + if (!dateRegex.test(date)) { + throw new Error(`Invalid date format: ${date}. Expected YYYY-MM-DD format.`); + } + } + + // Validate time slots + const validTimeSlots = ["morning", "afternoon", "evening"]; + for (const slot of input.preferred_time_slots) { + if (!validTimeSlots.includes(slot)) { + throw new Error(`Invalid time slot: ${slot}. Must be one of: ${validTimeSlots.join(", ")}`); + } + } + + // Prepare the payload exactly as the API expects + // Only include fields that the API accepts - no jitsi_room_id or other fields + const payload: { + first_name: string; + last_name: string; + email: string; + preferred_dates: string[]; + preferred_time_slots: string[]; + phone?: string; + reason?: string; + } = { + first_name: input.first_name.trim(), + last_name: input.last_name.trim(), + email: input.email.trim().toLowerCase(), + preferred_dates: input.preferred_dates, + preferred_time_slots: input.preferred_time_slots, + }; + + // Only add optional fields if they have values + if (input.phone && input.phone.trim()) { + payload.phone = input.phone.trim(); + } + if (input.reason && input.reason.trim()) { + payload.reason = input.reason.trim(); + } + + // Log the payload for debugging + console.log("Creating appointment with payload:", JSON.stringify(payload, null, 2)); + console.log("API endpoint:", API_ENDPOINTS.meetings.createAppointment); + const response = await fetch(API_ENDPOINTS.meetings.createAppointment, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, }, - body: JSON.stringify(input), + body: JSON.stringify(payload), }); - const data: AppointmentResponse = await response.json(); + // Read response text first (can only be read once) + const responseText = await response.text(); + + // Check content type before parsing + const contentType = response.headers.get("content-type"); + let data: any; + + if (contentType && contentType.includes("application/json")) { + try { + if (!responseText) { + throw new Error(`Server returned empty response (${response.status})`); + } + data = JSON.parse(responseText); + } catch (e) { + // If JSON parsing fails, log the actual response + console.error("Failed to parse JSON response:", { + status: response.status, + statusText: response.statusText, + contentType, + url: API_ENDPOINTS.meetings.createAppointment, + preview: responseText.substring(0, 500) + }); + throw new Error(`Server error (${response.status}): ${response.statusText || 'Invalid response format'}`); + } + } else { + // Response is not JSON (likely HTML error page) + // Try to extract error message from HTML if possible + let errorMessage = `Server error (${response.status}): ${response.statusText || 'Internal Server Error'}`; + + // Try to find error details in HTML + const errorMatch = responseText.match(/]*>(.*?)<\/pre>/is) || + responseText.match(/]*>(.*?)<\/h1>/is) || + responseText.match(/]*>(.*?)<\/title>/is); + + if (errorMatch && errorMatch[1]) { + const htmlError = errorMatch[1].replace(/<[^>]*>/g, '').trim(); + if (htmlError) { + errorMessage += `. ${htmlError}`; + } + } + + console.error("Non-JSON response received:", { + status: response.status, + statusText: response.statusText, + contentType, + url: API_ENDPOINTS.meetings.createAppointment, + payload: input, + preview: responseText.substring(0, 1000) + }); + + throw new Error(errorMessage); + } if (!response.ok) { const errorMessage = extractErrorMessage(data as unknown as ApiError); @@ -235,10 +343,67 @@ export async function scheduleAppointment( body: JSON.stringify(input), }); - const data: AppointmentResponse = await response.json(); + let data: any; + const contentType = response.headers.get("content-type"); + + if (contentType && contentType.includes("application/json")) { + try { + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + } catch (e) { + data = {}; + } + } else { + const text = await response.text(); + data = text || {}; + } if (!response.ok) { - const errorMessage = extractErrorMessage(data as unknown as ApiError); + // Try to extract detailed error information + let errorMessage = `Failed to schedule appointment (${response.status})`; + + if (data && Object.keys(data).length > 0) { + // Check for common error formats + if (data.detail) { + errorMessage = Array.isArray(data.detail) ? data.detail.join(", ") : String(data.detail); + } else if (data.message) { + errorMessage = Array.isArray(data.message) ? data.message.join(", ") : String(data.message); + } else if (data.error) { + errorMessage = Array.isArray(data.error) ? data.error.join(", ") : String(data.error); + } else if (typeof data === "string") { + errorMessage = data; + } else { + // Check for field-specific errors + const fieldErrors: string[] = []; + Object.keys(data).forEach((key) => { + if (key !== "detail" && key !== "message" && key !== "error") { + const fieldError = data[key]; + if (Array.isArray(fieldError)) { + fieldErrors.push(`${key}: ${fieldError.join(", ")}`); + } else if (typeof fieldError === "string") { + fieldErrors.push(`${key}: ${fieldError}`); + } + } + }); + if (fieldErrors.length > 0) { + errorMessage = fieldErrors.join(". "); + } else { + // If we have data but can't parse it, show the status + errorMessage = `Server error: ${response.status} ${response.statusText}`; + } + } + } else { + // No data in response + errorMessage = `Server error: ${response.status} ${response.statusText || 'Unknown error'}`; + } + + console.error("Schedule appointment error:", { + status: response.status, + statusText: response.statusText, + data, + errorMessage, + }); + throw new Error(errorMessage); } diff --git a/lib/actions/auth.ts b/lib/actions/auth.ts index 229e722..ddd67c3 100644 --- a/lib/actions/auth.ts +++ b/lib/actions/auth.ts @@ -413,7 +413,7 @@ export async function updateProfile(input: UpdateProfileInput): Promise { } const response = await fetch(API_ENDPOINTS.auth.updateProfile, { - method: "PATCH", + method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${tokens.access}`, diff --git a/package.json b/package.json index 3a51670..86d2034 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@tanstack/react-query": "^5.90.10", + "@types/react-datepicker": "^7.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -27,6 +28,7 @@ "next": "16.0.1", "next-themes": "^0.4.6", "react": "19.2.0", + "react-datepicker": "^8.9.0", "react-day-picker": "^9.11.1", "react-dom": "19.2.0", "react-markdown": "^10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9562850..f09397d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@tanstack/react-query': specifier: ^5.90.10 version: 5.90.10(react@19.2.0) + '@types/react-datepicker': + specifier: ^7.0.0 + version: 7.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -53,6 +56,9 @@ importers: react: specifier: 19.2.0 version: 19.2.0 + react-datepicker: + specifier: ^8.9.0 + version: 8.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-day-picker: specifier: ^9.11.1 version: 9.11.1(react@19.2.0) @@ -241,6 +247,12 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react@0.27.16': + resolution: {integrity: sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} @@ -946,6 +958,10 @@ packages: '@types/node@20.19.24': resolution: {integrity: sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==} + '@types/react-datepicker@7.0.0': + resolution: {integrity: sha512-4tWwOUq589tozyQPBVEqGNng5DaZkomx5IVNuur868yYdgjH6RaL373/HKiVt1IDoNNXYiTGspm1F7kjrarM8Q==} + deprecated: This is a stub types definition. react-datepicker provides its own type definitions, so you do not need this installed. + '@types/react-dom@19.2.2': resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} peerDependencies: @@ -2334,6 +2350,12 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-datepicker@8.9.0: + resolution: {integrity: sha512-yoRsGxjqVRjk8iUBssrW9jcinTeyP9mAfTpuzdKvlESOUjdrY0sfDTzIZWJAn38jvNcxW1dnDmW1CinjiFdxYQ==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-day-picker@9.11.1: resolution: {integrity: sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==} engines: {node: '>=18'} @@ -2576,6 +2598,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tabbable@6.3.0: + resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} + tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} @@ -2937,6 +2962,14 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@floating-ui/react@0.27.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@floating-ui/utils': 0.2.10 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + tabbable: 6.3.0 + '@floating-ui/utils@0.2.10': {} '@humanfs/core@0.19.1': {} @@ -3562,6 +3595,13 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/react-datepicker@7.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + react-datepicker: 8.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + transitivePeerDependencies: + - react + - react-dom + '@types/react-dom@19.2.2(@types/react@19.2.2)': dependencies: '@types/react': 19.2.2 @@ -5310,6 +5350,14 @@ snapshots: queue-microtask@1.2.3: {} + react-datepicker@8.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@floating-ui/react': 0.27.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + clsx: 2.1.1 + date-fns: 4.1.0 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-day-picker@9.11.1(react@19.2.0): dependencies: '@date-fns/tz': 1.4.1 @@ -5654,6 +5702,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tabbable@6.3.0: {} + tailwind-merge@3.3.1: {} tailwindcss@4.1.16: {}