
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Property management resident portal with tenant header, 4 financial overview cards (monthly rent, next due date, autopay status, escrow deposit), autopay toggle with date schedule selection, and a complete 6-record payment ledger with receipt actions.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/tenant-rent-ledger.json$npx shadcn@latest add https://uipkge.dev/r/react/tenant-rent-ledger.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/tenant-rent-ledger.json$bunx shadcn@latest add https://uipkge.dev/r/react/tenant-rent-ledger.jsonnpx shadcn@latest add @uipkge-react/tenant-rent-ledgerInstalls to:components/blocks/Type aliases exported from this item's source. Use these to shape the data you pass in.
PaymentRecordinterface PaymentRecord {
id: string
period: string
description: string
dueDate: string
paidDate: string
method: string
methodType: 'ach' | 'card' | 'wire'
amount: string
status: 'paid' | 'waived'
statusLabel: string
}'use client'
import * as React from 'react'
import { useState } from 'react'
import {
Building2,
Calendar,
CheckCircle2,
Clock,
CreditCard,
Download,
Home,
Landmark,
ShieldCheck,
Zap,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
export interface PaymentRecord {
id: string
period: string
description: string
dueDate: string
paidDate: string
method: string
methodType: 'ach' | 'card' | 'wire'
amount: string
status: 'paid' | 'waived'
statusLabel: string
}
const financialCards = [
{
title: 'Monthly Base Rent',
value: '$3,450.00',
unit: '/ mo',
description: 'Fixed lease rate through Jul 2027',
icon: Home,
iconColor: 'bg-primary/10 text-primary',
},
{
title: 'Next Due Date',
value: 'Sep 01, 2026',
unit: '',
description: 'Due in 11 days · Grace period to Sep 5',
highlightBadge: 'Due in 11 days',
icon: Calendar,
iconColor: 'bg-info/10 text-info',
},
{
title: 'Autopay Status',
value: 'Active',
unit: '',
description: 'Scheduled for 1st of month with Visa •••• 4892',
icon: Zap,
iconColor: 'bg-success/10 text-success',
},
{
title: 'Security Deposit on File',
value: '$3,450.00',
unit: '',
description: 'Held in Escrow (Interest Bearing)',
icon: ShieldCheck,
iconColor: 'bg-warning/10 text-warning',
},
]
const paymentRecords: PaymentRecord[] = [
{
id: 'pay-2026-08',
period: 'August 2026 Rent',
description: 'Base Rent ($3,400.00) + High-Speed Fiber ($50.00)',
dueDate: 'Aug 01, 2026',
paidDate: 'Aug 01, 2026',
method: 'ACH Transfer · Chase •••• 4892',
methodType: 'ach',
amount: '$3,450.00',
status: 'paid',
statusLabel: 'Paid on Time',
},
{
id: 'pay-2026-07',
period: 'July 2026 Rent',
description: 'Base Rent ($3,400.00) + Assigned Parking Spot #4B ($50.00)',
dueDate: 'Jul 01, 2026',
paidDate: 'Jul 01, 2026',
method: 'ACH Transfer · Chase •••• 4892',
methodType: 'ach',
amount: '$3,450.00',
status: 'paid',
statusLabel: 'Paid on Time',
},
{
id: 'pay-2026-06',
period: 'June 2026 Rent',
description: 'Base Rent ($3,400.00) + Water & Sewer Allocation ($50.00)',
dueDate: 'Jun 01, 2026',
paidDate: 'Jun 03, 2026',
method: 'Visa •••• 4892',
methodType: 'card',
amount: '$3,450.00',
status: 'waived',
statusLabel: 'Late Fee Waived',
},
{
id: 'pay-2026-05',
period: 'May 2026 Rent',
description: 'Base Rent ($3,400.00) + Storage Locker #12 ($50.00)',
dueDate: 'May 01, 2026',
paidDate: 'May 01, 2026',
method: 'ACH Transfer · Chase •••• 4892',
methodType: 'ach',
amount: '$3,450.00',
status: 'paid',
statusLabel: 'Paid on Time',
},
{
id: 'pay-2026-04',
period: 'April 2026 Rent',
description: 'First Full Month Base Rent ($3,400.00) + Pet Rent ($50.00)',
dueDate: 'Apr 01, 2026',
paidDate: 'Apr 01, 2026',
method: 'ACH Transfer · Chase •••• 4892',
methodType: 'ach',
amount: '$3,450.00',
status: 'paid',
statusLabel: 'Paid on Time',
},
{
id: 'pay-2026-03-dep',
period: 'Security Deposit & Move-In Fee',
description: '100% Refundable Security Escrow Deposit + Key Fob Activation',
dueDate: 'Mar 15, 2026',
paidDate: 'Mar 15, 2026',
method: 'Wire Transfer · Wells Fargo •••• 1044',
methodType: 'wire',
amount: '$3,450.00',
status: 'paid',
statusLabel: 'Paid on Time',
},
]
export function TenantRentLedger({ className }: { className?: string }) {
const [autopayEnabled, setAutopayEnabled] = useState(true)
const [autopayDay, setAutopayDay] = useState<'1st' | '5th'>('1st')
return (
<div data-slot="tenant-rent-ledger" className={cn('w-full space-y-6', className)}>
{/* Resident & Unit Header */}
<div className="bg-card flex flex-col gap-4 rounded-xl border p-5 shadow-xs sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1.5">
<div className="flex flex-wrap items-center gap-2.5">
<h2 className="text-foreground text-xl font-bold tracking-tight sm:text-2xl">Elena Rostova</h2>
<Badge wrap variant="secondary" className="text-xs font-semibold">
Unit 4B
</Badge>
<Badge wrap variant="outline" className="border-success/20 bg-success/10 text-success text-xs font-medium">
<CheckCircle2 className="text-success size-3" aria-hidden="true" />
$0.00 Paid in Full
</Badge>
</div>
<div className="text-muted-foreground flex items-center gap-1.5 text-xs sm:text-sm">
<Building2 className="text-muted-foreground size-4 shrink-0" aria-hidden="true" />
<span>Pacific Heights Luxury Residences, San Francisco</span>
</div>
</div>
<div className="flex items-center gap-3 self-start sm:self-auto">
<Button className="font-medium shadow-xs">
<CreditCard className="size-4" aria-hidden="true" />
Pay Rent Now
</Button>
</div>
</div>
{/* 4 Financial Overview Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{financialCards.map((card) => {
const Icon = card.icon
return (
<Card key={card.title} className="shadow-xs">
<CardContent className="space-y-2 p-5">
<div className="flex items-center justify-between gap-2">
<p className="text-muted-foreground text-xs font-medium">{card.title}</p>
<div className={cn('flex size-8 shrink-0 items-center justify-center rounded-lg', card.iconColor)}>
<Icon className="size-4" aria-hidden="true" />
</div>
</div>
<div>
<div className="flex items-baseline gap-1">
<p className="text-foreground text-2xl font-bold tracking-tight tabular-nums">{card.value}</p>
{card.unit && <span className="text-muted-foreground text-xs font-normal">{card.unit}</span>}
</div>
<p className="text-muted-foreground mt-1 text-xs">{card.description}</p>
</div>
</CardContent>
</Card>
)
})}
</div>
{/* Autopay & Payment Method Card */}
<Card className="shadow-xs">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base font-semibold">Autopay & Payment Preferences</CardTitle>
<CardDescription className="text-xs">
Configure automatic monthly rent deduction and manage saved funding methods.
</CardDescription>
</div>
{autopayEnabled ? (
<Badge
wrap
variant="outline"
className="border-success/20 bg-success/10 text-success text-xs font-medium"
>
Autopay Active
</Badge>
) : (
<Badge wrap variant="secondary" className="text-xs font-medium">
Autopay Paused
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-5">
{/* Toggle Autopay Switch */}
<div className="bg-muted/30 flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-0.5">
<label htmlFor="autopay-toggle-react" className="text-foreground cursor-pointer text-sm font-medium">
Enable Automatic Rent Deductions
</label>
<p className="text-muted-foreground text-xs">
Automatically charge your primary payment method on your selected schedule before due date.
</p>
</div>
<Switch id="autopay-toggle-react" checked={autopayEnabled} onCheckedChange={setAutopayEnabled} />
</div>
<Separator />
{/* Primary Bank Account / Card */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Primary Payment Method
</h4>
<Button variant="ghost" size="xs" className="text-primary hover:text-primary text-xs font-medium">
Change Method
</Button>
</div>
<div className="flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3.5">
<div className="bg-background flex size-10 shrink-0 items-center justify-center rounded-lg border shadow-xs">
<CreditCard className="text-foreground size-5" aria-hidden="true" />
</div>
<div className="space-y-0.5">
<div className="flex items-center gap-2">
<span className="text-foreground text-sm font-semibold tabular-nums">Visa •••• 4892</span>
<Badge wrap variant="secondary" className="text-xs font-normal">
Default
</Badge>
</div>
<p className="text-muted-foreground text-xs">Elena Rostova · Expires 08/28 · Bank of America</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge wrap variant="outline" className="border-success/20 bg-success/10 text-success text-xs">
<CheckCircle2 className="text-success size-3" aria-hidden="true" />
Verified for ACH & Debit
</Badge>
</div>
</div>
</div>
<Separator />
{/* Autopay Date Selector (1st vs 5th) */}
<div className="space-y-3">
<div className="space-y-0.5">
<h4 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Scheduled Deduction Date
</h4>
<p className="text-muted-foreground text-xs">
Select when rent will be debited from your account each cycle.
</p>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<button
type="button"
disabled={!autopayEnabled}
className={cn(
'focus-visible:ring-ring flex cursor-pointer items-start gap-3 rounded-lg border p-3.5 text-left transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
autopayDay === '1st' && autopayEnabled
? 'border-primary bg-primary/5 ring-primary ring-1'
: 'border-border hover:bg-muted/50',
)}
onClick={() => setAutopayDay('1st')}
>
<div
className={cn(
'mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border transition-colors',
autopayDay === '1st' && autopayEnabled
? 'border-primary bg-primary text-primary-foreground'
: 'border-muted-foreground/50',
)}
>
{autopayDay === '1st' && autopayEnabled && <span className="bg-background size-1.5 rounded-full" />}
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-foreground text-sm font-medium">1st of the month</span>
<Badge
wrap
variant="outline"
className="border-success/20 bg-success/10 text-success text-xs font-medium"
>
Recommended
</Badge>
</div>
<p className="text-muted-foreground text-xs">
Debited promptly on the rent due date. Ensures zero delinquency risk.
</p>
</div>
</button>
<button
type="button"
disabled={!autopayEnabled}
className={cn(
'focus-visible:ring-ring flex cursor-pointer items-start gap-3 rounded-lg border p-3.5 text-left transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
autopayDay === '5th' && autopayEnabled
? 'border-primary bg-primary/5 ring-primary ring-1'
: 'border-border hover:bg-muted/50',
)}
onClick={() => setAutopayDay('5th')}
>
<div
className={cn(
'mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border transition-colors',
autopayDay === '5th' && autopayEnabled
? 'border-primary bg-primary text-primary-foreground'
: 'border-muted-foreground/50',
)}
>
{autopayDay === '5th' && autopayEnabled && <span className="bg-background size-1.5 rounded-full" />}
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-foreground text-sm font-medium">5th of the month</span>
<Badge wrap variant="secondary" className="text-xs font-normal">
Grace Period
</Badge>
</div>
<p className="text-muted-foreground text-xs">
Processes within the standard 5-day building grace window before late fees.
</p>
</div>
</button>
</div>
</div>
</CardContent>
</Card>
{/* Payment History Ledger Table */}
<div className="space-y-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 className="text-foreground text-base font-semibold tracking-tight">Payment History & Ledger</h3>
<p className="text-muted-foreground text-xs">
Complete transaction record of monthly rent charges, utility allocations, and security deposits.
</p>
</div>
<Button aria-label="Download attachment" variant="outline" size="sm" className="self-start sm:self-auto">
<Download className="size-4" aria-hidden="true" />
Download Statement (PDF)
</Button>
</div>
<div className="bg-card overflow-x-auto rounded-lg border shadow-xs">
<Table>
<TableHeader>
<TableRow>
<TableHead>Billing Period & Description</TableHead>
<TableHead>Due Date</TableHead>
<TableHead>Paid Date</TableHead>
<TableHead>Payment Method</TableHead>
<TableHead className="text-right">Amount Paid</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paymentRecords.map((record) => (
<TableRow key={record.id} className="hover:bg-muted/50">
<TableCell>
<div className="text-foreground text-sm font-medium">{record.period}</div>
<div className="text-muted-foreground text-xs">{record.description}</div>
</TableCell>
<TableCell className="text-muted-foreground text-sm tabular-nums">{record.dueDate}</TableCell>
<TableCell className="text-foreground text-sm font-medium tabular-nums">{record.paidDate}</TableCell>
<TableCell>
<div className="text-foreground flex items-center gap-1.5 text-xs sm:text-sm">
{record.methodType === 'ach' && (
<Landmark className="text-muted-foreground size-3.5 shrink-0" aria-hidden="true" />
)}
{record.methodType === 'card' && (
<CreditCard className="text-muted-foreground size-3.5 shrink-0" aria-hidden="true" />
)}
{record.methodType === 'wire' && (
<ShieldCheck className="text-muted-foreground size-3.5 shrink-0" aria-hidden="true" />
)}
<span className="truncate">{record.method}</span>
</div>
</TableCell>
<TableCell className="text-foreground text-right text-sm font-bold tabular-nums">
{record.amount}
</TableCell>
<TableCell>
{record.status === 'paid' ? (
<Badge
wrap
variant="outline"
className="border-success/20 bg-success/10 text-success text-xs font-medium"
>
<CheckCircle2 className="text-success size-3" aria-hidden="true" />
{record.statusLabel}
</Badge>
) : (
<Badge wrap variant="outline" className="border-info/20 bg-info/10 text-info text-xs font-medium">
<Clock className="text-info size-3" aria-hidden="true" />
{record.statusLabel}
</Badge>
)}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground h-8 gap-1.5 text-xs"
aria-label="Download receipt PDF"
>
<Download className="size-3.5" aria-hidden="true" />
<span>Receipt</span>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="text-muted-foreground flex flex-col gap-3 pt-1 text-xs sm:flex-row sm:items-center sm:justify-between">
<p>
Showing <span className="text-foreground font-medium">6</span> of{' '}
<span className="text-foreground font-medium">6</span> lease payments · Lease Ref:{' '}
<span className="font-mono">PH-4B-2026</span>
</p>
<div className="flex items-center gap-2">
<Button variant="outline" size="xs" disabled>
Previous
</Button>
<Button variant="outline" size="xs" disabled>
Next
</Button>
</div>
</div>
</div>
</div>
)
}
Raw manifest:https://uipkge.dev/r/react/tenant-rent-ledger.json