
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
List of stored payment cards using compact 3D card visuals. Marks one as the default, allows setting a different one as default, removing with a confirmation dialog, and adding a new card via an inline PaymentForm that collapses open. Emits `add`, `remove`, and `set-default` — consumer owns the persistence.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/saved-cards-list.json$npx shadcn@latest add https://uipkge.dev/r/react/saved-cards-list.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/saved-cards-list.json$bunx shadcn@latest add https://uipkge.dev/r/react/saved-cards-list.jsonnpx shadcn@latest add @uipkge-react/saved-cards-listInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | — | optional |
description | string | — | optional |
headerAction | React.ReactNode | — | optional |
onAdd | (payload: AddPayload) => void | — | optional |
className | string | — | optional |
children | React.ReactNode | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
SavedCardinterface SavedCard {
id: string
brand: CardBrand
last4: string
expiry: string
holder: string
}AddPayloadinterface AddPayload {
number: string
name: string
expiry: string
cvc: string
brand: CardBrand
}'use client'
import * as React from 'react'
import { CheckCircle2, Plus, Trash2 } from 'lucide-react'
import { PaymentCard } from '@/components/ui/payment-card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { AddCardDialog, type AddPayload, type CardBrand } from './AddCardDialog'
export type { CardBrand, AddPayload } from './AddCardDialog'
export interface SavedCard {
id: string
brand: CardBrand
last4: string
expiry: string
holder: string
}
export interface SavedCardsListProps {
title?: string
description?: string
headerAction?: React.ReactNode
onAdd?: (payload: AddPayload) => void
className?: string
children?: React.ReactNode
}
export function SavedCardsList({ title, description, headerAction, onAdd, className, children }: SavedCardsListProps) {
const [showAdd, setShowAdd] = React.useState(false)
const hasRows = React.Children.count(children) > 0
return (
<div
data-slot="saved-cards-list"
className={['bg-card text-card-foreground mx-auto w-full max-w-2xl rounded-xl border shadow-sm', className]
.filter(Boolean)
.join(' ')}
>
<div className="flex items-center justify-between border-b px-5 py-4">
<div>
<h3 className="text-base font-semibold">{title ?? 'Saved cards'}</h3>
<p className="text-muted-foreground text-xs">{description ?? 'Manage the cards used for billing.'}</p>
</div>
{headerAction ??
(onAdd ? (
<Button size="sm" onClick={() => setShowAdd((v) => !v)}>
<Plus className="size-4" /> Add card
</Button>
) : null)}
</div>
{/* Add form (collapses inline) */}
<AddCardDialog open={showAdd} onOpenChange={setShowAdd} onAdd={onAdd} />
{/* Empty state */}
{!hasRows && !showAdd ? (
<div className="px-6 py-14 text-center">
<div className="bg-muted text-muted-foreground mx-auto grid size-12 place-items-center rounded-full">
<Plus className="size-5" />
</div>
<h4 className="mt-3 text-sm font-medium">No cards saved</h4>
<p className="text-muted-foreground text-xs">Add a card to use it for future payments.</p>
{onAdd ? (
<Button size="sm" className="mt-4" onClick={() => setShowAdd(true)}>
Add your first card
</Button>
) : null}
</div>
) : (
/* List */
<ul className="divide-border divide-y">{children}</ul>
)}
</div>
)
}
function brandLabel(b: CardBrand) {
return { visa: 'Visa', mastercard: 'Mastercard', amex: 'Amex', discover: 'Discover', unknown: 'Card' }[b]
}
function maskedNumberFromLast4(last4: string, brand: CardBrand): string {
if (brand === 'amex') return `•••• •••••• •${last4}`
return `•••• •••• •••• ${last4}`
}
export interface SavedCardRowProps {
brand: CardBrand
last4: string
expiry: string
holder: string
isDefault?: boolean
onSetDefault?: () => void
onRemove?: () => void
}
export function SavedCardRow({ brand, last4, expiry, holder, isDefault, onSetDefault, onRemove }: SavedCardRowProps) {
const [confirmOpen, setConfirmOpen] = React.useState(false)
function confirmRemove() {
onRemove?.()
setConfirmOpen(false)
}
return (
<>
<li className="hover:bg-muted/30 flex items-center gap-4 px-5 py-4 transition-colors">
<PaymentCard
number={maskedNumberFromLast4(last4, brand)}
name={holder}
expiry={expiry}
brand={brand}
variant="compact"
flip={false}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">
{brandLabel(brand)} ending {last4}
</p>
{isDefault ? (
<Badge variant="secondary">
<CheckCircle2 className="size-3" /> Default
</Badge>
) : null}
</div>
<p className="text-muted-foreground text-xs">
Expires {expiry} · {holder}
</p>
</div>
<div className="flex gap-1">
{!isDefault && onSetDefault ? (
<Button variant="ghost" size="sm" onClick={onSetDefault}>
Set default
</Button>
) : null}
{onRemove ? (
<Button variant="ghost" size="icon-sm" aria-label="Remove card" onClick={() => setConfirmOpen(true)}>
<Trash2 className="text-muted-foreground size-4" />
</Button>
) : null}
</div>
</li>
{/* Remove confirmation */}
<Dialog open={confirmOpen} onOpenChange={(open) => !open && setConfirmOpen(false)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Remove this card?</DialogTitle>
<DialogDescription>
{brandLabel(brand)} ending {last4} will no longer be available for payments.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setConfirmOpen(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={confirmRemove}>
Remove
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
'use client'
import * as React from 'react'
import { PaymentForm } from '@/components/blocks/PaymentForm'
export type CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown'
export interface AddPayload {
number: string
name: string
expiry: string
cvc: string
brand: CardBrand
}
export interface AddCardDialogProps {
open: boolean
onOpenChange?: (open: boolean) => void
onAdd?: (payload: AddPayload) => void
}
export function AddCardDialog({ open, onOpenChange, onAdd }: AddCardDialogProps) {
if (!open) return null
return (
<div className="bg-muted/30 max-h-[1000px] overflow-hidden border-b p-4 opacity-100">
<PaymentForm
amount={0}
showWallets={false}
onSubmit={async (p) => {
onAdd?.(p as AddPayload)
onOpenChange?.(false)
}}
/>
</div>
)
}
Raw manifest:https://uipkge.dev/r/react/saved-cards-list.json