
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Luxury hotel room booking and reservation widget with interactive date range picker, guest & room steppers, room upgrade checkboxes, live reactive pricing breakdown, and instant reservation checkout.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/hotel-booking-reservation.json$npx shadcn@latest add https://uipkge.dev/r/react/hotel-booking-reservation.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/hotel-booking-reservation.json$bunx shadcn@latest add https://uipkge.dev/r/react/hotel-booking-reservation.jsonnpx shadcn@latest add @uipkge-react/hotel-booking-reservationInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
className | string | — | optional |
roomTitle | string | — | optional |
hotelName | string | — | optional |
location | string | — | optional |
rating | number | — | optional |
reviewsCount | number | — | optional |
basePrice | number | — | optional |
initialCheckIn | string | — | optional |
initialCheckOut | string | — | optional |
initialAdults | number | — | optional |
initialChildren | number | — | optional |
initialRooms | number | — | optional |
'use client'
import * as React from 'react'
import {
Bath,
BedDouble,
Building2,
Calendar,
CalendarDays,
Check,
CheckCircle2,
Coffee,
CreditCard,
Heart,
Info,
Lock,
MapPin,
Maximize2,
Minus,
Plus,
RotateCcw,
Share2,
ShieldCheck,
Star,
Users,
Baby,
Waves,
Wifi,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
export interface HotelBookingReservationProps {
className?: string
roomTitle?: string
hotelName?: string
location?: string
rating?: number
reviewsCount?: number
basePrice?: number
initialCheckIn?: string
initialCheckOut?: string
initialAdults?: number
initialChildren?: number
initialRooms?: number
}
const roomPhotos = [
{
url: 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=1200&auto=format&fit=crop&q=80',
title: 'King Master Suite & Ocean Balcony',
},
{
url: 'https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800&auto=format&fit=crop&q=80',
title: 'Atlantic Ocean Sunrise Balcony',
},
{
url: 'https://images.unsplash.com/photo-1584622650111-993a426fbf0a?w=800&auto=format&fit=crop&q=80',
title: 'Italian Marble Spa Bathroom',
},
{
url: 'https://images.unsplash.com/photo-1540541338287-41700207dee6?w=800&auto=format&fit=crop&q=80',
title: 'Private Beach Club & Ocean Cabanas',
},
]
export function HotelBookingReservation({
className,
roomTitle = 'Deluxe Ocean View King Suite',
hotelName = 'The Ritz-Carlton Bal Harbour, Miami',
location = 'Bal Harbour, Miami Beach, FL',
rating = 5.0,
reviewsCount = 120,
basePrice = 485,
initialCheckIn = '2026-08-24',
initialCheckOut = '2026-08-29',
initialAdults = 2,
initialChildren = 1,
initialRooms = 1,
}: HotelBookingReservationProps) {
const [checkInDate, setCheckInDate] = React.useState(initialCheckIn)
const [checkOutDate, setCheckOutDate] = React.useState(initialCheckOut)
const [adults, setAdults] = React.useState(initialAdults)
const [children, setChildren] = React.useState(initialChildren)
const [rooms, setRooms] = React.useState(initialRooms)
const [specialRequests, setSpecialRequests] = React.useState('')
const [isBooked, setIsBooked] = React.useState(false)
const [isSaved, setIsSaved] = React.useState(false)
const [activePhotoIndex, setActivePhotoIndex] = React.useState(0)
// Upgrades state: Oceanfront balcony and Daily Breakfast are checked by default
const [selectedUpgrades, setSelectedUpgrades] = React.useState<string[]>(['balcony', 'breakfast'])
const [breakfastGuests, setBreakfastGuests] = React.useState(1)
function toggleUpgrade(id: string) {
setSelectedUpgrades((prev) => (prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]))
}
// Date calculations
const nights = React.useMemo(() => {
if (!checkInDate || !checkOutDate) return 5
const start = new Date(checkInDate).getTime()
const end = new Date(checkOutDate).getTime()
const diffDays = Math.round((end - start) / (1000 * 60 * 60 * 24))
return diffDays > 0 ? diffDays : 1
}, [checkInDate, checkOutDate])
function setDuration(presetNights: number) {
if (!checkInDate) return
const start = new Date(checkInDate + 'T00:00:00')
start.setDate(start.getDate() + presetNights)
const y = start.getFullYear()
const m = String(start.getMonth() + 1).padStart(2, '0')
const d = String(start.getDate()).padStart(2, '0')
setCheckOutDate(`${y}-${m}-${d}`)
}
function formatDateDisplay(dateStr: string) {
if (!dateStr) return ''
const [year, month, day] = dateStr.split('-').map(Number)
if (!year || !month || !day) return dateStr
const date = new Date(year, month - 1, day)
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
const formattedRange = React.useMemo(() => {
return `${formatDateDisplay(checkInDate)} → ${formatDateDisplay(checkOutDate)} · ${nights} ${nights === 1 ? 'Night' : 'Nights'}`
}, [checkInDate, checkOutDate, nights])
const cancellationDeadline = React.useMemo(() => {
if (!checkInDate) return '48 hours prior to arrival'
const start = new Date(checkInDate + 'T00:00:00')
start.setDate(start.getDate() - 2)
const y = start.getFullYear()
const m = String(start.getMonth() + 1).padStart(2, '0')
const d = String(start.getDate()).padStart(2, '0')
return formatDateDisplay(`${y}-${m}-${d}`)
}, [checkInDate])
// Financial calculations
const roomSubtotal = basePrice * nights * rooms
const balconyCost = selectedUpgrades.includes('balcony') ? 50 * nights * rooms : 0
const breakfastCost = selectedUpgrades.includes('breakfast') ? 35 * breakfastGuests * nights : 0
const chauffeurCost = selectedUpgrades.includes('chauffeur') ? 120 : 0
const spaCost = selectedUpgrades.includes('spa') ? 65 * adults : 0
const selectedAddonsTotal = balconyCost + breakfastCost + chauffeurCost + spaCost
const resortFee = 30 * nights * rooms
const taxableSubtotal = roomSubtotal + selectedAddonsTotal + resortFee
const estimatedTaxes = Math.round(taxableSubtotal * 0.14 * 100) / 100
const totalStayPrice = taxableSubtotal + estimatedTaxes
function formatCurrency(val: number) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(val)
}
// Stepper modifiers
function updateAdults(delta: number) {
setAdults((prev) => {
const next = prev + delta
if (next >= 1 && next <= 8) {
if (breakfastGuests > next) {
setBreakfastGuests(next)
}
return next
}
return prev
})
}
function updateChildren(delta: number) {
setChildren((prev) => {
const next = prev + delta
if (next >= 0 && next <= 6) return next
return prev
})
}
function updateRooms(delta: number) {
setRooms((prev) => {
const next = prev + delta
if (next >= 1 && next <= 4) return next
return prev
})
}
return (
<div
data-slot="hotel-booking-reservation"
className={cn('mx-auto w-full max-w-6xl space-y-8 p-4 sm:p-6 lg:p-8', className)}
>
{/* Confirmation Banner (Visible when booked) */}
{isBooked && (
<div className="border-success/30 bg-success/10 bg-success/15 relative flex flex-col gap-4 rounded-xl border p-5 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-start gap-3.5">
<div className="bg-success/20 text-success mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-full">
<CheckCircle2 className="size-5" />
</div>
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-foreground text-base font-semibold">
Reservation Confirmed · Reference #RC-884920
</h3>
<Badge wrap variant="outline" className="border-success/40 text-success text-xs font-semibold">
Guaranteed Direct
</Badge>
</div>
<p className="text-muted-foreground text-xs sm:text-sm">
A confirmation voucher and check-in barcode have been sent to your registered email. No cancellation
penalty applies before {cancellationDeadline}.
</p>
</div>
</div>
<Button variant="outline" size="sm" className="shrink-0 font-medium" onClick={() => setIsBooked(false)}>
<RotateCcw className="size-3.5" />
Modify Reservation
</Button>
</div>
)}
{/* Room Hero Header Card */}
<Card className="border-border overflow-hidden shadow-xs">
<div className="grid grid-cols-1 gap-6 p-5 sm:p-6 lg:grid-cols-12 lg:gap-8">
{/* Room Photo Showcase */}
<div className="space-y-3 lg:col-span-6">
<div className="border-border bg-muted/40 relative aspect-16/10 w-full overflow-hidden rounded-xl border">
<img
src={roomPhotos[activePhotoIndex]?.url}
alt={roomPhotos[activePhotoIndex]?.title}
className="size-full object-cover transition-colors duration-300"
/>
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-black/20" />
<div className="absolute top-3 left-3 flex flex-wrap gap-1.5">
<Badge
wrap
variant="secondary"
className="bg-background/90 text-foreground text-xs font-medium backdrop-blur-xs"
>
Oceanfront View
</Badge>
<Badge
wrap
variant="outline"
className="bg-background/80 text-foreground border-white/20 text-xs backdrop-blur-xs"
>
Floor 16
</Badge>
</div>
<div className="absolute top-3 right-3 flex items-center gap-1.5">
<button
type="button"
aria-label="Save to favorites"
className="bg-background/80 hover:bg-background text-foreground flex size-8 items-center justify-center rounded-full backdrop-blur-xs transition-colors"
onClick={() => setIsSaved(!isSaved)}
>
<Heart
className={cn(
'size-4 transition-colors',
isSaved ? 'fill-destructive text-destructive' : 'text-foreground',
)}
/>
</button>
<button
type="button"
aria-label="Share suite"
className="bg-background/80 hover:bg-background text-foreground flex size-8 items-center justify-center rounded-full backdrop-blur-xs transition-colors"
>
<Share2 className="size-4" />
</button>
</div>
<div className="absolute right-3 bottom-3 left-3 flex items-center justify-between text-xs text-white">
<span className="font-medium drop-shadow-xs">{roomPhotos[activePhotoIndex]?.title}</span>
<span className="rounded bg-black/60 px-2 py-0.5 font-mono text-xs backdrop-blur-xs">
{activePhotoIndex + 1} / {roomPhotos.length}
</span>
</div>
</div>
{/* Mini Thumbnail Selector */}
<div className="grid grid-cols-4 gap-2">
{roomPhotos.map((photo, idx) => (
<button
key={idx}
type="button"
aria-label={photo.title}
className={cn(
'border-border relative aspect-16/10 overflow-hidden rounded-lg border transition-colors',
activePhotoIndex === idx ? 'ring-primary border-primary ring-2' : 'opacity-70 hover:opacity-100',
)}
onClick={() => setActivePhotoIndex(idx)}
>
<img src={photo.url} alt={photo.title} className="size-full object-cover" />
</button>
))}
</div>
</div>
{/* Room Meta & Overview Information */}
<div className="flex flex-col justify-between space-y-4 lg:col-span-6">
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="text-warning flex items-center gap-1.5 text-xs font-medium">
<Star className="fill-warning text-warning size-4" />
<span className="font-bold tabular-nums">{rating.toFixed(1)}</span>
<span className="text-muted-foreground underline underline-offset-2">
{reviewsCount} verified guest reviews
</span>
</div>
<Badge
wrap
variant="outline"
className="border-primary/30 bg-primary/5 text-primary text-xs font-medium"
>
Official Best Rate Guarantee
</Badge>
</div>
<div className="space-y-1">
<h1 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">{roomTitle}</h1>
<div className="text-muted-foreground flex items-center gap-1.5 text-xs sm:text-sm">
<Building2 className="text-primary size-4 shrink-0" />
<span className="text-foreground font-medium">{hotelName}</span>
</div>
<div className="text-muted-foreground flex items-center gap-1.5 text-xs">
<MapPin className="size-3.5 shrink-0" />
<span>{location}</span>
</div>
</div>
<p className="text-muted-foreground text-xs leading-relaxed sm:text-sm">
Expansive oceanfront sanctuary featuring custom King pillowtop bedding, floor-to-ceiling glass doors
opening onto an oversized private balcony with sweeping Atlantic panoramas, freestanding soaking tub,
and bespoke in-room dining.
</p>
{/* Room Key Amenities Grid */}
<div className="grid grid-cols-1 gap-2 pt-1 sm:grid-cols-2 md:grid-cols-3">
<div className="border-border/60 bg-muted/20 flex items-center gap-2 rounded-md border p-2 text-xs">
<Maximize2 className="text-muted-foreground size-3.5 shrink-0" />
<span className="text-foreground font-medium">680 sq ft / 63 m²</span>
</div>
<div className="border-border/60 bg-muted/20 flex items-center gap-2 rounded-md border p-2 text-xs">
<BedDouble className="text-muted-foreground size-3.5 shrink-0" />
<span className="text-foreground font-medium">1 King Bed</span>
</div>
<div className="border-border/60 bg-muted/20 flex items-center gap-2 rounded-md border p-2 text-xs">
<Waves className="text-muted-foreground size-3.5 shrink-0" />
<span className="text-foreground font-medium">Ocean Balcony</span>
</div>
<div className="border-border/60 bg-muted/20 flex items-center gap-2 rounded-md border p-2 text-xs">
<Bath className="text-muted-foreground size-3.5 shrink-0" />
<span className="text-foreground font-medium">Marble Soaking Tub</span>
</div>
<div className="border-border/60 bg-muted/20 flex items-center gap-2 rounded-md border p-2 text-xs">
<Wifi className="text-muted-foreground size-3.5 shrink-0" />
<span className="text-foreground font-medium">Fast Fiber WiFi</span>
</div>
<div className="border-border/60 bg-muted/20 flex items-center gap-2 rounded-md border p-2 text-xs">
<Coffee className="text-muted-foreground size-3.5 shrink-0" />
<span className="text-foreground font-medium">Nespresso Machine</span>
</div>
</div>
</div>
<div className="border-border/80 bg-muted/30 flex flex-wrap items-center justify-between gap-3 rounded-xl border p-3.5 sm:p-4">
<div>
<span className="text-muted-foreground block text-xs font-medium tracking-wider uppercase">
Direct Member Rate
</span>
<div className="flex flex-wrap items-baseline gap-2">
<span className="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">
{formatCurrency(basePrice)}
</span>
<span className="text-muted-foreground text-xs font-normal sm:text-sm">/ night</span>
<span className="text-muted-foreground text-xs tabular-nums line-through sm:text-sm">$560.00</span>
</div>
</div>
<Badge
wrap
variant="secondary"
className="border-success/20 bg-success/10 text-success text-xs font-semibold"
>
Save 13% Direct
</Badge>
</div>
</div>
</div>
</Card>
{/* Interactive 2-Column Booking Configuration & Price Breakdown Grid */}
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12 lg:items-start">
{/* Left Column: Reservation Inputs, Guests & Upgrades */}
<div className="space-y-6 lg:col-span-7">
{/* Stay Dates Selection */}
<Card className="shadow-xs">
<CardHeader>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<CardTitle className="text-lg">Stay Dates & Duration</CardTitle>
<CardDescription>
Select your check-in and check-out dates to calculate live availability.
</CardDescription>
</div>
<Badge wrap variant="outline" className="gap-1 text-xs font-medium">
<CalendarDays className="text-primary size-3.5" />
{nights} {nights === 1 ? 'Night' : 'Nights'}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-5">
{/* Date Inputs */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<label
htmlFor="check-in-date"
className="text-foreground flex items-center gap-1.5 text-xs font-semibold"
>
<Calendar className="text-primary size-3.5" />
Check-in Date (3:00 PM)
</label>
<Input
id="check-in-date"
type="date"
value={checkInDate}
onChange={(e) => setCheckInDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<label
htmlFor="check-out-date"
className="text-foreground flex items-center gap-1.5 text-xs font-semibold"
>
<Calendar className="text-primary size-3.5" />
Check-out Date (12:00 PM)
</label>
<Input
id="check-out-date"
type="date"
value={checkOutDate}
onChange={(e) => setCheckOutDate(e.target.value)}
/>
</div>
</div>
{/* Stay Summary Highlight & Quick Length Presets */}
<div className="border-border/60 bg-muted/30 flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between">
<div className="text-foreground text-xs font-medium">
<span className="text-muted-foreground">Selected Stay: </span>
<span className="font-semibold">{formattedRange}</span>
</div>
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-muted-foreground mr-1 text-xs">Quick:</span>
{[3, 5, 7, 10].map((preset) => (
<Button
key={preset}
variant="outline"
size="xs"
className={
nights === preset ? 'bg-primary text-primary-foreground border-primary hover:bg-primary/90' : ''
}
onClick={() => setDuration(preset)}
>
{preset}N
</Button>
))}
</div>
</div>
</CardContent>
</Card>
{/* Guests & Rooms Steppers */}
<Card className="shadow-xs">
<CardHeader>
<CardTitle className="text-lg">Guests & Rooms</CardTitle>
<CardDescription>Configure the number of adults, children, and suites requested.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Adults Stepper */}
<div className="border-border/60 flex flex-wrap items-center justify-between rounded-lg border p-3.5">
<div className="flex items-center gap-3">
<div className="bg-primary/10 text-primary border-primary/20 flex size-9 shrink-0 items-center justify-center rounded-lg border">
<Users className="size-4" />
</div>
<div className="space-y-0.5">
<span className="text-foreground text-sm font-medium">Adults</span>
<p className="text-muted-foreground text-xs">Ages 13 and above</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="icon-sm"
disabled={adults <= 1}
aria-label="Decrease adults"
onClick={() => updateAdults(-1)}
>
<Minus className="size-3.5" />
</Button>
<span className="text-foreground w-6 text-center text-sm font-bold tabular-nums">{adults}</span>
<Button
variant="outline"
size="icon-sm"
disabled={adults >= 8}
aria-label="Increase adults"
onClick={() => updateAdults(1)}
>
<Plus className="size-3.5" />
</Button>
</div>
</div>
{/* Children Stepper */}
<div className="border-border/60 flex flex-wrap items-center justify-between rounded-lg border p-3.5">
<div className="flex items-center gap-3">
<div className="bg-primary/10 text-primary border-primary/20 flex size-9 shrink-0 items-center justify-center rounded-lg border">
<Baby className="size-4" />
</div>
<div className="space-y-0.5">
<span className="text-foreground text-sm font-medium">Children</span>
<p className="text-muted-foreground text-xs">Ages 0 to 12 years</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="icon-sm"
disabled={children <= 0}
aria-label="Decrease children"
onClick={() => updateChildren(-1)}
>
<Minus className="size-3.5" />
</Button>
<span className="text-foreground w-6 text-center text-sm font-bold tabular-nums">{children}</span>
<Button
variant="outline"
size="icon-sm"
disabled={children >= 6}
aria-label="Increase children"
onClick={() => updateChildren(1)}
>
<Plus className="size-3.5" />
</Button>
</div>
</div>
{/* Rooms Stepper */}
<div className="border-border/60 flex flex-wrap items-center justify-between rounded-lg border p-3.5">
<div className="flex items-center gap-3">
<div className="bg-primary/10 text-primary border-primary/20 flex size-9 shrink-0 items-center justify-center rounded-lg border">
<BedDouble className="size-4" />
</div>
<div className="space-y-0.5">
<span className="text-foreground text-sm font-medium">Suites / Rooms</span>
<p className="text-muted-foreground text-xs">Max 4 guests per suite</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="icon-sm"
disabled={rooms <= 1}
aria-label="Decrease rooms"
onClick={() => updateRooms(-1)}
>
<Minus className="size-3.5" />
</Button>
<span className="text-foreground w-6 text-center text-sm font-bold tabular-nums">{rooms}</span>
<Button
variant="outline"
size="icon-sm"
disabled={rooms >= 4}
aria-label="Increase rooms"
onClick={() => updateRooms(1)}
>
<Plus className="size-3.5" />
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Room Upgrades & Add-ons */}
<Card className="shadow-xs">
<CardHeader>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<CardTitle className="text-lg">Room Upgrades & Exclusive Services</CardTitle>
<CardDescription>
Tailor your stay with luxury amenities and curated hotel enhancements.
</CardDescription>
</div>
<Badge wrap variant="secondary" className="text-xs">
{selectedUpgrades.length} Selected
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
{/* Upgrade 1: Oceanfront Balcony */}
<div
role="button"
tabIndex={0}
className={cn(
'border-border focus-visible:ring-ring flex cursor-pointer items-start justify-between gap-4 rounded-xl border p-4 transition-colors select-none focus-visible:ring-2 focus-visible:outline-none',
selectedUpgrades.includes('balcony')
? 'border-primary/40 bg-primary/5 shadow-xs'
: 'bg-card hover:border-border/80 opacity-80 hover:opacity-100',
)}
onClick={() => toggleUpgrade('balcony')}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
toggleUpgrade('balcony')
}
}}
>
<div className="flex flex-wrap items-start gap-3.5">
<Checkbox
id="upgrade-balcony"
checked={selectedUpgrades.includes('balcony')}
className="pointer-events-none mt-1"
tabIndex={-1}
/>
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-foreground text-sm font-semibold">Oceanfront High-Floor Balcony</span>
<Badge wrap variant="outline" className="text-xs font-medium">
+$50 / night
</Badge>
</div>
<p className="text-muted-foreground text-xs leading-relaxed">
Guaranteed unobstructed 180° Atlantic Ocean sunrise view on floors 15+ with premium teak deck
loungers.
</p>
</div>
</div>
<div className="shrink-0 text-right">
<span className="text-foreground text-sm font-bold tabular-nums">
+{formatCurrency(50 * nights * rooms)}
</span>
<span className="text-muted-foreground block text-xs">for {nights} nights</span>
</div>
</div>
{/* Upgrade 2: Gourmet Breakfast */}
<div
role="button"
tabIndex={0}
className={cn(
'border-border focus-visible:ring-ring flex cursor-pointer items-start justify-between gap-4 rounded-xl border p-4 transition-colors select-none focus-visible:ring-2 focus-visible:outline-none',
selectedUpgrades.includes('breakfast')
? 'border-primary/40 bg-primary/5 shadow-xs'
: 'bg-card hover:border-border/80 opacity-80 hover:opacity-100',
)}
onClick={() => toggleUpgrade('breakfast')}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
toggleUpgrade('breakfast')
}
}}
>
<div className="flex flex-wrap items-start gap-3.5">
<Checkbox
id="upgrade-breakfast"
checked={selectedUpgrades.includes('breakfast')}
className="pointer-events-none mt-1"
tabIndex={-1}
/>
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-foreground text-sm font-semibold">Daily Gourmet Breakfast</span>
<Badge wrap variant="outline" className="text-xs font-medium">
+$35 / person / day
</Badge>
</div>
<p className="text-muted-foreground text-xs leading-relaxed">
Artisanal morning buffet at Artisan Beach House or private veranda in-room breakfast with fresh
juices.
</p>
{/* Guest selection when active */}
{selectedUpgrades.includes('breakfast') && (
<div
className="flex flex-wrap items-center gap-2 pt-1.5 text-xs"
onClick={(e) => e.stopPropagation()}
>
<span className="text-muted-foreground">Breakfast plan for:</span>
<div className="flex flex-wrap items-center gap-1.5">
{[1, 2].map((count) => (
<Button
key={count}
variant="outline"
size="xs"
className={
breakfastGuests === count ? 'bg-primary text-primary-foreground border-primary' : ''
}
onClick={() => setBreakfastGuests(count)}
>
{count} {count === 1 ? 'Guest' : 'Guests'}
</Button>
))}
</div>
</div>
)}
</div>
</div>
<div className="shrink-0 text-right">
<span className="text-foreground text-sm font-bold tabular-nums">
+{formatCurrency(35 * breakfastGuests * nights)}
</span>
<span className="text-muted-foreground block text-xs">
{breakfastGuests} {breakfastGuests === 1 ? 'guest' : 'guests'} · {nights}d
</span>
</div>
</div>
{/* Upgrade 3: Airport Chauffeur */}
<div
role="button"
tabIndex={0}
className={cn(
'border-border focus-visible:ring-ring flex cursor-pointer items-start justify-between gap-4 rounded-xl border p-4 transition-colors select-none focus-visible:ring-2 focus-visible:outline-none',
selectedUpgrades.includes('chauffeur')
? 'border-primary/40 bg-primary/5 shadow-xs'
: 'bg-card hover:border-border/80 opacity-80 hover:opacity-100',
)}
onClick={() => toggleUpgrade('chauffeur')}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
toggleUpgrade('chauffeur')
}
}}
>
<div className="flex flex-wrap items-start gap-3.5">
<Checkbox
id="upgrade-chauffeur"
checked={selectedUpgrades.includes('chauffeur')}
className="pointer-events-none mt-1"
tabIndex={-1}
/>
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-foreground text-sm font-semibold">Private Airport Luxury Chauffeur</span>
<Badge wrap variant="outline" className="text-xs font-medium">
+$120 one-time
</Badge>
</div>
<p className="text-muted-foreground text-xs leading-relaxed">
Curbside meet-and-greet with dedicated Cadillac Escalade or Mercedes S-Class transfer from MIA or
FLL.
</p>
</div>
</div>
<div className="shrink-0 text-right">
<span className="text-foreground text-sm font-bold tabular-nums">+$120.00</span>
<span className="text-muted-foreground block text-xs">flat one-time</span>
</div>
</div>
</CardContent>
</Card>
{/* Special Requests Card */}
<Card className="shadow-xs">
<CardHeader className="pb-3">
<CardTitle className="text-base">Special Requests & Arrival Notes</CardTitle>
<CardDescription>
Let us know if you are celebrating a special occasion or require early check-in.
</CardDescription>
</CardHeader>
<CardContent>
<Input
id="special-requests"
value={specialRequests}
onChange={(e) => setSpecialRequests(e.target.value)}
placeholder="e.g., Honeymoon celebration, feather-free bedding, quiet high floor..."
maxLength={200}
showCount
/>
</CardContent>
</Card>
</div>
{/* Right Column: Total Price Breakdown & Checkout Card (Sticky) */}
<div className="lg:sticky lg:top-8 lg:col-span-5">
<Card className="border-border bg-card shadow-xs">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Stay Price Breakdown
</span>
<Badge
wrap
variant="secondary"
className="bg-primary/10 text-primary border-primary/20 gap-1 text-xs font-medium"
>
<CheckCircle2 className="size-3" />
Instant Confirmation
</Badge>
</div>
<div className="mt-3 space-y-1">
<div className="text-muted-foreground text-xs">Total Stay Price (USD)</div>
<div className="flex items-baseline gap-2">
<span className="text-foreground text-3xl font-bold tracking-tight tabular-nums sm:text-4xl">
{formatCurrency(totalStayPrice)}
</span>
</div>
<p className="text-muted-foreground text-xs">
Total for <span className="text-foreground font-medium">{nights} nights</span> · {rooms} suite ·{' '}
{adults + children} guests
</p>
</div>
</CardHeader>
<CardContent className="space-y-4">
<Separator />
{/* Itemized Pricing Breakdown */}
<div className="space-y-2.5">
{/* Base Room Subtotal */}
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
Room subtotal ({formatCurrency(basePrice)} × {nights} {nights === 1 ? 'night' : 'nights'}
{rooms > 1 ? ` × ${rooms} rooms` : ''})
</span>
<span className="text-foreground font-semibold tabular-nums">{formatCurrency(roomSubtotal)}</span>
</div>
{/* Selected Add-ons summary / itemized */}
<div className="space-y-1.5">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Selected Add-ons ({selectedUpgrades.length} active)</span>
<span className="text-foreground font-semibold tabular-nums">
{formatCurrency(selectedAddonsTotal)}
</span>
</div>
{/* Addon sub-items */}
{selectedUpgrades.length > 0 && (
<div className="border-border/60 bg-muted/20 space-y-1 rounded-md border p-2.5 text-xs">
{selectedUpgrades.includes('balcony') && (
<div className="text-muted-foreground flex items-center justify-between">
<span>
• Oceanfront Balcony ({formatCurrency(50)} × {nights}n)
</span>
<span className="text-foreground font-medium tabular-nums">
{formatCurrency(balconyCost)}
</span>
</div>
)}
{selectedUpgrades.includes('breakfast') && (
<div className="text-muted-foreground flex items-center justify-between">
<span>
• Gourmet Breakfast ({formatCurrency(35)} × {breakfastGuests}g × {nights}d)
</span>
<span className="text-foreground font-medium tabular-nums">
{formatCurrency(breakfastCost)}
</span>
</div>
)}
{selectedUpgrades.includes('chauffeur') && (
<div className="text-muted-foreground flex items-center justify-between">
<span>• Luxury Airport Chauffeur (one-time)</span>
<span className="text-foreground font-medium tabular-nums">
{formatCurrency(chauffeurCost)}
</span>
</div>
)}
{selectedUpgrades.includes('spa') && (
<div className="text-muted-foreground flex items-center justify-between">
<span>
• Thermal Spa Pass ({formatCurrency(65)} × {adults} guests)
</span>
<span className="text-foreground font-medium tabular-nums">{formatCurrency(spaCost)}</span>
</div>
)}
</div>
)}
</div>
{/* Resort Fee */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1">
<span className="text-muted-foreground">Resort & Hospitality Fee</span>
<Info className="text-muted-foreground/70 size-3.5 cursor-help" />
</div>
<span className="text-foreground font-semibold tabular-nums">{formatCurrency(resortFee)}</span>
</div>
<div className="text-muted-foreground pl-0.5 text-xs">
$30.00 / night · Beach loungers, high-speed WiFi, welcome champagne
</div>
{/* Estimated Taxes */}
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Estimated Taxes & Tourism Surcharge (14%)</span>
<span className="text-foreground font-semibold tabular-nums">{formatCurrency(estimatedTaxes)}</span>
</div>
</div>
<Separator />
{/* Cancellation Policy Banner */}
<div className="border-success/20 bg-success/10 bg-success/15 space-y-1 rounded-xl border p-3.5">
<div className="flex items-center gap-2">
<ShieldCheck className="text-success size-4 shrink-0" />
<span className="text-foreground text-xs font-semibold">
Free cancellation until 48 hours before check-in
</span>
</div>
<p className="text-muted-foreground pl-6 text-xs leading-normal">
Cancel before <span className="text-foreground font-medium">{cancellationDeadline}</span> for a 100%
full refund with zero fees.
</p>
</div>
{/* Trust Bullet Points */}
<ul className="text-muted-foreground space-y-2 text-xs">
<li className="flex items-center gap-2">
<Check className="text-primary size-3.5 shrink-0" />
<span>No upfront booking fees or surprise service surcharges</span>
</li>
<li className="flex items-center gap-2">
<Check className="text-primary size-3.5 shrink-0" />
<span>Official Ritz-Carlton direct member rewards applied</span>
</li>
<li className="flex items-center gap-2">
<Lock className="text-primary size-3.5 shrink-0" />
<span>256-bit encrypted secure bank checkout</span>
</li>
</ul>
</CardContent>
<CardFooter className="flex flex-col gap-2.5 pt-2">
<Button className="w-full gap-2 font-semibold shadow-xs" size="lg" onClick={() => setIsBooked(true)}>
<CreditCard className="size-4" />
<span>Reserve & Pay Now ({formatCurrency(totalStayPrice)})</span>
</Button>
<Button
variant="outline"
className="w-full text-xs font-medium"
size="default"
onClick={() => setIsBooked(true)}
>
Hold Room · Pay at Check-in
</Button>
<p className="text-muted-foreground text-center text-xs">
You won't be charged yet. Final confirmation is sent instantly.
</p>
</CardFooter>
</Card>
</div>
</div>
</div>
)
}
Raw manifest:https://uipkge.dev/r/react/hotel-booking-reservation.json