
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Architectural unit floor plan inspector and residential layout explorer with 2D CAD-styled blueprint canvas, interactive room highlight pins, dimensional callouts, room breakdown specs, and building unit availability table.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/floor-plan-explorer.json$npx shadcn@latest add https://uipkge.dev/r/react/floor-plan-explorer.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/floor-plan-explorer.json$bunx shadcn@latest add https://uipkge.dev/r/react/floor-plan-explorer.jsonnpx shadcn@latest add @uipkge-react/floor-plan-explorerInstalls to:components/blocks/floor-plan-explorer/| Name | Type / Values | Default | Required |
|---|---|---|---|
className | string | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
RoomSpecinterface RoomSpec {
id: string
name: string
shortLabel: string
dimensionsImperial: string
dimensionsMetric: string
areaSqFt: number
areaSqM: number
flooring: string
features: string[]
pinX: number
pinY: number
rect: {
x: number
y: number
w: number
h: number
}
}AvailableUnitinterface AvailableUnit {
unitNumber: string
floor: number
view: string
monthlyRent: number
sqFt: number
status: 'available-now' | 'available-soon' | 'leased' | 'under-contract'
statusLabel: string
moveInDate: string
exposure: string
}FloorPlanDatainterface FloorPlanData {
id: string
tabKey: string
tabLabel: string
marketingName: string
tier: string
beds: number
baths: number
totalAreaSqFt: number
totalAreaSqM: number
interiorSqFt: number
exteriorSqFt: number
ceilingHeight: string
exposure: string
startingRent: number
description: string
highlights: string[]
rooms: RoomSpec[]
availableUnits: AvailableUnit[]
}'use client'
import * as React from 'react'
import { useMemo } from 'react'
import { Bath, Bed, Building2, Check, Download } 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 { AvailableUnitsTable } from './AvailableUnitsTable'
import { FloorPlanCanvas } from './FloorPlanCanvas'
import { type AvailableUnit, type FloorPlanData, type RoomSpec } from './floor-plan-explorer-types'
export type { AvailableUnit, FloorPlanData, RoomSpec } from './floor-plan-explorer-types'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
export interface FloorPlanExplorerProps {
className?: string
}
const floorPlans: Record<string, FloorPlanData> = {
'plan-a': {
id: 'plan-a',
tabKey: 'plan-a',
tabLabel: 'Plan A - 1 Bed / 1 Bath',
marketingName: 'Plan A - The Urban Haven',
tier: 'Urban Gallery Residence',
beds: 1,
baths: 1,
totalAreaSqFt: 780,
totalAreaSqM: 72.5,
interiorSqFt: 735,
exteriorSqFt: 45,
ceilingHeight: `10' 0" Finished`,
exposure: 'East · Garden & Sunrise',
startingRent: 3450,
description:
'Artfully designed open-concept layout featuring oversized east-facing windows, gourmet galley kitchen with quartz surfaces, secluded bedroom suite with walk-in closet, and a private sunrise terrace.',
highlights: [
'Floor-to-ceiling glass wall',
'European quartz kitchen island',
'Deep soaking tub & frameless shower',
'Private sunrise terrace',
],
rooms: [
{
id: 'living',
name: 'Living & Dining Room',
shortLabel: 'Living / Dining',
dimensionsImperial: `16' × 12'`,
dimensionsMetric: '4.9m × 3.7m',
areaSqFt: 192,
areaSqM: 17.8,
flooring: 'Wide Plank Natural White Oak',
features: ['East Sunrise Exposure', '9-Foot Glazing Wall', 'Concealed Media Wiring'],
pinX: 280,
pinY: 175,
rect: { x: 170, y: 90, w: 230, h: 170 },
},
{
id: 'master-bed',
name: 'Master Bedroom',
shortLabel: 'Master Bed',
dimensionsImperial: `13' × 11'`,
dimensionsMetric: '4.0m × 3.4m',
areaSqFt: 143,
areaSqM: 13.3,
flooring: 'Plush Organic Wool Loop',
features: ['Acoustic Insulation', 'Custom Closet Built-ins', 'Blackout Shade Pocket'],
pinX: 505,
pinY: 175,
rect: { x: 410, y: 90, w: 190, h: 170 },
},
{
id: 'kitchen',
name: 'Gourmet Galley Kitchen',
shortLabel: 'Kitchen',
dimensionsImperial: `11' × 9'`,
dimensionsMetric: '3.4m × 2.7m',
areaSqFt: 99,
areaSqM: 9.2,
flooring: 'Calacatta Honed Quartzite',
features: ['Integrated Bosch Appliances', 'Undermount LED Lights', 'Soft-Close Walnut Joinery'],
pinX: 280,
pinY: 330,
rect: { x: 170, y: 270, w: 230, h: 120 },
},
{
id: 'master-bath',
name: 'Primary Full Bath',
shortLabel: 'Full Bath',
dimensionsImperial: `9' × 6'`,
dimensionsMetric: '2.7m × 1.8m',
areaSqFt: 54,
areaSqM: 5.0,
flooring: 'Matte Slate & Terrazzo',
features: ['Kohler Undermount Sink', 'Frameless Glass Shower', 'Backlit Anti-Fog Vanity'],
pinX: 505,
pinY: 330,
rect: { x: 410, y: 270, w: 190, h: 120 },
},
{
id: 'balcony',
name: 'Private Terrace',
shortLabel: 'Terrace',
dimensionsImperial: `9' × 5'`,
dimensionsMetric: '2.7m × 1.5m',
areaSqFt: 45,
areaSqM: 4.2,
flooring: 'Weathered Teak Decking',
features: ['Architectural Steel Railing', 'Exterior Power Outlet', 'Garden Courtyard Vistas'],
pinX: 105,
pinY: 175,
rect: { x: 50, y: 90, w: 110, h: 170 },
},
],
availableUnits: [
{
unitNumber: '304',
floor: 3,
view: 'Courtyard & Garden',
monthlyRent: 3450,
sqFt: 780,
status: 'available-now',
statusLabel: 'Available Now',
moveInDate: 'Immediate',
exposure: 'East',
},
{
unitNumber: '604',
floor: 6,
view: 'Skyline Sunrise View',
monthlyRent: 3650,
sqFt: 780,
status: 'available-soon',
statusLabel: 'Available Oct 15',
moveInDate: 'Oct 15, 2026',
exposure: 'East / North',
},
{
unitNumber: '904',
floor: 9,
view: 'High-Floor Park View',
monthlyRent: 3800,
sqFt: 780,
status: 'leased',
statusLabel: 'Leased',
moveInDate: 'Occupied',
exposure: 'East',
},
],
},
'plan-b': {
id: 'plan-b',
tabKey: 'plan-b',
tabLabel: 'Plan B - 2 Bed / 2 Bath',
marketingName: 'Plan B - The Skyline Suite',
tier: 'Signature Corner Residence',
beds: 2,
baths: 2,
totalAreaSqFt: 1420,
totalAreaSqM: 132.0,
interiorSqFt: 1360,
exteriorSqFt: 60,
ceilingHeight: `10' 6" Finished`,
exposure: 'Southwest · Bay & City Skyline',
startingRent: 4850,
description:
'Expansive dual-exposure residence centered around a dramatic living and entertaining salon with floor-to-ceiling panoramic glass, chef-grade kitchen island, split master and guest wings, and a sheltered private terrace.',
highlights: [
'Dual-aspect corner positioning with 270° light',
'Chef island with 10-foot marble waterfall',
'Primary wing with dual walk-in closets',
'Dedicated foyer vestibule & utility pantry',
],
rooms: [
{
id: 'living',
name: 'Living Room',
shortLabel: 'Living Room',
dimensionsImperial: `18' × 14'`,
dimensionsMetric: '5.5m × 4.3m',
areaSqFt: 252,
areaSqM: 23.4,
flooring: 'Wide Plank European White Oak',
features: ['Floor-to-Ceiling Corner Glazing', 'Motorized Solar Shades', 'Recessed Architectural Lighting'],
pinX: 255,
pinY: 160,
rect: { x: 150, y: 75, w: 220, h: 175 },
},
{
id: 'master-bed',
name: 'Master Bedroom',
shortLabel: 'Master Bed',
dimensionsImperial: `15' × 12'`,
dimensionsMetric: '4.6m × 3.7m',
areaSqFt: 180,
areaSqM: 16.7,
flooring: 'New Zealand Wool Weave',
features: ['South Bay Views', 'Direct En-Suite Access', 'Custom Millwork Wardrobe'],
pinX: 455,
pinY: 145,
rect: { x: 380, y: 75, w: 155, h: 150 },
},
{
id: 'kitchen',
name: 'Chef Kitchen & Dining',
shortLabel: 'Kitchen',
dimensionsImperial: `12' × 10'`,
dimensionsMetric: '3.7m × 3.0m',
areaSqFt: 120,
areaSqM: 11.1,
flooring: 'Calacatta Honed Quartzite',
features: ['Waterfall Marble Island', 'Sub-Zero & Wolf Appliances', 'Pantry Cabinetry'],
pinX: 255,
pinY: 325,
rect: { x: 150, y: 260, w: 220, h: 135 },
},
{
id: 'master-bath',
name: 'Master Bath',
shortLabel: 'Master Bath',
dimensionsImperial: `10' × 8'`,
dimensionsMetric: '3.0m × 2.4m',
areaSqFt: 80,
areaSqM: 7.4,
flooring: 'Bianco Carrara Marble',
features: ['Dual Floating Vanities', 'Walk-in Rain Shower', 'Acoustic Privacy Door'],
pinX: 590,
pinY: 145,
rect: { x: 545, y: 75, w: 90, h: 150 },
},
{
id: 'guest-bed',
name: 'Guest Bedroom / Study',
shortLabel: 'Guest Bed',
dimensionsImperial: `13' × 11'`,
dimensionsMetric: '4.0m × 3.4m',
areaSqFt: 143,
areaSqM: 13.3,
flooring: 'Wide Plank European White Oak',
features: ['Multi-Function Home Office', 'South Window Exposure', 'Built-in Closet System'],
pinX: 565,
pinY: 320,
rect: { x: 490, y: 245, w: 145, h: 150 },
},
{
id: 'guest-bath',
name: 'Guest Full Bath',
shortLabel: 'Guest Bath',
dimensionsImperial: `8' × 6'`,
dimensionsMetric: '2.4m × 1.8m',
areaSqFt: 48,
areaSqM: 4.5,
flooring: 'Smoked Gray Terrazzo',
features: ['Full Soaking Tub', 'Modern Matte Black Fixtures', 'Linen Storage'],
pinX: 430,
pinY: 320,
rect: { x: 380, y: 245, w: 100, h: 150 },
},
{
id: 'balcony',
name: 'Private Terrace',
shortLabel: 'Terrace',
dimensionsImperial: `10' × 6'`,
dimensionsMetric: '3.0m × 1.8m',
areaSqFt: 60,
areaSqM: 5.6,
flooring: 'Architectural Ipe Wood',
features: ['Panoramic Bay Overlook', 'Weatherproof Recessed Sconces', 'Glass Wind Guard'],
pinX: 90,
pinY: 160,
rect: { x: 40, y: 75, w: 100, h: 175 },
},
],
availableUnits: [
{
unitNumber: '402',
floor: 4,
view: 'South City View',
monthlyRent: 4850,
sqFt: 1420,
status: 'available-now',
statusLabel: 'Available Now',
moveInDate: 'Immediate',
exposure: 'South / West',
},
{
unitNumber: '708',
floor: 7,
view: 'Bay & Marina View',
monthlyRent: 5200,
sqFt: 1420,
status: 'available-soon',
statusLabel: 'Available Sep 1',
moveInDate: 'Sep 1, 2026',
exposure: 'Bay View / South',
},
{
unitNumber: '1102',
floor: 11,
view: 'Penthouse Level Panoramic',
monthlyRent: 5600,
sqFt: 1420,
status: 'leased',
statusLabel: 'Leased',
moveInDate: 'Occupied',
exposure: 'Panoramic South',
},
],
},
'plan-c': {
id: 'plan-c',
tabKey: 'plan-c',
tabLabel: 'Plan C - 3 Bed Penthouse',
marketingName: 'Plan C - The Grandview Penthouse',
tier: 'Crown Estate Penthouse',
beds: 3,
baths: 3.5,
totalAreaSqFt: 2250,
totalAreaSqM: 209.0,
interiorSqFt: 2090,
exteriorSqFt: 160,
ceilingHeight: `11' 8" Finished`,
exposure: '360° Panoramic · Harbor, Bay & Bridges',
startingRent: 9400,
description:
'A true architectural masterpiece occupying the top levels of the tower. Direct key-lock private elevator access opens into a sweeping grand salon with 11.5-foot ceilings, full culinary kitchen with butler pantry, spa sanctuary primary suite, and wraparound sky terrace.',
highlights: [
'Private elevator vestibule with secure biometric access',
'Wraparound sky terrace with built-in gas fire table',
'Primary retreat with double walk-in dressing galleries',
'Butler service pantry with climate-controlled wine vault',
],
rooms: [
{
id: 'grand-salon',
name: 'Grand Salon & Living',
shortLabel: 'Grand Salon',
dimensionsImperial: `24' × 16'`,
dimensionsMetric: '7.3m × 4.9m',
areaSqFt: 384,
areaSqM: 35.7,
flooring: 'Herringbone European Oak',
features: ['360° Bay Vista Glazing', 'Gas Linear Fireplace', 'Custom Architectural Coving'],
pinX: 275,
pinY: 155,
rect: { x: 160, y: 85, w: 240, h: 150 },
},
{
id: 'primary-suite',
name: 'Primary Penthouse Suite',
shortLabel: 'Primary Suite',
dimensionsImperial: `18' × 14'`,
dimensionsMetric: '5.5m × 4.3m',
areaSqFt: 252,
areaSqM: 23.4,
flooring: 'Custom Silk-Wool Blend Weave',
features: ['Harbor Sunrise Exposure', 'Sitting Lounge Area', 'Dual Walk-in Closets'],
pinX: 480,
pinY: 155,
rect: { x: 410, y: 85, w: 140, h: 150 },
},
{
id: 'spa-bath',
name: 'Spa Sanctuary Primary Bath',
shortLabel: 'Spa Bath',
dimensionsImperial: `12' × 9'`,
dimensionsMetric: '3.7m × 2.7m',
areaSqFt: 108,
areaSqM: 10.0,
flooring: 'Bookmatched Calacatta Marble',
features: ['Freestanding Oval Soaking Tub', 'Dual Steam Shower', 'Radiant Heated Flooring'],
pinX: 595,
pinY: 155,
rect: { x: 560, y: 85, w: 80, h: 150 },
},
{
id: 'dining',
name: 'Formal Dining Gallery',
shortLabel: 'Dining Room',
dimensionsImperial: `15' × 12'`,
dimensionsMetric: '4.6m × 3.7m',
areaSqFt: 180,
areaSqM: 16.7,
flooring: 'Herringbone European Oak',
features: ['Seating for 10-12 Guests', 'Integrated Bar Cabinetry', 'Designer Chandelier Anchor'],
pinX: 235,
pinY: 315,
rect: { x: 160, y: 245, w: 150, h: 145 },
},
{
id: 'kitchen',
name: 'Culinary Kitchen & Pantry',
shortLabel: 'Kitchen',
dimensionsImperial: `16' × 11'`,
dimensionsMetric: '4.9m × 3.4m',
areaSqFt: 176,
areaSqM: 16.3,
flooring: 'Taj Mahal Quartzite',
features: ['12-Foot Monolithic Island', 'Gaggenau 400 Series Suite', 'Butler Prep Scullery'],
pinX: 95,
pinY: 315,
rect: { x: 40, y: 245, w: 110, h: 145 },
},
{
id: 'bed-2',
name: 'Bedroom Suite 2',
shortLabel: 'Bed 2 Suite',
dimensionsImperial: `14' × 12'`,
dimensionsMetric: '4.3m × 3.7m',
areaSqFt: 168,
areaSqM: 15.6,
flooring: 'Wide Plank European Oak',
features: ['En-Suite Private Bath', 'South Window Bank', 'Deep Built-in Wardrobe'],
pinX: 395,
pinY: 315,
rect: { x: 320, y: 245, w: 150, h: 145 },
},
{
id: 'bed-3',
name: 'Bedroom 3 / Executive Study',
shortLabel: 'Bed 3 / Study',
dimensionsImperial: `13' × 11'`,
dimensionsMetric: '4.0m × 3.4m',
areaSqFt: 143,
areaSqM: 13.3,
flooring: 'Custom Walnut Parquetry',
features: ['Integrated Library Shelving', 'Acoustic Pocket Doors', 'Corner View Light'],
pinX: 560,
pinY: 315,
rect: { x: 480, y: 245, w: 160, h: 145 },
},
{
id: 'sky-terrace',
name: 'Wraparound Sky Terrace',
shortLabel: 'Sky Terrace',
dimensionsImperial: `20' × 8'`,
dimensionsMetric: '6.1m × 2.4m',
areaSqFt: 160,
areaSqM: 14.9,
flooring: 'Porcelain Pavers & Teak',
features: ['Integrated Linear Gas Hearth', 'Outdoor Audio Pre-Wire', 'Frameless Wind Barrier'],
pinX: 340,
pinY: 45,
rect: { x: 160, y: 25, w: 360, h: 50 },
},
],
availableUnits: [
{
unitNumber: 'PH-01',
floor: 14,
view: '360° Bay & City Skyline',
monthlyRent: 9400,
sqFt: 2250,
status: 'available-now',
statusLabel: 'Available Now',
moveInDate: 'Immediate',
exposure: '360° Panoramic',
},
{
unitNumber: 'PH-02',
floor: 14,
view: 'Harbor Sunrise & Marina',
monthlyRent: 9100,
sqFt: 2250,
status: 'under-contract',
statusLabel: 'Under Contract',
moveInDate: 'Pending',
exposure: 'East / South',
},
{
unitNumber: 'PH-03',
floor: 15,
view: 'Crown Sky Penthouse with Spa',
monthlyRent: 10500,
sqFt: 2250,
status: 'available-soon',
statusLabel: 'Available Nov 1',
moveInDate: 'Nov 1, 2026',
exposure: 'Crown Panoramic',
},
],
},
}
export function FloorPlanExplorer({ className }: FloorPlanExplorerProps) {
const [activeTab, setActiveTab] = React.useState('plan-b')
const [selectedRoomId, setSelectedRoomId] = React.useState<string | null>('living')
const [hoveredRoomId, setHoveredRoomId] = React.useState<string | null>(null)
const [selectedUnitNumber, setSelectedUnitNumber] = React.useState<string>('402')
const [showDimensions, setShowDimensions] = React.useState(true)
const [showFurniture, setShowFurniture] = React.useState(true)
const [showPins, setShowPins] = React.useState(true)
const [unitSystem, setUnitSystem] = React.useState<'imperial' | 'metric'>('imperial')
const [isDownloadingPdf, setIsDownloadingPdf] = React.useState(false)
const [downloadSuccess, setDownloadSuccess] = React.useState(false)
const [showTourSuccess, setShowTourSuccess] = React.useState(false)
const currentPlan = useMemo(() => floorPlans[activeTab] || floorPlans['plan-b'], [activeTab])
const activeRoom = useMemo(() => {
if (hoveredRoomId) {
return currentPlan.rooms.find((r) => r.id === hoveredRoomId) || null
}
if (selectedRoomId) {
return currentPlan.rooms.find((r) => r.id === selectedRoomId) || null
}
return currentPlan.rooms[0] || null
}, [hoveredRoomId, selectedRoomId, currentPlan])
const handleSelectTab = (key: string) => {
setActiveTab(key)
const plan = floorPlans[key]
if (plan) {
setSelectedRoomId(plan.rooms[0]?.id || null)
setHoveredRoomId(null)
setSelectedUnitNumber(plan.availableUnits[0]?.unitNumber || '')
}
}
const handleSelectRoom = (roomId: string) => {
setSelectedRoomId((prev) => (prev === roomId ? null : roomId))
}
const handleDownloadPdf = () => {
setIsDownloadingPdf(true)
setDownloadSuccess(false)
setTimeout(() => {
setIsDownloadingPdf(false)
setDownloadSuccess(true)
setTimeout(() => {
setDownloadSuccess(false)
}, 4000)
}, 900)
}
const handleScheduleTour = () => {
setShowTourSuccess(true)
setTimeout(() => {
setShowTourSuccess(false)
}, 4500)
}
return (
<div data-slot="floor-plan-explorer" className={cn('bg-background text-foreground w-full space-y-6', className)}>
{/* Header: Building Title, Plan Switcher Tabs, Action Button */}
<header className="border-border flex flex-col gap-4 border-b pb-6 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-muted-foreground gap-1 px-2 py-0.5 font-mono text-xs">
<Building2 className="text-primary size-3" />
The Grandview Collection
</Badge>
<span className="text-muted-foreground text-xs">Tower West · Residences</span>
</div>
<h1 className="text-xl font-semibold tracking-tight sm:text-2xl">
The Grandview Penthouse Collection · Floor Plan Explorer
</h1>
<p className="text-muted-foreground text-xs sm:text-sm">
Interactive architectural layout inspector, dimensional specifications, and real-time residential unit
availability.
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button
aria-label="Download attachment"
variant="outline"
size="sm"
className="h-9 gap-2 text-xs"
disabled={isDownloadingPdf}
onClick={handleDownloadPdf}
>
{!downloadSuccess ? (
<Download className="text-muted-foreground size-3.5" />
) : (
<Check className="text-success size-3.5" />
)}
<span>
{isDownloadingPdf ? 'Generating PDF...' : downloadSuccess ? 'PDF Downloaded' : 'Download PDF Floor Plan'}
</span>
</Button>
</div>
</header>
{/* Plan Selection Tabs Bar */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<Tabs value={activeTab} className="w-full sm:w-auto" onValueChange={handleSelectTab}>
<TabsList className="bg-muted/60 grid h-10 w-full grid-cols-3 gap-1 p-1 sm:w-auto">
<TabsTrigger
value="plan-a"
className="data-[state=active]:bg-background px-3 text-xs font-medium data-[state=active]:shadow-xs sm:px-4 sm:text-sm"
>
Plan A - 1 Bed / 1 Bath
</TabsTrigger>
<TabsTrigger
value="plan-b"
className="data-[state=active]:bg-background px-3 text-xs font-medium data-[state=active]:shadow-xs sm:px-4 sm:text-sm"
>
Plan B - 2 Bed / 2 Bath
</TabsTrigger>
<TabsTrigger
value="plan-c"
className="data-[state=active]:bg-background px-3 text-xs font-medium data-[state=active]:shadow-xs sm:px-4 sm:text-sm"
>
Plan C - 3 Bed Penthouse
</TabsTrigger>
</TabsList>
</Tabs>
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">Units:</span>
<div className="border-border bg-muted/30 inline-flex rounded-md border p-0.5">
<button
type="button"
className={cn(
'focus-visible:ring-ring rounded px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
unitSystem === 'imperial'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)}
onClick={() => setUnitSystem('imperial')}
>
Sq Ft / ft
</button>
<button
type="button"
className={cn(
'focus-visible:ring-ring rounded px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
unitSystem === 'metric'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)}
onClick={() => setUnitSystem('metric')}
>
m² / m
</button>
</div>
</div>
</div>
{/* 2-Column Inspector: Left 2D CAD Blueprint Canvas, Right Specs & Availability */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12">
{/* Left Column: 2D Architectural Layout Canvas (7 cols) */}
<FloorPlanCanvas
plan={currentPlan}
activeRoom={activeRoom}
unitSystem={unitSystem}
showDimensions={showDimensions}
showFurniture={showFurniture}
showPins={showPins}
selectedRoomId={selectedRoomId}
hoveredRoomId={hoveredRoomId}
onShowDimensionsChange={setShowDimensions}
onShowFurnitureChange={setShowFurniture}
onShowPinsChange={setShowPins}
onHoverRoom={setHoveredRoomId}
onSelectRoom={handleSelectRoom}
/>
{/* Right Column: Unit Specs, Room Breakdown & Available Units Table (5 cols) */}
<div className="space-y-6 lg:col-span-5">
{/* Selected Plan Header Card with Total Area */}
<Card className="border-border bg-card shadow-xs">
<CardHeader className="pb-3">
<div className="flex flex-wrap items-center justify-between">
<Badge variant="secondary" className="font-mono text-xs">
{currentPlan.tier}
</Badge>
<div className="text-muted-foreground flex items-center gap-1.5 text-xs">
<Bed className="size-3.5" />
<span>{currentPlan.beds} Bed</span>
<span>·</span>
<Bath className="size-3.5" />
<span>{currentPlan.baths} Bath</span>
</div>
</div>
<CardTitle className="mt-1 text-lg font-semibold tracking-tight sm:text-xl">
{currentPlan.marketingName}
</CardTitle>
<CardDescription className="text-xs leading-relaxed sm:text-sm">
{currentPlan.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Total Area Hero Metric Box */}
<div className="border-border bg-muted/40 rounded-lg border p-4">
<p className="text-muted-foreground text-xs font-medium">Total Architectural Living Area</p>
<div className="mt-1 flex items-baseline gap-2">
<span className="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">
{currentPlan.totalAreaSqFt.toLocaleString()}
</span>
<span className="text-muted-foreground text-sm font-medium">Sq Ft</span>
<span className="text-muted-foreground/60">/</span>
<span className="text-muted-foreground font-mono text-lg font-semibold tabular-nums">
{currentPlan.totalAreaSqM.toFixed(1)}
</span>
<span className="text-muted-foreground text-xs font-medium">m²</span>
</div>
<div className="border-border/60 text-muted-foreground mt-2 flex flex-wrap items-center justify-between border-t pt-2 text-xs">
<span>
Interior:{' '}
<strong className="text-foreground font-mono tabular-nums">{currentPlan.interiorSqFt} sq ft</strong>
</span>
<span>
Terrace:{' '}
<strong className="text-foreground font-mono tabular-nums">{currentPlan.exteriorSqFt} sq ft</strong>
</span>
</div>
</div>
{/* Room Breakdown List with Interactive Highlights */}
<div>
<div className="mb-2 flex flex-wrap items-center justify-between">
<h4 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Room Breakdown & Dimensions
</h4>
<span className="text-muted-foreground text-xs">{currentPlan.rooms.length} Spaces</span>
</div>
<div className="space-y-1.5">
{currentPlan.rooms.map((room) => (
<div
key={room.id}
className={cn(
'flex cursor-pointer items-center justify-between rounded-md border p-2.5 transition-colors duration-150',
selectedRoomId === room.id
? 'border-primary bg-primary/5 shadow-xs'
: hoveredRoomId === room.id
? 'border-border bg-muted/60'
: 'border-border/60 bg-card hover:bg-muted/30',
)}
onMouseEnter={() => setHoveredRoomId(room.id)}
onMouseLeave={() => setHoveredRoomId(null)}
onClick={() => handleSelectRoom(room.id)}
>
<div className="flex min-w-0 items-center gap-2">
<span
className={cn(
'size-2 rounded-full transition-colors',
selectedRoomId === room.id
? 'bg-primary'
: hoveredRoomId === room.id
? 'bg-primary/70'
: 'bg-muted-foreground/40',
)}
/>
<div>
<p className="text-foreground text-xs font-medium">{room.name}</p>
<p className="text-muted-foreground text-xs">{room.flooring}</p>
</div>
</div>
<div className="shrink-0 text-right">
<p className="text-foreground font-mono text-xs font-semibold tabular-nums">
{unitSystem === 'imperial' ? room.dimensionsImperial : room.dimensionsMetric}
</p>
<p className="text-muted-foreground font-mono text-xs tabular-nums">
{unitSystem === 'imperial' ? `${room.areaSqFt} sq ft` : `${room.areaSqM} m²`}
</p>
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
{/* Available Units in Building Table */}
<AvailableUnitsTable
plan={currentPlan}
selectedUnitNumber={selectedUnitNumber}
tourSuccess={showTourSuccess}
onSelectUnit={setSelectedUnitNumber}
onScheduleTour={handleScheduleTour}
/>
</div>
</div>
</div>
)
}
export default FloorPlanExplorer
'use client'
import { Home, Layers, MapPin, Ruler } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { cn } from '@/lib/utils'
import { type FloorPlanData, type RoomSpec } from './floor-plan-explorer-types'
export function FloorPlanCanvas({
plan,
activeRoom,
unitSystem,
showDimensions,
showFurniture,
showPins,
selectedRoomId,
hoveredRoomId,
onShowDimensionsChange,
onShowFurnitureChange,
onShowPinsChange,
onHoverRoom,
onSelectRoom,
}: {
plan: FloorPlanData
activeRoom: RoomSpec | null
unitSystem: 'imperial' | 'metric'
showDimensions: boolean
showFurniture: boolean
showPins: boolean
selectedRoomId: string | null
hoveredRoomId: string | null
onShowDimensionsChange: (visible: boolean) => void
onShowFurnitureChange: (visible: boolean) => void
onShowPinsChange: (visible: boolean) => void
onHoverRoom: (roomId: string | null) => void
onSelectRoom: (roomId: string) => void
}) {
return (
<>
{/* Left Column: 2D Architectural Layout Canvas (7 cols) */}
<div className="space-y-4 lg:col-span-7">
<Card className="border-border bg-card overflow-hidden shadow-xs">
{/* Canvas Toolbar Header */}
<div className="border-border bg-muted/30 flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
<div className="flex items-center gap-2.5">
<span className="relative flex size-2">
<span className="bg-primary/60 absolute inline-flex size-full rounded-full opacity-75"></span>
<span className="bg-primary relative inline-flex size-2 rounded-full"></span>
</span>
<div>
<p className="text-foreground text-xs font-medium">2D Architectural CAD Schematic</p>
<p className="text-muted-foreground text-xs">Interactive room layout & dimensional spans</p>
</div>
</div>
{/* Canvas View Options */}
<div className="flex items-center gap-1.5">
<Button
variant="ghost"
size="sm"
className={cn('h-7 gap-1 px-2 text-xs', showDimensions && 'bg-muted text-foreground')}
onClick={() => onShowDimensionsChange(!showDimensions)}
>
<Ruler className="text-muted-foreground size-3" />
<span>Dimensions</span>
</Button>
<Button
variant="ghost"
size="sm"
className={cn('h-7 gap-1 px-2 text-xs', showFurniture && 'bg-muted text-foreground')}
onClick={() => onShowFurnitureChange(!showFurniture)}
>
<Layers className="text-muted-foreground size-3" />
<span>Fixtures</span>
</Button>
<Button
variant="ghost"
size="sm"
className={cn('h-7 gap-1 px-2 text-xs', showPins && 'bg-muted text-foreground')}
onClick={() => onShowPinsChange(!showPins)}
>
<MapPin className="text-muted-foreground size-3" />
<span>Pins</span>
</Button>
</div>
</div>
{/* Canvas Drawing Surface */}
<div className="bg-card/95 relative p-3 sm:p-5">
<svg
viewBox="0 0 680 430"
className="border-border/70 bg-card w-full rounded border select-none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{/* Blueprint Architectural Grid Pattern */}
<pattern id="cad-grid-pattern-react" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" className="stroke-muted-foreground/10" strokeWidth="0.75" />
</pattern>
<pattern id="cad-grid-major-react" width="100" height="100" patternUnits="userSpaceOnUse">
<rect width="100" height="100" fill="url(#cad-grid-pattern-react)" />
<path d="M 100 0 L 0 0 0 100" fill="none" className="stroke-muted-foreground/20" strokeWidth="1.25" />
</pattern>
{/* Balcony / Terrace Wood Decking Pattern */}
<pattern
id="deck-slats-react"
width="8"
height="8"
patternUnits="userSpaceOnUse"
patternTransform="rotate(45)"
>
<line x1="0" y1="0" x2="0" y2="8" className="stroke-muted-foreground/25" strokeWidth="1.5" />
</pattern>
</defs>
{/* Blueprint Grid Background */}
<rect width="680" height="430" fill="url(#cad-grid-major-react)" />
{/* North Arrow Compass Widget */}
<g transform="translate(635, 38)" className="text-muted-foreground">
<circle cx="0" cy="0" r="18" fill="none" className="stroke-border" strokeWidth="1.2" />
<path d="M 0 -13 L 4 4 L 0 1 L -4 4 Z" className="fill-primary" />
<path d="M 0 1 L 4 4 L 0 13 L -4 4 Z" className="fill-muted-foreground/30" />
<text x="0" y="-15" textAnchor="middle" className="fill-foreground font-mono text-xs font-bold">
N
</text>
</g>
{/* Scale Indicator Bar */}
<g transform="translate(25, 412)" className="text-muted-foreground">
<line x1="0" y1="0" x2="100" y2="0" className="stroke-foreground" strokeWidth="2" />
<line x1="0" y1="-4" x2="0" y2="4" className="stroke-foreground" strokeWidth="2" />
<line x1="50" y1="-3" x2="50" y2="3" className="stroke-foreground" strokeWidth="1.5" />
<line x1="100" y1="-4" x2="100" y2="4" className="stroke-foreground" strokeWidth="2" />
<text x="0" y="-7" className="fill-muted-foreground font-mono text-xs">
0
</text>
<text x="46" y="-7" className="fill-muted-foreground font-mono text-xs">
10'
</text>
<text x="94" y="-7" className="fill-muted-foreground font-mono text-xs">
20'
</text>
<text x="115" y="3" className="fill-muted-foreground font-mono text-xs">
Scale 1/4" = 1'0"
</text>
</g>
{/* Architectural Rooms Layout Polygons */}
<g>
{plan.rooms.map((room) => (
<g
key={room.id}
className="cursor-pointer transition-colors duration-200"
onMouseEnter={() => onHoverRoom(room.id)}
onMouseLeave={() => onHoverRoom(null)}
onClick={() => onSelectRoom(room.id)}
>
{/* Room Floor Fill */}
<rect
x={room.rect.x}
y={room.rect.y}
width={room.rect.w}
height={room.rect.h}
fill={
room.id === 'balcony' || room.id === 'sky-terrace' ? 'url(#deck-slats-react)' : 'currentColor'
}
className={cn(
'transition-colors duration-150',
selectedRoomId === room.id || hoveredRoomId === room.id
? 'text-primary/15'
: room.id === 'balcony' || room.id === 'sky-terrace'
? 'text-muted/60'
: 'text-card',
)}
/>
{/* Room Border Perimeter */}
<rect
x={room.rect.x}
y={room.rect.y}
width={room.rect.w}
height={room.rect.h}
fill="none"
className={cn(
'transition-colors duration-150',
selectedRoomId === room.id
? 'stroke-primary stroke-[2.5]'
: hoveredRoomId === room.id
? 'stroke-primary/70 stroke-[2]'
: 'stroke-border/90 stroke-[1.5]',
)}
/>
{/* Architectural Fixtures Silhouette Layer */}
{showFurniture && (
<g className="stroke-muted-foreground/35 text-muted-foreground/20 pointer-events-none fill-none">
{/* Living Room: Sofa & Coffee Table */}
{(room.id === 'living' || room.id === 'grand-salon') && (
<>
<rect
x={room.rect.x + 24}
y={room.rect.y + 24}
width={75}
height={40}
rx={4}
strokeWidth={1.2}
className="stroke-muted-foreground/40"
/>
<rect
x={room.rect.x + 38}
y={room.rect.y + 72}
width={46}
height={24}
rx={2}
strokeWidth={1.2}
className="stroke-muted-foreground/30"
/>
<line
x1={room.rect.x + 130}
y1={room.rect.y + 20}
x2={room.rect.x + 130}
y2={room.rect.y + 110}
strokeDasharray="3 3"
strokeWidth={1}
className="stroke-muted-foreground/20"
/>
</>
)}
{/* Bedroom: Bed Silhouette & Pillows */}
{(room.id === 'master-bed' ||
room.id === 'guest-bed' ||
room.id === 'primary-suite' ||
room.id === 'bed-2') && (
<>
<rect
x={room.rect.x + 20}
y={room.rect.y + 25}
width={60}
height={75}
rx={3}
strokeWidth={1.2}
className="stroke-muted-foreground/40"
/>
<rect
x={room.rect.x + 26}
y={room.rect.y + 30}
width={20}
height={14}
rx={2}
strokeWidth={1.2}
className="stroke-muted-foreground/30"
/>
<rect
x={room.rect.x + 54}
y={room.rect.y + 30}
width={20}
height={14}
rx={2}
strokeWidth={1.2}
className="stroke-muted-foreground/30"
/>
<line
x1={room.rect.x + 20}
y1={room.rect.y + 55}
x2={room.rect.x + 80}
y2={room.rect.y + 55}
strokeWidth={1}
className="stroke-muted-foreground/20"
/>
</>
)}
{/* Kitchen: Island & Cooktop / Prep Sink */}
{room.id === 'kitchen' && (
<>
<rect
x={room.rect.x + 18}
y={room.rect.y + 20}
width={85}
height={36}
rx={2}
strokeWidth={1.2}
className="stroke-muted-foreground/40"
/>
<circle
cx={room.rect.x + 38}
cy={room.rect.y + 38}
r={6}
strokeWidth={1}
className="stroke-muted-foreground/40"
/>
<circle
cx={room.rect.x + 56}
cy={room.rect.y + 38}
r={6}
strokeWidth={1}
className="stroke-muted-foreground/40"
/>
<rect
x={room.rect.x + 72}
y={room.rect.y + 30}
width={18}
height={16}
rx={1}
strokeWidth={1}
className="stroke-muted-foreground/30"
/>
</>
)}
{/* Bathrooms: Tub / Shower & Vanity */}
{(room.id === 'master-bath' || room.id === 'guest-bath' || room.id === 'spa-bath') && (
<>
<rect
x={room.rect.x + 12}
y={room.rect.y + 15}
width={38}
height={50}
rx={6}
strokeWidth={1.2}
className="stroke-muted-foreground/40"
/>
<circle
cx={room.rect.x + 31}
cy={room.rect.y + 40}
r={3}
strokeWidth={1}
className="stroke-muted-foreground/40"
/>
</>
)}
{/* Balcony: Railing Post Accents */}
{(room.id === 'balcony' || room.id === 'sky-terrace') && (
<line
x1={room.rect.x}
y1={room.rect.y}
x2={room.rect.x}
y2={room.rect.y + room.rect.h}
strokeWidth={3}
className="stroke-primary/50"
/>
)}
</g>
)}
{/* Dimension Leader Line Callouts */}
{showDimensions && (
<g className="pointer-events-none select-none">
<text
x={room.pinX}
y={room.pinY + 16}
textAnchor="middle"
className="fill-muted-foreground font-mono text-xs font-medium"
>
{unitSystem === 'imperial' ? room.dimensionsImperial : room.dimensionsMetric}
</text>
</g>
)}
</g>
))}
</g>
{/* Outer Perimeter Heavy Structural Walls */}
<g className="pointer-events-none">
{/* Windows with Glass Double Thin Lines */}
<line
x1="160"
y1="75"
x2="380"
y2="75"
className="stroke-sky-400 dark:stroke-sky-300"
strokeWidth="2.5"
/>
<line
x1="390"
y1="75"
x2="540"
y2="75"
className="stroke-sky-400 dark:stroke-sky-300"
strokeWidth="2.5"
/>
<line
x1="550"
y1="75"
x2="635"
y2="75"
className="stroke-sky-400 dark:stroke-sky-300"
strokeWidth="2.5"
/>
</g>
{/* Interactive Room Highlight Pins */}
{showPins && (
<g>
{plan.rooms.map((room) => (
<g
key={`pin-${room.id}`}
transform={`translate(${room.pinX}, ${room.pinY - 8})`}
className="cursor-pointer transition-transform duration-150"
onMouseEnter={() => onHoverRoom(room.id)}
onMouseLeave={() => onHoverRoom(null)}
onClick={() => onSelectRoom(room.id)}
>
{/* Pulsing Halo on Selected */}
{(selectedRoomId === room.id || hoveredRoomId === room.id) && (
<circle cx="0" cy="0" r="16" className="fill-primary/30" />
)}
{/* Pin Badge Background Pill */}
<rect
x="-48"
y="-12"
width="96"
height="24"
rx="12"
className={cn(
'transition-colors duration-150',
selectedRoomId === room.id
? 'fill-primary stroke-primary-foreground stroke-1 shadow-md'
: hoveredRoomId === room.id
? 'fill-foreground stroke-background stroke-1'
: 'fill-card/90 stroke-border stroke-1 shadow-xs backdrop-blur-xs',
)}
/>
{/* Pin Label Text */}
<text
x="0"
y="4"
textAnchor="middle"
className={cn(
'font-mono text-xs font-semibold select-none',
selectedRoomId === room.id
? 'fill-primary-foreground'
: hoveredRoomId === room.id
? 'fill-background'
: 'fill-foreground',
)}
>
{room.shortLabel}
</text>
</g>
))}
</g>
)}
</svg>
</div>
{/* Canvas Footer Status / Active Room Info */}
<div className="border-border bg-muted/20 flex flex-wrap items-center justify-between gap-3 border-t px-4 py-3">
<div className="flex items-center gap-2">
<Badge variant="outline" className="gap-1 font-mono text-xs font-normal">
<Home className="text-primary size-3" />
Active Focus: {activeRoom?.name || 'Overview'}
</Badge>
<span className="text-muted-foreground text-xs">
{unitSystem === 'imperial' ? activeRoom?.dimensionsImperial : activeRoom?.dimensionsMetric} ·{' '}
<span className="text-foreground font-mono font-medium tabular-nums">
{unitSystem === 'imperial' ? `${activeRoom?.areaSqFt} Sq Ft` : `${activeRoom?.areaSqM} m²`}
</span>
</span>
</div>
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<span className="hidden sm:inline">Finish:</span>
<span className="text-foreground font-medium">{activeRoom?.flooring}</span>
</div>
</div>
</Card>
{/* Floor Plan Features / Highlights Strip */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-4">
<div className="border-border bg-card rounded-lg border p-3 shadow-xs">
<p className="text-muted-foreground text-xs">Ceiling Height</p>
<p className="text-foreground mt-0.5 font-mono text-sm font-semibold tabular-nums">{plan.ceilingHeight}</p>
</div>
<div className="border-border bg-card rounded-lg border p-3 shadow-xs">
<p className="text-muted-foreground text-xs">Outdoor Space</p>
<p className="text-foreground mt-0.5 font-mono text-sm font-semibold tabular-nums">
{unitSystem === 'imperial'
? `${plan.exteriorSqFt} Sq Ft`
: `${(plan.exteriorSqFt * 0.0929).toFixed(1)} m²`}
</p>
</div>
<div className="border-border bg-card rounded-lg border p-3 shadow-xs">
<p className="text-muted-foreground text-xs">Exposure</p>
<p className="text-foreground mt-0.5 text-xs font-medium">{plan.exposure}</p>
</div>
<div className="border-border bg-card rounded-lg border p-3 shadow-xs">
<p className="text-muted-foreground text-xs">Starting Rent</p>
<p className="text-foreground mt-0.5 font-mono text-sm font-semibold tabular-nums">
${plan.startingRent.toLocaleString()}
<span className="text-muted-foreground text-xs font-normal">/mo</span>
</p>
</div>
</div>
</div>
</>
)
}
'use client'
import { Calendar, CheckCircle2, FileText } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { cn } from '@/lib/utils'
import { type FloorPlanData } from './floor-plan-explorer-types'
export function AvailableUnitsTable({
plan,
selectedUnitNumber,
tourSuccess,
onSelectUnit,
onScheduleTour,
}: {
plan: FloorPlanData
selectedUnitNumber: string
tourSuccess: boolean
onSelectUnit: (unitNumber: string) => void
onScheduleTour: () => void
}) {
return (
<>
{/* Available Units in Building Table */}
<Card className="border-border bg-card shadow-xs">
<CardHeader className="pb-3">
<div className="flex flex-wrap items-center justify-between">
<div>
<CardTitle className="text-sm font-semibold tracking-tight sm:text-base">
Available Units in Building
</CardTitle>
<CardDescription className="text-xs">
Real-time occupancy and lease rates for {plan.tabLabel}
</CardDescription>
</div>
<Badge variant="outline" className="font-mono text-xs">
{plan.availableUnits.filter((u) => u.status !== 'leased').length} Available
</Badge>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[80px] text-xs font-medium">Unit</TableHead>
<TableHead className="text-xs font-medium">Floor & View</TableHead>
<TableHead className="text-right text-xs font-medium">Monthly Rent</TableHead>
<TableHead className="text-right text-xs font-medium">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{plan.availableUnits.map((unit) => (
<TableRow
key={unit.unitNumber}
className={cn(
'cursor-pointer text-xs transition-colors',
selectedUnitNumber === unit.unitNumber ? 'bg-muted/80 font-medium' : 'hover:bg-muted/40',
)}
onClick={() => onSelectUnit(unit.unitNumber)}
>
<TableCell className="text-foreground font-mono font-semibold tabular-nums">
Unit {unit.unitNumber}
</TableCell>
<TableCell>
<div className="text-foreground text-xs">{unit.view}</div>
<div className="text-muted-foreground text-xs">
Floor {unit.floor} · {unit.exposure}
</div>
</TableCell>
<TableCell className="text-foreground text-right font-mono font-semibold tabular-nums">
${unit.monthlyRent.toLocaleString()}
<span className="text-muted-foreground font-normal">/mo</span>
</TableCell>
<TableCell className="text-right">
{unit.status === 'available-now' && (
<Badge className="border-success/20 bg-success/10 text-success text-xs font-normal">
{unit.statusLabel}
</Badge>
)}
{unit.status === 'available-soon' && (
<Badge className="border-info/20 bg-info/10 text-info text-xs font-normal">
{unit.statusLabel}
</Badge>
)}
{unit.status === 'under-contract' && (
<Badge className="border-warning/20 bg-warning/10 text-warning text-xs font-normal">
{unit.statusLabel}
</Badge>
)}
{unit.status === 'leased' && (
<Badge variant="secondary" className="text-muted-foreground text-xs font-normal">
{unit.statusLabel}
</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
<CardFooter className="border-border flex flex-col gap-2.5 border-t pt-4">
<div className="flex w-full flex-col gap-2 sm:flex-row">
<Button className="w-full gap-2 text-xs sm:flex-1" onClick={onScheduleTour}>
<Calendar className="size-3.5" />
<span>Schedule Private Showing</span>
</Button>
<Button variant="outline" className="w-full gap-2 text-xs sm:flex-1" onClick={onScheduleTour}>
<FileText className="size-3.5" />
<span>Apply for Unit</span>
</Button>
</div>
{tourSuccess && (
<div className="bg-success/10 text-success flex w-full items-center gap-2 rounded-md p-2.5 text-xs">
<CheckCircle2 className="size-4 shrink-0" />
<span>
Tour request received for Unit {selectedUnitNumber}. Leasing agent will contact you within 15 minutes.
</span>
</div>
)}
<p className="text-muted-foreground text-center text-xs">
Self-guided tours and agent-accompanied showings available daily 9:00 AM – 7:00 PM.
</p>
</CardFooter>
</Card>
</>
)
}
export interface RoomSpec {
id: string
name: string
shortLabel: string
dimensionsImperial: string
dimensionsMetric: string
areaSqFt: number
areaSqM: number
flooring: string
features: string[]
pinX: number
pinY: number
rect: {
x: number
y: number
w: number
h: number
}
}
export interface AvailableUnit {
unitNumber: string
floor: number
view: string
monthlyRent: number
sqFt: number
status: 'available-now' | 'available-soon' | 'leased' | 'under-contract'
statusLabel: string
moveInDate: string
exposure: string
}
export interface FloorPlanData {
id: string
tabKey: string
tabLabel: string
marketingName: string
tier: string
beds: number
baths: number
totalAreaSqFt: number
totalAreaSqM: number
interiorSqFt: number
exteriorSqFt: number
ceilingHeight: string
exposure: string
startingRent: number
description: string
highlights: string[]
rooms: RoomSpec[]
availableUnits: AvailableUnit[]
}
Raw manifest:https://uipkge.dev/r/react/floor-plan-explorer.json