@@ -15,6 +15,14 @@ import { useCreateBooking } from "@/features/venues/hooks";
1515import { useLocation , useNavigate } from "react-router-dom" ;
1616import { routes } from "@/router/routes" ;
1717import { Spinner } from "@/components/ui/spinner" ;
18+ import {
19+ Dialog ,
20+ DialogContent ,
21+ DialogDescription ,
22+ DialogFooter ,
23+ DialogHeader ,
24+ DialogTitle ,
25+ } from "@/components/ui/dialog" ;
1826
1927type Props = {
2028 venueId : string ;
@@ -31,6 +39,7 @@ export function BookingWidget({
3139} : Props ) {
3240 const [ range , setRange ] = useState < DateRange | undefined > ( ) ;
3341 const [ guests , setGuests ] = useState ( 1 ) ;
42+ const [ confirmOpen , setConfirmOpen ] = useState ( false ) ;
3443 const { token } = useAuth ( ) ;
3544 const navigate = useNavigate ( ) ;
3645 const location = useLocation ( ) ;
@@ -44,7 +53,7 @@ export function BookingWidget({
4453
4554 const { mutateAsync, isPending } = useCreateBooking ( venueId ) ;
4655
47- async function submit ( ) {
56+ function handlePrimaryAction ( ) {
4857 if ( ! token ) {
4958 navigate ( routes . auth . login , { state : { from : location } } ) ;
5059 return ;
@@ -57,15 +66,21 @@ export function BookingWidget({
5766 toast . error ( `Guests must be between 1 and ${ maxGuests } .` ) ;
5867 return ;
5968 }
69+ setConfirmOpen ( true ) ;
70+ }
71+
72+ async function confirmBooking ( ) {
73+ if ( ! range ?. from || ! range ?. to ) return ;
6074 try {
6175 await mutateAsync ( {
6276 dateFrom : toISODate ( range . from ) ,
63- dateTo : toISODate ( range . to ) , // checkout day
77+ dateTo : toISODate ( range . to ) ,
6478 guests,
6579 } ) ;
6680 toast . success ( "Booking confirmed!" ) ;
6781 setRange ( undefined ) ;
6882 setGuests ( 1 ) ;
83+ setConfirmOpen ( false ) ;
6984 } catch ( e : unknown ) {
7085 toast . error ( e instanceof Error ? e . message : "Booking failed" ) ;
7186 }
@@ -155,13 +170,70 @@ export function BookingWidget({
155170
156171 < Button
157172 className = "w-full"
158- onClick = { submit }
173+ onClick = { handlePrimaryAction }
159174 disabled = { isPending || ( ! ! token && ( ! range ?. from || ! range ?. to ) ) }
160175 aria-busy = { isPending }
161176 >
162177 { token && isPending && < Spinner className = "mr-2" aria-hidden = "true" /> }
163178 { token ? ( isPending ? "Booking…" : "Book now" ) : "Sign in to book" }
164179 </ Button >
180+
181+ < Dialog
182+ open = { confirmOpen }
183+ onOpenChange = { ( open ) => {
184+ if ( ! isPending ) setConfirmOpen ( open ) ;
185+ } }
186+ >
187+ < DialogContent >
188+ < DialogHeader >
189+ < DialogTitle > Confirm booking</ DialogTitle >
190+ < DialogDescription >
191+ Review your stay details before confirming.
192+ </ DialogDescription >
193+ </ DialogHeader >
194+ < div className = "space-y-2 text-sm" >
195+ < div className = "flex justify-between" >
196+ < span > Dates</ span >
197+ < span className = "font-medium" >
198+ { range ?. from && range ?. to
199+ ? formatDateRange ( range . from , range . to )
200+ : "Select dates" }
201+ </ span >
202+ </ div >
203+ < div className = "flex justify-between" >
204+ < span > Guests</ span >
205+ < span className = "font-medium" > { guests } </ span >
206+ </ div >
207+ < div className = "flex justify-between" >
208+ < span >
209+ { nights } { nights === 1 ? "night" : "nights" }
210+ </ span >
211+ < span className = "font-semibold" >
212+ { formatMoney ( total , { currency : "USD" } ) }
213+ </ span >
214+ </ div >
215+ </ div >
216+ < DialogFooter className = "flex flex-col gap-2 sm:flex-row sm:justify-end" >
217+ < Button
218+ type = "button"
219+ variant = "outline"
220+ onClick = { ( ) => setConfirmOpen ( false ) }
221+ disabled = { isPending }
222+ >
223+ Cancel
224+ </ Button >
225+ < Button
226+ type = "button"
227+ onClick = { confirmBooking }
228+ disabled = { isPending }
229+ aria-busy = { isPending }
230+ >
231+ { isPending && < Spinner className = "mr-2" aria-hidden = "true" /> }
232+ Confirm booking
233+ </ Button >
234+ </ DialogFooter >
235+ </ DialogContent >
236+ </ Dialog >
165237 </ section >
166238 ) ;
167239}
0 commit comments