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-vue@latest add https://uipkge.dev/r/vue/gift-card-balance-checker.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/gift-card-balance-checker.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/gift-card-balance-checker.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/gift-card-balance-checker.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/gift-card-balance-checkerInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | 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)
app/components/blocks/GiftCardBalanceChecker.vue27.3 kB
<script setup lang="ts"> import { computed, ref } from 'vue' import type { HTMLAttributes } from 'vue' import { ArrowDownLeft, ArrowUpRight, Check, CheckCircle2, Copy, CreditCard, Gift, HelpCircle, Lock, Plus, RefreshCw, ShieldCheck, ShoppingBag, Wallet, } from 'lucide-vue-next' 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' interface Props { class?: HTMLAttributes['class'] } const props = defineProps<Props>() 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', }, ] const cardNumber = ref('7482 - 9104 - 6382 - 8492') const securityPin = ref('4829') const isChecking = ref(false) const isVerified = ref(true) const copied = ref(false) const applied = ref(false) const feedbackMessage = ref<string | null>(null) const filterType = ref<'all' | 'redemptions' | 'loads'>('all') const transactions = ref<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: Event) { const target = e.target as HTMLInputElement const formatted = formatCardInput(target.value) cardNumber.value = formatted target.value = formatted } function handlePinChange(e: Event) { const target = e.target as HTMLInputElement const digits = target.value.replace(/\D/g, '').slice(0, 4) securityPin.value = digits target.value = digits } const currentBalance = computed(() => { if (transactions.value.length === 0) return 0 return transactions.value[0].balanceAfter }) const totalLoaded = computed(() => { return transactions.value.filter((t) => t.amount > 0).reduce((sum, t) => sum + t.amount, 0) }) const totalSpent = computed(() => { return Math.abs(transactions.value.filter((t) => t.amount < 0).reduce((sum, t) => sum + t.amount, 0)) }) const lastFourDigits = computed(() => { const digits = cardNumber.value.replace(/\D/g, '') return digits.length >= 4 ? digits.slice(-4) : '8492' }) const filteredTransactions = computed(() => { if (filterType.value === 'redemptions') { return transactions.value.filter((t) => t.type === 'redemption') } if (filterType.value === 'loads') { return transactions.value.filter((t) => t.type === 'activation' || t.type === 'reload') } return transactions.value }) function checkBalance() { isChecking.value = true feedbackMessage.value = null setTimeout(() => { isChecking.value = false isVerified.value = true feedbackMessage.value = `Gift card verified. Current active balance is $${currentBalance.value.toFixed(2)}.` setTimeout(() => { feedbackMessage.value = null }, 4000) }, 500) } function copyCardNumber() { const raw = cardNumber.value.replace(/\s+/g, '') if (navigator?.clipboard) { navigator.clipboard.writeText(raw) } copied.value = true setTimeout(() => { copied.value = false }, 2000) } function applyToOrder() { copyCardNumber() applied.value = true feedbackMessage.value = `Card ending in ${lastFourDigits.value} copied and applied to your checkout session!` setTimeout(() => { applied.value = false feedbackMessage.value = null }, 4000) } function reloadFunds(amount: number) { const newBalance = currentBalance.value + 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', } transactions.value = [newRecord, ...transactions.value] feedbackMessage.value = `Successfully reloaded +$${amount.toFixed(2)}! New card balance: $${newBalance.toFixed(2)}.` setTimeout(() => { feedbackMessage.value = null }, 4000) } </script> <template> <div data-slot="gift-card-balance-checker" :class="cn('w-full space-y-6', props.class)"> <!-- Header Section --> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <h2 class="text-foreground text-2xl font-bold tracking-tight">Check Gift Card Balance</h2> <Badge variant="outline" class="bg-primary/5 text-primary border-primary/20 gap-1"> <CheckCircle2 class="text-primary size-3" /> Storefront Pass </Badge> </div> <p class="text-muted-foreground text-sm"> Check your remaining card balance, review past order redemptions, or reload funds. </p> </div> <div class="flex items-center gap-2"> <TooltipProvider :delay-duration="150"> <Tooltip> <TooltipTrigger as-child> <div class="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 class="text-primary size-3.5" /> <span>PIN & Card Guide</span> </div> </TooltipTrigger> <TooltipContent side="bottom" class="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 --> <div v-if="feedbackMessage" class="border-success/20 bg-success/10 text-success flex items-center justify-between rounded-lg border px-4 py-2.5 text-sm" > <div class="flex items-center gap-2"> <CheckCircle2 class="text-success size-4 shrink-0" /> <span>{{ feedbackMessage }}</span> </div> </div> <!-- 2-Column Storefront Layout --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-12"> <!-- Left Column: Lookup Form & Digital Card Hero --> <div class="space-y-6 lg:col-span-5"> <!-- Lookup Card Form --> <Card class="border-border shadow-xs"> <CardHeader class="pb-4"> <div class="flex items-center justify-between"> <CardTitle class="text-base font-semibold">Card Lookup</CardTitle> <Badge v-if="isVerified" variant="outline" class="border-success/30 bg-success/10 text-success gap-1 text-xs" > <Check class="size-3" /> Active Card </Badge> </div> <CardDescription class="text-xs"> Enter your gift card credentials to view live balance and activity. </CardDescription> </CardHeader> <CardContent class="space-y-4 pb-4"> <!-- 16-Digit Card Number Input --> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <label for="gc-number-input" class="text-foreground text-xs font-medium"> 16-Digit Card Number </label> <TooltipProvider :delay-duration="150"> <Tooltip> <TooltipTrigger as-child> <button type="button" class="text-muted-foreground hover:text-foreground inline-flex items-center focus-visible:outline-none" aria-label="Gift card number info" > <HelpCircle class="size-3.5" /> </button> </TooltipTrigger> <TooltipContent side="top" class="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" :model-value="cardNumber" placeholder="XXXX - XXXX - XXXX - XXXX" maxlength="25" class="font-mono text-sm" :prefix-icon="CreditCard" @input="handleCardNumberChange" /> </div> <!-- 4-Digit Security PIN Input --> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <label for="gc-pin-input" class="text-foreground text-xs font-medium"> Security PIN (4 digits) </label> <TooltipProvider :delay-duration="150"> <Tooltip> <TooltipTrigger as-child> <button type="button" class="text-muted-foreground hover:text-foreground inline-flex items-center focus-visible:outline-none" aria-label="Security PIN info" > <HelpCircle class="size-3.5" /> </button> </TooltipTrigger> <TooltipContent side="top" class="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" :model-value="securityPin" type="password" placeholder="••••" maxlength="4" show-password-toggle class="font-mono text-sm" :prefix-icon="Lock" @input="handlePinChange" /> </div> <Button type="button" class="w-full font-medium" :disabled="isChecking" @click="checkBalance"> <RefreshCw v-if="isChecking" class="mr-2 size-4 animate-spin" /> <ShieldCheck v-else class="mr-2 size-4" /> {{ isChecking ? 'Verifying Balance...' : 'Check Balance' }} </Button> </CardContent> </Card> <!-- Stylized Digital Gift Card Hero Display --> <div class="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 class="relative flex flex-col justify-between space-y-6"> <!-- Card Top Bar: Brand & Pass Badge --> <div class="flex items-start justify-between"> <div class="flex items-center gap-2.5"> <div class="flex size-9 items-center justify-center rounded-xl border border-white/10 bg-white/10 text-white shadow-inner backdrop-blur-md" > <Gift class="text-success size-5" /> </div> <div> <p class="text-xs font-bold tracking-wider text-neutral-200 uppercase">Lumen Store</p> <p class="text-xs text-neutral-400">Digital Gift Pass</p> </div> </div> <Badge variant="outline" class="gap-1.5 border-white/20 bg-white/10 px-2.5 py-0.5 text-xs text-white backdrop-blur-md" > <span class="bg-success size-1.5 animate-pulse rounded-full" /> Verified Active </Badge> </div> <!-- Card Center: Chip Icon & Balance Display --> <div class="space-y-1 pt-1"> <div class="flex items-center justify-between"> <span class="text-xs font-medium tracking-wider text-neutral-400 uppercase">Available Balance</span> <!-- Chip graphic representation --> <div class="flex items-center gap-1 opacity-70"> <div class="border-warning/20/40 h-5 w-7 rounded-sm border bg-gradient-to-tr from-amber-400/20 to-amber-200/40" /> <span class="font-mono text-xs tracking-tighter text-neutral-400">NFC</span> </div> </div> <div class="flex items-baseline gap-2"> <span class="text-3xl font-bold tracking-tight text-white tabular-nums sm:text-4xl"> ${{ currentBalance.toFixed(2) }} </span> <span class="text-success text-xs font-medium">USD</span> </div> </div> <!-- Card Footer: Card Number & Expiry terms --> <div class="space-y-2 border-t border-white/10 pt-4"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <span class="font-mono text-sm tracking-widest text-neutral-200"> •••• •••• •••• {{ lastFourDigits }} </span> <button type="button" class="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" @click="copyCardNumber" > <Check v-if="copied" class="text-success size-3.5" /> <Copy v-else class="size-3.5" /> </button> </div> <span v-if="copied" class="animate-in fade-in text-success text-xs font-medium"> Copied! </span> </div> <div class="flex items-center justify-between text-xs text-neutral-400"> <span>Never Expires · No Inactivity Fees</span> <span class="font-mono text-neutral-500">PIN: ••••</span> </div> </div> </div> </div> <!-- Quick Action Buttons --> <div class="space-y-3"> <Button variant="default" class="w-full font-medium" @click="applyToOrder"> <Check v-if="applied" class="text-success mr-2 size-4" /> <ShoppingBag v-else class="mr-2 size-4" /> {{ applied ? 'Applied to Checkout!' : 'Apply to Next Order' }} </Button> <!-- Quick Reload Card --> <Card class="border-border shadow-xs"> <CardHeader class="p-4 pb-2"> <div class="flex items-center justify-between"> <CardTitle class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Quick Reload Funds </CardTitle> <Plus class="text-muted-foreground size-3.5" /> </div> </CardHeader> <CardContent class="p-4 pt-0"> <p class="text-muted-foreground mb-3 text-xs"> Top up your gift card instantly with a saved payment method. </p> <div class="grid grid-cols-3 gap-2"> <Button variant="outline" size="sm" class="hover:bg-primary/5 hover:text-primary hover:border-primary/30 w-full font-medium tabular-nums" @click="reloadFunds(25)" > + $25.00 </Button> <Button variant="outline" size="sm" class="hover:bg-primary/5 hover:text-primary hover:border-primary/30 w-full font-medium tabular-nums" @click="reloadFunds(50)" > + $50.00 </Button> <Button variant="outline" size="sm" class="hover:bg-primary/5 hover:text-primary hover:border-primary/30 w-full font-medium tabular-nums" @click="reloadFunds(100)" > + $100.00 </Button> </div> </CardContent> </Card> </div> </div> <!-- Right Column: Balance Stats & Redemption History Table --> <div class="space-y-6 lg:col-span-7"> <!-- Passbook Summary Metrics --> <div class="grid grid-cols-1 gap-3 sm:grid-cols-3"> <div class="border-border bg-card rounded-xl border p-4 shadow-xs"> <div class="text-muted-foreground mb-1 flex items-center justify-between text-xs"> <span>Available Balance</span> <Wallet class="text-primary size-3.5" /> </div> <p class="text-foreground text-2xl font-bold tracking-tight tabular-nums"> ${{ currentBalance.toFixed(2) }} </p> <p class="text-success mt-1 flex items-center gap-1 text-xs font-medium"> <Check class="size-3" /> Ready to spend </p> </div> <div class="border-border bg-card rounded-xl border p-4 shadow-xs"> <div class="text-muted-foreground mb-1 flex items-center justify-between text-xs"> <span>Total Value Loaded</span> <ArrowDownLeft class="text-success size-3.5" /> </div> <p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">${{ totalLoaded.toFixed(2) }}</p> <p class="text-muted-foreground mt-1 text-xs">All top-ups & issue</p> </div> <div class="border-border bg-card rounded-xl border p-4 shadow-xs"> <div class="text-muted-foreground mb-1 flex items-center justify-between text-xs"> <span>Total Redeemed</span> <ArrowUpRight class="text-muted-foreground size-3.5" /> </div> <p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">-${{ totalSpent.toFixed(2) }}</p> <p class="text-muted-foreground mt-1 text-xs">Past store orders</p> </div> </div> <!-- Redemption & Activity History Card --> <Card class="border-border shadow-xs"> <CardHeader class="pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <CardTitle class="text-base font-semibold">Redemption & Activity History</CardTitle> <CardDescription class="text-xs"> Detailed ledger of card issuance, promotional top-ups, and store checkouts. </CardDescription> </div> <!-- Filter Pills --> <div class="border-border bg-muted/40 flex items-center gap-1.5 rounded-lg border p-1 text-xs"> <button type="button" :class=" 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', ) " @click="filterType = 'all'" > All ({{ transactions.length }}) </button> <button type="button" :class=" 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', ) " @click="filterType = 'redemptions'" > Redemptions </button> <button type="button" :class=" 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', ) " @click="filterType = 'loads'" > Loads </button> </div> </div> </CardHeader> <CardContent class="p-0"> <div class="overflow-x-auto"> <Table> <TableHeader> <TableRow> <TableHead class="w-[110px]">Date</TableHead> <TableHead>Activity & Reference</TableHead> <TableHead class="w-[90px] text-center">Type</TableHead> <TableHead class="w-[100px] text-right">Amount</TableHead> <TableHead class="w-[110px] text-right">Resulting Balance</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow v-for="record in filteredTransactions" :key="record.id"> <TableCell class="text-muted-foreground text-xs font-medium"> {{ record.date }} </TableCell> <TableCell> <div class="space-y-0.5"> <p class="text-foreground text-sm leading-none font-medium"> {{ record.title }} </p> <p v-if="record.subtitle" class="text-muted-foreground text-xs"> {{ record.subtitle }} </p> </div> </TableCell> <TableCell class="text-center"> <Badge v-if="record.type === 'activation'" variant="outline" class="border-success/30 bg-success/10 text-success px-2 py-0.5 text-xs font-normal" > Activation </Badge> <Badge v-else-if="record.type === 'reload'" variant="outline" class="border-info/30 bg-info/10 text-info px-2 py-0.5 text-xs font-normal" > Reload </Badge> <Badge v-else variant="secondary" class="px-2 py-0.5 text-xs font-normal"> Redemption </Badge> </TableCell> <TableCell :class=" 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 class="text-muted-foreground text-right font-mono text-sm tabular-nums"> ${{ record.balanceAfter.toFixed(2) }} </TableCell> </TableRow> </TableBody> </Table> </div> </CardContent> <CardFooter class="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 class="flex items-center gap-1.5"> <ShieldCheck class="text-primary size-4 shrink-0" /> <span>Protected by 256-bit encryption · Zero liability policy</span> </div> <div class="flex items-center gap-3"> <button type="button" class="hover:text-foreground flex min-h-6 items-center gap-1 underline-offset-4 hover:underline" @click="checkBalance" > <RefreshCw class="size-3" /> Refresh Ledger </button> </div> </CardFooter> </Card> <!-- Digital Passbook Barcode Card --> <Card class="border-border bg-muted/20 shadow-xs"> <CardContent class="flex flex-col items-center justify-between gap-4 p-4 sm:flex-row sm:p-5"> <div class="space-y-1 text-center sm:text-left"> <h4 class="text-foreground flex items-center justify-center gap-2 text-sm font-semibold sm:justify-start"> <Wallet class="text-primary size-4" /> In-Store Digital Pass </h4> <p class="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 class="border-border bg-card flex flex-col items-center gap-1.5 rounded-lg border p-3 shadow-xs"> <div class="flex h-10 items-end gap-[3px] px-2" aria-hidden="true"> <div class="bg-foreground h-full w-[2px]" /> <div class="bg-foreground h-full w-[4px]" /> <div class="bg-foreground h-full w-[1px]" /> <div class="bg-foreground h-full w-[3px]" /> <div class="bg-foreground h-full w-[2px]" /> <div class="bg-foreground h-full w-[5px]" /> <div class="bg-foreground h-full w-[1px]" /> <div class="bg-foreground h-full w-[3px]" /> <div class="bg-foreground h-full w-[2px]" /> <div class="bg-foreground h-full w-[4px]" /> <div class="bg-foreground h-full w-[2px]" /> <div class="bg-foreground h-full w-[1px]" /> <div class="bg-foreground h-full w-[4px]" /> <div class="bg-foreground h-full w-[2px]" /> <div class="bg-foreground h-full w-[3px]" /> </div> <span class="text-muted-foreground font-mono text-xs tracking-widest"> {{ cardNumber }} </span> </div> </CardContent> </Card> </div> </div> </div> </template>
Raw manifest:https://uipkge.dev/r/vue/gift-card-balance-checker.json