Menu

Change language

GitHub repo

Gift Card Balance Checker

blockcommerce

Customer storefront gift card balance lookup, live digital card passbook, quick fund reloading, and complete redemption audit history ledger.

Installation

$npx uipkge-ng@latest add gift-card-balance-checker
Manifest:https://uipkge.dev/r/angular/gift-card-balance-checker.jsonInstalls to:src/app/components/blocks/gift-card-balance-checker/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

GiftCardTransactionRecord
interface GiftCardTransactionRecord {
  id: string
  date: string
  reference: string
  title: string
  subtitle?: string
  type: 'activation' | 'redemption' | 'reload'
  amount: number
  balanceAfter: number
  status: 'Completed' | 'Pending'
}

Files installed (2)

  • src/app/components/blocks/gift-card-balance-checker/gift-card-balance-checker.component.ts30.5 kB
    import { Component, DestroyRef, Input, computed, inject, signal, ChangeDetectionStrategy } from '@angular/core'
    import {
      ArrowDownLeft,
      ArrowUpRight,
      Check,
      CheckCircle2,
      Copy,
      CreditCard,
      Gift,
      HelpCircle,
      Lock,
      LucideAngularModule,
      Plus,
      RefreshCw,
      ShieldCheck,
      ShoppingBag,
      Wallet,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardFooterComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    import {
      UiTooltipComponent,
      UiTooltipContentComponent,
      UiTooltipProviderComponent,
      UiTooltipTriggerComponent,
    } from '@/ui/tooltip/tooltip.component'
    
    export interface GiftCardTransactionRecord {
      id: string
      date: string
      reference: string
      title: string
      subtitle?: string
      type: 'activation' | 'redemption' | 'reload'
      amount: number
      balanceAfter: number
      status: 'Completed' | 'Pending'
    }
    
    export type GiftCardFilterType = 'all' | 'redemptions' | 'loads'
    
    const initialTransactions: GiftCardTransactionRecord[] = [
      {
        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',
      },
    ]
    
    /** Groups digits in fours: "7482 - 9104 - 6382 - 8492" (max 16 digits). */
    export function formatGiftCardInput(raw: string): string {
      const digits = raw.replace(/\D/g, '').slice(0, 16)
      return digits.replace(/(.{4})(?=.)/g, '$1 - ')
    }
    
    /**
     * Angular port of the React GiftCardBalanceChecker block: card lookup form with a digital
     * gift-card hero, quick fund reloads, balance metrics, a filterable redemption ledger and an
     * in-store barcode pass.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-gift-card-balance-checker, [ui-gift-card-balance-checker]',
      standalone: true,
      host: {
        '[attr.data-slot]': '"gift-card-balance-checker"',
        '[class]': 'hostClass',
      },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardFooterComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiInputComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
        UiTooltipComponent,
        UiTooltipContentComponent,
        UiTooltipProviderComponent,
        UiTooltipTriggerComponent,
      ],
      template: `
        <!-- 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>
              <span ui-badge variant="outline" class="bg-primary/5 text-primary border-primary/20 gap-1">
                <lucide-icon [img]="CheckCircle2" [class]="'text-primary size-3'" />
                Storefront Pass
              </span>
            </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">
            <ui-tooltip-provider [delayDuration]="150">
              <ui-tooltip>
                <div
                  ui-tooltip-trigger
                  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"
                >
                  <lucide-icon [img]="HelpCircle" [class]="'text-primary size-3.5'" />
                  <span>PIN &amp; Card Guide</span>
                </div>
                <ui-tooltip-content 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.
                </ui-tooltip-content>
              </ui-tooltip>
            </ui-tooltip-provider>
          </div>
        </div>
    
        <!-- Feedback Banner if active -->
        @if (feedbackMessage()) {
          <div
            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">
              <lucide-icon [img]="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 -->
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="pb-4">
                <div class="flex items-center justify-between">
                  <h3 ui-card-title class="text-base font-semibold">Card Lookup</h3>
                  @if (isVerified()) {
                    <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1 text-xs">
                      <lucide-icon [img]="Check" [class]="'size-3'" />
                      Active Card
                    </span>
                  }
                </div>
                <p ui-card-description class="text-xs">
                  Enter your gift card credentials to view live balance and activity.
                </p>
              </div>
    
              <div ui-card-content 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-angular" class="text-foreground text-xs font-medium">
                      16-Digit Card Number
                    </label>
                    <ui-tooltip-provider [delayDuration]="150">
                      <ui-tooltip>
                        <button
                          ui-tooltip-trigger
                          type="button"
                          class="text-muted-foreground hover:text-foreground inline-flex items-center focus-visible:outline-none"
                          aria-label="Gift card number info"
                        >
                          <lucide-icon [img]="HelpCircle" [class]="'size-3.5'" />
                        </button>
                        <ui-tooltip-content side="top" class="max-w-xs text-xs">
                          Enter the 16 digits on the back of your card or in your receipt email.
                        </ui-tooltip-content>
                      </ui-tooltip>
                    </ui-tooltip-provider>
                  </div>
    
                  <ui-input
                    id="gc-number-input-angular"
                    [value]="cardNumber()"
                    placeholder="XXXX - XXXX - XXXX - XXXX"
                    [maxLength]="25"
                    class="font-mono text-sm"
                    [prefixIcon]="cardIcon"
                    (valueChange)="onCardNumberChange($event)"
                  />
                  <ng-template #cardIcon><lucide-icon [img]="CreditCard" [class]="'size-4'" /></ng-template>
                </div>
    
                <!-- 4-Digit Security PIN Input -->
                <div class="space-y-1.5">
                  <div class="flex items-center justify-between">
                    <label for="gc-pin-input-angular" class="text-foreground text-xs font-medium">
                      Security PIN (4 digits)
                    </label>
                    <ui-tooltip-provider [delayDuration]="150">
                      <ui-tooltip>
                        <button
                          ui-tooltip-trigger
                          type="button"
                          class="text-muted-foreground hover:text-foreground inline-flex items-center focus-visible:outline-none"
                          aria-label="Security PIN info"
                        >
                          <lucide-icon [img]="HelpCircle" [class]="'size-3.5'" />
                        </button>
                        <ui-tooltip-content 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.
                        </ui-tooltip-content>
                      </ui-tooltip>
                    </ui-tooltip-provider>
                  </div>
    
                  <ui-input
                    id="gc-pin-input-angular"
                    [value]="securityPin()"
                    type="password"
                    placeholder="••••"
                    [maxLength]="4"
                    showPasswordToggle
                    class="font-mono text-sm"
                    [prefixIcon]="lockIcon"
                    (valueChange)="onPinChange($event)"
                  />
                  <ng-template #lockIcon><lucide-icon [img]="Lock" [class]="'size-4'" /></ng-template>
                </div>
    
                <button
                  ui-button
                  type="button"
                  class="w-full font-medium"
                  [disabled]="isChecking()"
                  (click)="checkBalance()"
                >
                  @if (isChecking()) {
                    <lucide-icon [img]="RefreshCw" [class]="'mr-2 size-4 animate-spin'" />
                    Verifying Balance...
                  } @else {
                    <lucide-icon [img]="ShieldCheck" [class]="'mr-2 size-4'" />
                    Check Balance
                  }
                </button>
              </div>
            </div>
    
            <!-- 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"
                    >
                      <lucide-icon [img]="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>
    
                  <span
                    ui-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"></span>
                    Verified Active
                  </span>
                </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"
                      ></div>
                      <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()"
                      >
                        @if (copied()) {
                          <lucide-icon [img]="Check" [class]="'text-success size-3.5'" />
                        } @else {
                          <lucide-icon [img]="Copy" [class]="'size-3.5'" />
                        }
                      </button>
                    </div>
                    @if (copied()) {
                      <span 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 ui-button variant="default" class="w-full font-medium" (click)="applyToOrder()">
                @if (applied()) {
                  <lucide-icon [img]="Check" [class]="'text-success mr-2 size-4'" />
                  Applied to Checkout!
                } @else {
                  <lucide-icon [img]="ShoppingBag" [class]="'mr-2 size-4'" />
                  Apply to Next Order
                }
              </button>
    
              <!-- Quick Reload Card -->
              <div ui-card class="border-border shadow-xs">
                <div ui-card-header class="p-4 pb-2">
                  <div class="flex items-center justify-between">
                    <h3 ui-card-title class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                      Quick Reload Funds
                    </h3>
                    <lucide-icon [img]="Plus" [class]="'text-muted-foreground size-3.5'" />
                  </div>
                </div>
                <div ui-card-content 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
                      ui-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
                      ui-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
                      ui-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>
                </div>
              </div>
            </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>
                  <lucide-icon [img]="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">
                  <lucide-icon [img]="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>
                  <lucide-icon [img]="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 &amp; 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>
                  <lucide-icon [img]="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 -->
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="pb-3">
                <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                  <div>
                    <h3 ui-card-title class="text-base font-semibold">Redemption &amp; Activity History</h3>
                    <p ui-card-description class="text-xs">
                      Detailed ledger of card issuance, promotional top-ups, and store checkouts.
                    </p>
                  </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]="filterClass('all')" (click)="filterType.set('all')">
                      All ({{ transactions().length }})
                    </button>
                    <button type="button" [class]="filterClass('redemptions')" (click)="filterType.set('redemptions')">
                      Redemptions
                    </button>
                    <button type="button" [class]="filterClass('loads')" (click)="filterType.set('loads')">Loads</button>
                  </div>
                </div>
              </div>
    
              <div ui-card-content class="p-0">
                <div class="overflow-x-auto">
                  <div ui-table>
                    <thead ui-table-header>
                      <tr ui-table-row>
                        <th ui-table-head class="w-[110px]">Date</th>
                        <th ui-table-head>Activity &amp; Reference</th>
                        <th ui-table-head class="w-[90px] text-center">Type</th>
                        <th ui-table-head class="w-[100px] text-right">Amount</th>
                        <th ui-table-head class="w-[110px] text-right">Resulting Balance</th>
                      </tr>
                    </thead>
                    <tbody ui-table-body>
                      @for (record of filteredTransactions(); track record.id) {
                        <tr ui-table-row>
                          <td ui-table-cell class="text-muted-foreground text-xs font-medium">{{ record.date }}</td>
                          <td ui-table-cell>
                            <div class="space-y-0.5">
                              <p class="text-foreground text-sm leading-none font-medium">{{ record.title }}</p>
                              @if (record.subtitle) {
                                <p class="text-muted-foreground text-xs">{{ record.subtitle }}</p>
                              }
                            </div>
                          </td>
                          <td ui-table-cell class="text-center">
                            @if (record.type === 'activation') {
                              <span
                                ui-badge
                                variant="outline"
                                class="border-success/30 bg-success/10 text-success px-2 py-0.5 text-xs font-normal"
                              >
                                Activation
                              </span>
                            }
                            @if (record.type === 'reload') {
                              <span
                                ui-badge
                                variant="outline"
                                class="border-info/30 bg-info/10 text-info px-2 py-0.5 text-xs font-normal"
                              >
                                Reload
                              </span>
                            }
                            @if (record.type === 'redemption') {
                              <span ui-badge variant="secondary" class="px-2 py-0.5 text-xs font-normal">Redemption</span>
                            }
                          </td>
                          <td ui-table-cell [class]="amountClass(record)">{{ amountLabel(record) }}</td>
                          <td ui-table-cell class="text-muted-foreground text-right font-mono text-sm tabular-nums">
                            \${{ record.balanceAfter.toFixed(2) }}
                          </td>
                        </tr>
                      }
                    </tbody>
                  </div>
                </div>
              </div>
    
              <div
                ui-card-footer
                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">
                  <lucide-icon [img]="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()"
                  >
                    <lucide-icon [img]="RefreshCw" [class]="'size-3'" />
                    Refresh Ledger
                  </button>
                </div>
              </div>
            </div>
    
            <!-- Digital Passbook Barcode Card -->
            <div ui-card class="border-border bg-muted/20 shadow-xs">
              <div ui-card-content 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">
                    <lucide-icon [img]="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>
                    <div class="bg-foreground h-full w-[4px]"></div>
                    <div class="bg-foreground h-full w-[1px]"></div>
                    <div class="bg-foreground h-full w-[3px]"></div>
                    <div class="bg-foreground h-full w-[2px]"></div>
                    <div class="bg-foreground h-full w-[5px]"></div>
                    <div class="bg-foreground h-full w-[1px]"></div>
                    <div class="bg-foreground h-full w-[3px]"></div>
                    <div class="bg-foreground h-full w-[2px]"></div>
                    <div class="bg-foreground h-full w-[4px]"></div>
                    <div class="bg-foreground h-full w-[2px]"></div>
                    <div class="bg-foreground h-full w-[1px]"></div>
                    <div class="bg-foreground h-full w-[4px]"></div>
                    <div class="bg-foreground h-full w-[2px]"></div>
                    <div class="bg-foreground h-full w-[3px]"></div>
                  </div>
                  <span class="text-muted-foreground font-mono text-xs tracking-widest">{{ cardNumber() }}</span>
                </div>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiGiftCardBalanceCheckerComponent {
      @Input('class') className?: string
    
      private readonly destroyRef = inject(DestroyRef)
      private readonly timers = new Set<ReturnType<typeof setTimeout>>()
    
      protected readonly ArrowDownLeft = ArrowDownLeft
      protected readonly ArrowUpRight = ArrowUpRight
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Copy = Copy
      protected readonly CreditCard = CreditCard
      protected readonly Gift = Gift
      protected readonly HelpCircle = HelpCircle
      protected readonly Lock = Lock
      protected readonly Plus = Plus
      protected readonly RefreshCw = RefreshCw
      protected readonly ShieldCheck = ShieldCheck
      protected readonly ShoppingBag = ShoppingBag
      protected readonly Wallet = Wallet
    
      readonly cardNumber = signal('7482 - 9104 - 6382 - 8492')
      readonly securityPin = signal('4829')
      readonly isChecking = signal(false)
      readonly isVerified = signal(true)
      readonly copied = signal(false)
      readonly applied = signal(false)
      readonly feedbackMessage = signal<string | null>(null)
      readonly filterType = signal<GiftCardFilterType>('all')
      readonly transactions = signal<GiftCardTransactionRecord[]>(initialTransactions)
    
      readonly currentBalance = computed(() => {
        const list = this.transactions()
        return list.length === 0 ? 0 : list[0]!.balanceAfter
      })
    
      readonly totalLoaded = computed(() =>
        this.transactions()
          .filter((t) => t.amount > 0)
          .reduce((sum, t) => sum + t.amount, 0),
      )
    
      readonly totalSpent = computed(() =>
        Math.abs(
          this.transactions()
            .filter((t) => t.amount < 0)
            .reduce((sum, t) => sum + t.amount, 0),
        ),
      )
    
      readonly lastFourDigits = computed(() => {
        const digits = this.cardNumber().replace(/\D/g, '')
        return digits.length >= 4 ? digits.slice(-4) : '8492'
      })
    
      readonly filteredTransactions = computed(() => {
        const filter = this.filterType()
        const list = this.transactions()
        if (filter === 'redemptions') return list.filter((t) => t.type === 'redemption')
        if (filter === 'loads') return list.filter((t) => t.type === 'activation' || t.type === 'reload')
        return list
      })
    
      constructor() {
        this.destroyRef.onDestroy(() => {
          for (const t of this.timers) clearTimeout(t)
          this.timers.clear()
        })
      }
    
      get hostClass(): string {
        return cn('block w-full space-y-6', this.className)
      }
    
      protected filterClass(type: GiftCardFilterType): string {
        return cn(
          'rounded px-2.5 py-1 font-medium transition-colors',
          this.filterType() === type
            ? 'bg-background text-foreground shadow-xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      protected amountClass(record: GiftCardTransactionRecord): string {
        return cn('text-right text-sm font-medium tabular-nums', record.amount > 0 ? 'text-success' : 'text-foreground')
      }
    
      protected amountLabel(record: GiftCardTransactionRecord): string {
        return record.amount > 0 ? `+$${record.amount.toFixed(2)}` : `-$${Math.abs(record.amount).toFixed(2)}`
      }
    
      private later(fn: () => void, ms: number): void {
        const t = setTimeout(() => {
          this.timers.delete(t)
          fn()
        }, ms)
        this.timers.add(t)
      }
    
      onCardNumberChange(value: string): void {
        this.cardNumber.set(formatGiftCardInput(value))
      }
    
      onPinChange(value: string): void {
        this.securityPin.set(value.replace(/\D/g, '').slice(0, 4))
      }
    
      checkBalance(): void {
        this.isChecking.set(true)
        this.feedbackMessage.set(null)
        this.later(() => {
          this.isChecking.set(false)
          this.isVerified.set(true)
          this.feedbackMessage.set(`Gift card verified. Current active balance is $${this.currentBalance().toFixed(2)}.`)
          this.later(() => this.feedbackMessage.set(null), 4000)
        }, 500)
      }
    
      copyCardNumber(): void {
        const raw = this.cardNumber().replace(/\s+/g, '')
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText(raw).catch(() => {})
        }
        this.copied.set(true)
        this.later(() => this.copied.set(false), 2000)
      }
    
      applyToOrder(): void {
        this.copyCardNumber()
        this.applied.set(true)
        this.feedbackMessage.set(`Card ending in ${this.lastFourDigits()} copied and applied to your checkout session!`)
        this.later(() => {
          this.applied.set(false)
          this.feedbackMessage.set(null)
        }, 4000)
      }
    
      reloadFunds(amount: number): void {
        const newBalance = this.currentBalance() + amount
        const newRecord: GiftCardTransactionRecord = {
          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,
          balanceAfter: newBalance,
          status: 'Completed',
        }
        this.transactions.update((prev) => [newRecord, ...prev])
        this.feedbackMessage.set(
          `Successfully reloaded +$${amount.toFixed(2)}! New card balance: $${newBalance.toFixed(2)}.`,
        )
        this.later(() => this.feedbackMessage.set(null), 4000)
      }
    }
    
  • src/app/components/blocks/gift-card-balance-checker/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/gift-card-balance-checker.json