Gift Card Balance Checker
blockcommerceCustomer storefront gift card balance lookup, live digital card passbook, quick fund reloading, and complete redemption audit history ledger.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/gift-card-balance-checker.json$npx shadcn@latest add https://uipkge.dev/r/react/gift-card-balance-checker.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/gift-card-balance-checker.json$bunx shadcn@latest add https://uipkge.dev/r/react/gift-card-balance-checker.jsonNamed registry:
npx shadcn@latest add @uipkge-react/gift-card-balance-checkerInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
className | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
TransactionRecordinterface TransactionRecord {
id: string
date: string
reference: string
title: string
subtitle?: string
type: 'activation' | 'redemption' | 'reload'
amount: number
balanceAfter: number
status: 'Completed' | 'Pending'
}Files installed (1)
components/blocks/GiftCardBalanceChecker.tsx29.4 kB
'use client' import * as React from 'react' import { useMemo } from 'react' import { ArrowDownLeft, ArrowUpRight, Check, CheckCircle2, Copy, CreditCard, Gift, HelpCircle, Lock, Plus, RefreshCw, ShieldCheck, ShoppingBag, Wallet, } 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 { Input } from '@/components/ui/input' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' export interface GiftCardBalanceCheckerProps { className?: string } interface TransactionRecord { id: string date: string reference: string title: string subtitle?: string type: 'activation' | 'redemption' | 'reload' amount: number balanceAfter: number status: 'Completed' | 'Pending' } const initialTransactions: TransactionRecord[] = [ { id: 'tx-1', date: 'Dec 19, 2024', reference: '#ORD-84112', title: 'Store Checkout Redemption', subtitle: 'Order #ORD-84112 · Leather Travel Wallet', type: 'redemption', amount: -15.0, balanceAfter: 150.0, status: 'Completed', }, { id: 'tx-2', date: 'Nov 04, 2024', reference: '#ORD-84920', title: 'Store Checkout Redemption', subtitle: 'Order #ORD-84920 · Merino Knit Sweater', type: 'redemption', amount: -35.0, balanceAfter: 165.0, status: 'Completed', }, { id: 'tx-3', date: 'Oct 12, 2024', reference: '#ACT-99014', title: 'Initial Activation & Fund Issue', subtitle: 'Digital Storefront Pass Issued', type: 'activation', amount: 200.0, balanceAfter: 200.0, status: 'Completed', }, ] export function GiftCardBalanceChecker({ className }: GiftCardBalanceCheckerProps) { const [cardNumber, setCardNumber] = React.useState('7482 - 9104 - 6382 - 8492') const [securityPin, setSecurityPin] = React.useState('4829') const [isChecking, setIsChecking] = React.useState(false) const [isVerified, setIsVerified] = React.useState(true) const [copied, setCopied] = React.useState(false) const [applied, setApplied] = React.useState(false) const [feedbackMessage, setFeedbackMessage] = React.useState<string | null>(null) const [filterType, setFilterType] = React.useState<'all' | 'redemptions' | 'loads'>('all') const [transactions, setTransactions] = React.useState<TransactionRecord[]>(initialTransactions) function formatCardInput(raw: string): string { const digits = raw.replace(/\D/g, '').slice(0, 16) return digits.replace(/(.{4})(?=.)/g, '$1 - ') } function handleCardNumberChange(e: React.ChangeEvent<HTMLInputElement>) { const formatted = formatCardInput(e.target.value) setCardNumber(formatted) } function handlePinChange(e: React.ChangeEvent<HTMLInputElement>) { const digits = e.target.value.replace(/\D/g, '').slice(0, 4) setSecurityPin(digits) } const currentBalance = useMemo(() => { if (transactions.length === 0) return 0 return transactions[0].balanceAfter }, [transactions]) const totalLoaded = useMemo(() => { return transactions.filter((t) => t.amount > 0).reduce((sum, t) => sum + t.amount, 0) }, [transactions]) const totalSpent = useMemo(() => { return Math.abs(transactions.filter((t) => t.amount < 0).reduce((sum, t) => sum + t.amount, 0)) }, [transactions]) const lastFourDigits = useMemo(() => { const digits = cardNumber.replace(/\D/g, '') return digits.length >= 4 ? digits.slice(-4) : '8492' }, [cardNumber]) const filteredTransactions = useMemo(() => { if (filterType === 'redemptions') { return transactions.filter((t) => t.type === 'redemption') } if (filterType === 'loads') { return transactions.filter((t) => t.type === 'activation' || t.type === 'reload') } return transactions }, [transactions, filterType]) function checkBalance() { setIsChecking(true) setFeedbackMessage(null) setTimeout(() => { setIsChecking(false) setIsVerified(true) setFeedbackMessage(`Gift card verified. Current active balance is $${currentBalance.toFixed(2)}.`) setTimeout(() => { setFeedbackMessage(null) }, 4000) }, 500) } function copyCardNumber() { const raw = cardNumber.replace(/\s+/g, '') if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard.writeText(raw) } setCopied(true) setTimeout(() => { setCopied(false) }, 2000) } function applyToOrder() { copyCardNumber() setApplied(true) setFeedbackMessage(`Card ending in ${lastFourDigits} copied and applied to your checkout session!`) setTimeout(() => { setApplied(false) setFeedbackMessage(null) }, 4000) } function reloadFunds(amount: number) { const newBalance = currentBalance + amount const newRecord: TransactionRecord = { id: `tx-${Date.now()}`, date: 'Today', reference: `#RLD-${Math.floor(10000 + Math.random() * 90000)}`, title: 'Manual Online Fund Reload', subtitle: `Instant Online Top-Up (+ $${amount.toFixed(2)})`, type: 'reload', amount: amount, balanceAfter: newBalance, status: 'Completed', } setTransactions((prev) => [newRecord, ...prev]) setFeedbackMessage(`Successfully reloaded +$${amount.toFixed(2)}! New card balance: $${newBalance.toFixed(2)}.`) setTimeout(() => { setFeedbackMessage(null) }, 4000) } return ( <div data-slot="gift-card-balance-checker" className={cn('w-full space-y-6', className)}> {/* Header Section */} <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div className="space-y-1"> <div className="flex items-center gap-2"> <h2 className="text-foreground text-2xl font-bold tracking-tight">Check Gift Card Balance</h2> <Badge variant="outline" className="bg-primary/5 text-primary border-primary/20 gap-1"> <CheckCircle2 className="text-primary size-3" /> Storefront Pass </Badge> </div> <p className="text-muted-foreground text-sm"> Check your remaining card balance, review past order redemptions, or reload funds. </p> </div> <div className="flex items-center gap-2"> <TooltipProvider delayDuration={150}> <Tooltip> <TooltipTrigger asChild> <div className="border-border bg-card text-muted-foreground hover:text-foreground flex cursor-pointer items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs shadow-xs"> <HelpCircle className="text-primary size-3.5" /> <span>PIN & Card Guide</span> </div> </TooltipTrigger> <TooltipContent side="bottom" className="max-w-xs text-xs"> Locate the 16-digit card code on the back of your physical card or inside your digital gift email. The 4-digit security PIN is under the scratch-off foil. </TooltipContent> </Tooltip> </TooltipProvider> </div> </div> {/* Feedback Banner if active */} {feedbackMessage && ( <div className="border-success/20 bg-success/10 text-success flex items-center justify-between rounded-lg border px-4 py-2.5 text-sm"> <div className="flex items-center gap-2"> <CheckCircle2 className="text-success size-4 shrink-0" /> <span>{feedbackMessage}</span> </div> </div> )} {/* 2-Column Storefront Layout */} <div className="grid grid-cols-1 gap-6 lg:grid-cols-12"> {/* Left Column: Lookup Form & Digital Card Hero */} <div className="space-y-6 lg:col-span-5"> {/* Lookup Card Form */} <Card className="border-border shadow-xs"> <CardHeader className="pb-4"> <div className="flex items-center justify-between"> <CardTitle className="text-base font-semibold">Card Lookup</CardTitle> {isVerified && ( <Badge variant="outline" className="border-success/30 bg-success/10 text-success gap-1 text-xs"> <Check className="size-3" /> Active Card </Badge> )} </div> <CardDescription className="text-xs"> Enter your gift card credentials to view live balance and activity. </CardDescription> </CardHeader> <CardContent className="space-y-4 pb-4"> {/* 16-Digit Card Number Input */} <div className="space-y-1.5"> <div className="flex items-center justify-between"> <label htmlFor="gc-number-input-react" className="text-foreground text-xs font-medium"> 16-Digit Card Number </label> <TooltipProvider delayDuration={150}> <Tooltip> <TooltipTrigger asChild> <button type="button" className="text-muted-foreground hover:text-foreground inline-flex items-center focus-visible:outline-none" aria-label="Gift card number info" > <HelpCircle className="size-3.5" /> </button> </TooltipTrigger> <TooltipContent side="top" className="max-w-xs text-xs"> Enter the 16 digits on the back of your card or in your receipt email. </TooltipContent> </Tooltip> </TooltipProvider> </div> <Input id="gc-number-input-react" value={cardNumber} placeholder="XXXX - XXXX - XXXX - XXXX" maxLength={25} className="font-mono text-sm" prefixIcon={<CreditCard className="size-4" />} onChange={handleCardNumberChange} /> </div> {/* 4-Digit Security PIN Input */} <div className="space-y-1.5"> <div className="flex items-center justify-between"> <label htmlFor="gc-pin-input-react" className="text-foreground text-xs font-medium"> Security PIN (4 digits) </label> <TooltipProvider delayDuration={150}> <Tooltip> <TooltipTrigger asChild> <button type="button" className="text-muted-foreground hover:text-foreground inline-flex items-center focus-visible:outline-none" aria-label="Security PIN info" > <HelpCircle className="size-3.5" /> </button> </TooltipTrigger> <TooltipContent side="top" className="max-w-xs text-xs"> The 4-digit PIN is revealed by scratching the silver foil or listed in your digital claim email. </TooltipContent> </Tooltip> </TooltipProvider> </div> <Input id="gc-pin-input-react" value={securityPin} type="password" placeholder="••••" maxLength={4} showPasswordToggle className="font-mono text-sm" prefixIcon={<Lock className="size-4" />} onChange={handlePinChange} /> </div> <Button type="button" className="w-full font-medium" disabled={isChecking} onClick={checkBalance}> {isChecking ? ( <> <RefreshCw className="mr-2 size-4 animate-spin" /> Verifying Balance... </> ) : ( <> <ShieldCheck className="mr-2 size-4" /> Check Balance </> )} </Button> </CardContent> </Card> {/* Stylized Digital Gift Card Hero Display */} <div className="relative overflow-hidden rounded-2xl border border-white/10 bg-gradient-to-br from-zinc-950 via-neutral-900 to-zinc-900 p-6 text-white shadow-lg"> <div className="relative flex flex-col justify-between space-y-6"> {/* Card Top Bar: Brand & Pass Badge */} <div className="flex items-start justify-between"> <div className="flex items-center gap-2.5"> <div className="flex size-9 items-center justify-center rounded-xl border border-white/10 bg-white/10 text-white shadow-inner backdrop-blur-md"> <Gift className="text-success size-5" /> </div> <div> <p className="text-xs font-bold tracking-wider text-neutral-200 uppercase">Lumen Store</p> <p className="text-xs text-neutral-400">Digital Gift Pass</p> </div> </div> <Badge variant="outline" className="gap-1.5 border-white/20 bg-white/10 px-2.5 py-0.5 text-xs text-white backdrop-blur-md" > <span className="bg-success size-1.5 animate-pulse rounded-full" /> Verified Active </Badge> </div> {/* Card Center: Chip Icon & Balance Display */} <div className="space-y-1 pt-1"> <div className="flex items-center justify-between"> <span className="text-xs font-medium tracking-wider text-neutral-400 uppercase"> Available Balance </span> {/* Chip graphic representation */} <div className="flex items-center gap-1 opacity-70"> <div className="border-warning/20/40 h-5 w-7 rounded-sm border bg-gradient-to-tr from-amber-400/20 to-amber-200/40" /> <span className="font-mono text-xs tracking-tighter text-neutral-400">NFC</span> </div> </div> <div className="flex items-baseline gap-2"> <span className="text-3xl font-bold tracking-tight text-white tabular-nums sm:text-4xl"> ${currentBalance.toFixed(2)} </span> <span className="text-success text-xs font-medium">USD</span> </div> </div> {/* Card Footer: Card Number & Expiry terms */} <div className="space-y-2 border-t border-white/10 pt-4"> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <span className="font-mono text-sm tracking-widest text-neutral-200"> •••• •••• •••• {lastFourDigits} </span> <button type="button" className="rounded p-1 text-neutral-400 transition-colors hover:text-white focus-visible:ring-1 focus-visible:ring-white/40 focus-visible:outline-none" aria-label="Copy card number" onClick={copyCardNumber} > {copied ? <Check className="text-success size-3.5" /> : <Copy className="size-3.5" />} </button> </div> {copied && <span className="animate-in fade-in text-success text-xs font-medium">Copied!</span>} </div> <div className="flex items-center justify-between text-xs text-neutral-400"> <span>Never Expires · No Inactivity Fees</span> <span className="font-mono text-neutral-500">PIN: ••••</span> </div> </div> </div> </div> {/* Quick Action Buttons */} <div className="space-y-3"> <Button variant="default" className="w-full font-medium" onClick={applyToOrder}> {applied ? ( <> <Check className="text-success mr-2 size-4" /> Applied to Checkout! </> ) : ( <> <ShoppingBag className="mr-2 size-4" /> Apply to Next Order </> )} </Button> {/* Quick Reload Card */} <Card className="border-border shadow-xs"> <CardHeader className="p-4 pb-2"> <div className="flex items-center justify-between"> <CardTitle className="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Quick Reload Funds </CardTitle> <Plus className="text-muted-foreground size-3.5" /> </div> </CardHeader> <CardContent className="p-4 pt-0"> <p className="text-muted-foreground mb-3 text-xs"> Top up your gift card instantly with a saved payment method. </p> <div className="grid grid-cols-3 gap-2"> <Button variant="outline" size="sm" className="hover:bg-primary/5 hover:text-primary hover:border-primary/30 w-full font-medium tabular-nums" onClick={() => reloadFunds(25)} > + $25.00 </Button> <Button variant="outline" size="sm" className="hover:bg-primary/5 hover:text-primary hover:border-primary/30 w-full font-medium tabular-nums" onClick={() => reloadFunds(50)} > + $50.00 </Button> <Button variant="outline" size="sm" className="hover:bg-primary/5 hover:text-primary hover:border-primary/30 w-full font-medium tabular-nums" onClick={() => reloadFunds(100)} > + $100.00 </Button> </div> </CardContent> </Card> </div> </div> {/* Right Column: Balance Stats & Redemption History Table */} <div className="space-y-6 lg:col-span-7"> {/* Passbook Summary Metrics */} <div className="grid grid-cols-1 gap-3 sm:grid-cols-3"> <div className="border-border bg-card rounded-xl border p-4 shadow-xs"> <div className="text-muted-foreground mb-1 flex items-center justify-between text-xs"> <span>Available Balance</span> <Wallet className="text-primary size-3.5" /> </div> <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums"> ${currentBalance.toFixed(2)} </p> <p className="text-success mt-1 flex items-center gap-1 text-xs font-medium"> <Check className="size-3" /> Ready to spend </p> </div> <div className="border-border bg-card rounded-xl border p-4 shadow-xs"> <div className="text-muted-foreground mb-1 flex items-center justify-between text-xs"> <span>Total Value Loaded</span> <ArrowDownLeft className="text-success size-3.5" /> </div> <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums"> ${totalLoaded.toFixed(2)} </p> <p className="text-muted-foreground mt-1 text-xs">All top-ups & issue</p> </div> <div className="border-border bg-card rounded-xl border p-4 shadow-xs"> <div className="text-muted-foreground mb-1 flex items-center justify-between text-xs"> <span>Total Redeemed</span> <ArrowUpRight className="text-muted-foreground size-3.5" /> </div> <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums"> -${totalSpent.toFixed(2)} </p> <p className="text-muted-foreground mt-1 text-xs">Past store orders</p> </div> </div> {/* Redemption & Activity History Card */} <Card className="border-border shadow-xs"> <CardHeader className="pb-3"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <CardTitle className="text-base font-semibold">Redemption & Activity History</CardTitle> <CardDescription className="text-xs"> Detailed ledger of card issuance, promotional top-ups, and store checkouts. </CardDescription> </div> {/* Filter Pills */} <div className="border-border bg-muted/40 flex items-center gap-1.5 rounded-lg border p-1 text-xs"> <button type="button" className={cn( 'rounded px-2.5 py-1 font-medium transition-colors', filterType === 'all' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setFilterType('all')} > All ({transactions.length}) </button> <button type="button" className={cn( 'rounded px-2.5 py-1 font-medium transition-colors', filterType === 'redemptions' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setFilterType('redemptions')} > Redemptions </button> <button type="button" className={cn( 'rounded px-2.5 py-1 font-medium transition-colors', filterType === 'loads' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setFilterType('loads')} > Loads </button> </div> </div> </CardHeader> <CardContent className="p-0"> <div className="overflow-x-auto"> <Table> <TableHeader> <TableRow> <TableHead className="w-[110px]">Date</TableHead> <TableHead>Activity & Reference</TableHead> <TableHead className="w-[90px] text-center">Type</TableHead> <TableHead className="w-[100px] text-right">Amount</TableHead> <TableHead className="w-[110px] text-right">Resulting Balance</TableHead> </TableRow> </TableHeader> <TableBody> {filteredTransactions.map((record) => ( <TableRow key={record.id}> <TableCell className="text-muted-foreground text-xs font-medium">{record.date}</TableCell> <TableCell> <div className="space-y-0.5"> <p className="text-foreground text-sm leading-none font-medium">{record.title}</p> {record.subtitle && <p className="text-muted-foreground text-xs">{record.subtitle}</p>} </div> </TableCell> <TableCell className="text-center"> {record.type === 'activation' && ( <Badge variant="outline" className="border-success/30 bg-success/10 text-success px-2 py-0.5 text-xs font-normal" > Activation </Badge> )} {record.type === 'reload' && ( <Badge variant="outline" className="border-info/30 bg-info/10 text-info px-2 py-0.5 text-xs font-normal" > Reload </Badge> )} {record.type === 'redemption' && ( <Badge variant="secondary" className="px-2 py-0.5 text-xs font-normal"> Redemption </Badge> )} </TableCell> <TableCell className={cn( 'text-right text-sm font-medium tabular-nums', record.amount > 0 ? 'text-success' : 'text-foreground', )} > {record.amount > 0 ? `+$${record.amount.toFixed(2)}` : `-$${Math.abs(record.amount).toFixed(2)}`} </TableCell> <TableCell className="text-muted-foreground text-right font-mono text-sm tabular-nums"> ${record.balanceAfter.toFixed(2)} </TableCell> </TableRow> ))} </TableBody> </Table> </div> </CardContent> <CardFooter className="border-border text-muted-foreground flex flex-col gap-3 border-t p-4 text-xs sm:flex-row sm:items-center sm:justify-between"> <div className="flex items-center gap-1.5"> <ShieldCheck className="text-primary size-4 shrink-0" /> <span>Protected by 256-bit encryption · Zero liability policy</span> </div> <div className="flex items-center gap-3"> <button type="button" className="hover:text-foreground flex min-h-6 items-center gap-1 underline-offset-4 hover:underline" onClick={checkBalance} > <RefreshCw className="size-3" /> Refresh Ledger </button> </div> </CardFooter> </Card> {/* Digital Passbook Barcode Card */} <Card className="border-border bg-muted/20 shadow-xs"> <CardContent className="flex flex-col items-center justify-between gap-4 p-4 sm:flex-row sm:p-5"> <div className="space-y-1 text-center sm:text-left"> <h4 className="text-foreground flex items-center justify-center gap-2 text-sm font-semibold sm:justify-start"> <Wallet className="text-primary size-4" /> In-Store Digital Pass </h4> <p className="text-muted-foreground max-w-sm text-xs"> Present this barcode at checkout in any physical store location to scan and redeem card funds. </p> </div> {/* Barcode Simulation */} <div className="border-border bg-card flex flex-col items-center gap-1.5 rounded-lg border p-3 shadow-xs"> <div className="flex h-10 items-end gap-[3px] px-2" aria-hidden="true"> <div className="bg-foreground h-full w-[2px]" /> <div className="bg-foreground h-full w-[4px]" /> <div className="bg-foreground h-full w-[1px]" /> <div className="bg-foreground h-full w-[3px]" /> <div className="bg-foreground h-full w-[2px]" /> <div className="bg-foreground h-full w-[5px]" /> <div className="bg-foreground h-full w-[1px]" /> <div className="bg-foreground h-full w-[3px]" /> <div className="bg-foreground h-full w-[2px]" /> <div className="bg-foreground h-full w-[4px]" /> <div className="bg-foreground h-full w-[2px]" /> <div className="bg-foreground h-full w-[1px]" /> <div className="bg-foreground h-full w-[4px]" /> <div className="bg-foreground h-full w-[2px]" /> <div className="bg-foreground h-full w-[3px]" /> </div> <span className="text-muted-foreground font-mono text-xs tracking-widest">{cardNumber}</span> </div> </CardContent> </Card> </div> </div> </div> ) } export default GiftCardBalanceChecker
Raw manifest:https://uipkge.dev/r/react/gift-card-balance-checker.json