Loyalty Rewards Tier Hub
blockcommerceSephora and Starbucks style VIP customer loyalty tier dashboard, points wallet, and rewards catalog. Features a customer hero card with points balance, reward valuation, and tier progress tracker, 4 VIP privilege perks with unlock statuses, a 3-item redeemable rewards catalog grid, and a recent points earnings & redemption history ledger.
Installation
$pnpm dlx uipkge-ng@latest add loyalty-rewards-tier-hub$npx uipkge-ng@latest add loyalty-rewards-tier-hub$yarn dlx uipkge-ng@latest add loyalty-rewards-tier-hub$bunx uipkge-ng@latest add loyalty-rewards-tier-hubManifest:
https://uipkge.dev/r/angular/loyalty-rewards-tier-hub.jsonInstalls to:src/app/components/blocks/loyalty-rewards-tier-hub/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
LoyaltyPerkinterface LoyaltyPerk {
id: string
title: string
subtitle: string
description: string
status: 'active' | 'locked'
tierBadge: string
unlockPoints?: number
}LoyaltyRewardinterface LoyaltyReward {
id: string
title: string
category: string
pointsCost: number
description: string
unlocked: boolean
pointsNeeded?: number
code?: string
}LoyaltyLedgerEntryinterface LoyaltyLedgerEntry {
id: string
title: string
subtitle: string
type: string
date: string
pointsChange: number
isPositive: boolean
}Files installed (2)
src/app/components/blocks/loyalty-rewards-tier-hub/loyalty-rewards-tier-hub.component.ts25 kB
import { Component, Input, signal, ChangeDetectionStrategy } from '@angular/core' import { Award, Check, Crown, Gift, Headphones, History, Lock, LucideAngularModule, ShoppingBag, Tag, Ticket, Truck, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component' 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 { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface LoyaltyPerk { id: string title: string subtitle: string description: string status: 'active' | 'locked' tierBadge: string unlockPoints?: number } export interface LoyaltyReward { id: string title: string category: string pointsCost: number description: string unlocked: boolean pointsNeeded?: number code?: string } export interface LoyaltyLedgerEntry { id: string title: string subtitle: string type: string date: string pointsChange: number isPositive: boolean } const perks: LoyaltyPerk[] = [ { id: 'perk-shipping', title: 'Free Express Shipping on All Orders', subtitle: 'All Orders', description: 'Complimentary 2-day express shipping on every online order with zero minimum spend.', status: 'active', tierBadge: 'Active', }, { id: 'perk-archive', title: 'Early Access to Private Archive Sales', subtitle: '48h Early Access', description: 'Shop seasonal private archives and limited capsule releases 48 hours before public launch.', status: 'active', tierBadge: 'Active', }, { id: 'perk-stylist', title: 'Dedicated VIP Concierge & Personal Stylist', subtitle: '1-on-1 Stylist Line', description: 'Direct priority chat with our senior stylists for bespoke fashion curation and concierge care.', status: 'active', tierBadge: 'Active', }, { id: 'perk-annual-gift', title: 'Diamond Exclusive Annual Gift', subtitle: 'Bespoke Curated Box', description: 'Curated luxury anniversary gift box ($120+ retail value) delivered automatically each winter.', status: 'locked', tierBadge: 'Unlocks at 3,000 pts', unlockPoints: 3000, }, ] const rewards: LoyaltyReward[] = [ { id: 'reward-15-off', title: '$15 Off Any Order', category: 'Store Credit Voucher', pointsCost: 1500, description: 'Instant digital coupon applied at checkout on any order above $50.', unlocked: true, code: 'VIP-SAVE15', }, { id: 'reward-leather-pouch', title: 'Free Travel Leather Pouch', category: 'Exclusive Merchandise', pointsCost: 2000, description: 'Handcrafted Italian pebbled leather travel accessory pouch in signature Midnight Noir.', unlocked: true, code: 'CLAIM-POUCH24', }, { id: 'reward-audio-30', title: '$30 Off Premium Audio', category: 'Partner Exclusive', pointsCost: 3000, description: 'Exclusive member markdown on noise-cancelling headphones and studio sound gear.', unlocked: false, pointsNeeded: 550, }, ] const ledgerTransactions: LoyaltyLedgerEntry[] = [ { id: 'tx-1', title: 'Order #ORD-9428 - Studio Silk Blouse', subtitle: 'Online Purchase · $190.00 subtotal', type: 'Earned · 2x Platinum Multiplier', date: 'Oct 18, 2024', pointsChange: 380, isPositive: true, }, { id: 'tx-2', title: 'Redeemed: $10 Voucher Code #VIP-10OFF', subtitle: 'Rewards Catalog · Checkout Redemption', type: 'Reward Redemption', date: 'Oct 10, 2024', pointsChange: -1000, isPositive: false, }, { id: 'tx-3', title: 'Order #ORD-9104 - Cashmere Wrap', subtitle: 'Online Purchase · $320.00 subtotal', type: 'Earned · 2x Platinum Multiplier', date: 'Sep 29, 2024', pointsChange: 640, isPositive: true, }, { id: 'tx-4', title: 'Tier Upgrade Bonus - Platinum Milestone', subtitle: 'Milestone Threshold · 2,000 pts Achieved', type: 'Milestone Tier Reward', date: 'Sep 15, 2024', pointsChange: 500, isPositive: true, }, ] /** * VIP loyalty tier dashboard: customer hero card with points balance, multiplier and * next-tier progress, the four tier privilege perks, a three-item rewards catalog with * redeemable vouchers, and the points history ledger. Angular port of the React * `LoyaltyRewardsTierHub` block (same markup, classes, copy and sample data). */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-loyalty-rewards-tier-hub, [ui-loyalty-rewards-tier-hub]', standalone: true, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], host: { '[attr.data-slot]': '"loyalty-rewards-tier-hub"', '[class]': 'hostClass', }, template: ` <!-- Header Section --> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <div class="flex items-center gap-2"> <span ui-badge wrap variant="secondary" class="gap-1.5 font-medium"> <lucide-icon [img]="Award" class="text-primary size-3.5" aria-hidden="true" /> VIP Rewards Club </span> </div> <h1 class="text-foreground mt-2 text-2xl font-bold tracking-tight sm:text-3xl">Loyalty & Rewards Hub</h1> <p class="text-muted-foreground mt-1 max-w-2xl text-sm sm:text-base"> Track VIP tier progress, redeem points for luxury perks, and review your member transaction ledger. </p> </div> </div> <!-- Customer Loyalty Hero Card --> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-content class="space-y-6 p-6"> <!-- Profile Header & Tier Status --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-4"> <span ui-avatar class="border-primary/20 size-14 border-2 shadow-xs"> <span ui-avatar-fallback class="bg-primary/10 text-primary text-base font-semibold">ER</span> </span> <div> <h2 class="text-foreground text-lg font-bold tracking-tight sm:text-xl">{{ customerName }}</h2> <p class="text-muted-foreground text-xs font-medium sm:text-sm">{{ tierName }} · Member since 2022</p> </div> </div> <span ui-badge wrap variant="outline" class="border-primary/20 bg-primary/10 text-primary gap-1.5 px-3 py-1.5 text-xs font-semibold" > <lucide-icon [img]="Crown" class="text-primary size-3.5 shrink-0" aria-hidden="true" /> <span>Platinum Tier · 2x Points Multiplier</span> </span> </div> <div ui-separator></div> <!-- Points Balance & Multiplier Display --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <!-- Balance Stat --> <div class="border-border/60 bg-muted/30 flex flex-col justify-between rounded-xl border p-4"> <div> <p class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Available Points Balance </p> <div class="mt-1 flex items-baseline gap-2"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums sm:text-4xl">2,450</span> <span class="text-muted-foreground text-sm font-medium">Points</span> </div> </div> <p class="text-muted-foreground mt-3 text-xs font-medium"> Estimated reward cash value: <span class="text-success text-success font-semibold tabular-nums">$24.50 reward value</span> </p> </div> <!-- Tier Multiplier Stat --> <div class="border-border/60 bg-muted/30 flex flex-col justify-between rounded-xl border p-4"> <div> <p class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Current Multiplier</p> <div class="mt-1 flex items-center gap-2"> <span class="text-foreground text-3xl font-bold tracking-tight tabular-nums sm:text-4xl">2x</span> <span ui-badge wrap variant="success" class="gap-1 text-xs"> <lucide-icon [img]="Zap" class="size-3" aria-hidden="true" /> Active </span> </div> </div> <p class="text-muted-foreground mt-3 text-xs font-medium"> Earn 2 points per $1 spent on all boutique catalog orders. </p> </div> <!-- Next Reward Threshold --> <div class="border-border/60 bg-muted/30 flex flex-col justify-between rounded-xl border p-4 sm:col-span-2 lg:col-span-1" > <div> <p class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Next Tier Goal</p> <div class="mt-1 flex items-baseline gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">550 pts</span> <span class="text-muted-foreground text-xs font-medium">to Diamond VIP</span> </div> </div> <p class="text-muted-foreground mt-3 text-xs font-medium"> Unlocks 3x points multiplier and annual exclusive gift. </p> </div> </div> <!-- Next Tier Progress Bar Section --> <div class="border-border/80 bg-muted/40 space-y-2.5 rounded-xl border p-4"> <div class="flex flex-wrap items-center justify-between gap-2 text-xs sm:text-sm"> <div class="flex items-center gap-2"> <lucide-icon [img]="Award" class="text-primary size-4 shrink-0" aria-hidden="true" /> <span class="text-foreground font-semibold">Next Tier Progress: Diamond VIP</span> </div> <span class="text-foreground font-semibold tabular-nums">2,450 / 3,000 Points</span> </div> <div ui-progress [value]="81.6" class="h-2.5"></div> <div class="flex flex-wrap items-center justify-between gap-2 text-xs"> <span class="text-muted-foreground font-medium tabular-nums"> 81.6% to Diamond VIP · 550 points to upgrade </span> <span class="text-muted-foreground">Target qualification date: Dec 31, 2024</span> </div> </div> </div> </div> <!-- 4 VIP Tier Privilege Perks Summary --> <div class="space-y-3"> <div class="flex items-center justify-between gap-2"> <div> <h3 class="text-foreground text-base font-semibold">VIP Tier Privilege Perks</h3> <p class="text-muted-foreground text-xs"> Active benefits unlocked with your Platinum status and next-tier Diamond previews. </p> </div> <span ui-badge wrap variant="outline" class="shrink-0 text-xs font-medium tabular-nums"> 3 of 4 Perks Active </span> </div> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4"> @for (perk of perks; track perk.id) { <div [class]=" cn( 'flex flex-col justify-between rounded-xl border p-4 transition-colors', perk.status === 'active' ? 'border-border/80 bg-card shadow-xs' : 'border-border/60 bg-muted/20 opacity-85' ) " > <div class="space-y-3"> <div class="flex items-center justify-between gap-2"> <div [class]="perkIconClass(perk)"> @if (perk.id === 'perk-shipping') { <lucide-icon [img]="Truck" class="size-4" aria-hidden="true" /> } @if (perk.id === 'perk-archive') { <lucide-icon [img]="Tag" class="size-4" aria-hidden="true" /> } @if (perk.id === 'perk-stylist') { <lucide-icon [img]="Headphones" class="size-4" aria-hidden="true" /> } @if (perk.id === 'perk-annual-gift') { <lucide-icon [img]="Gift" class="size-4" aria-hidden="true" /> } </div> @if (perk.status === 'active') { <span ui-badge wrap variant="success" class="gap-1 text-xs"> <lucide-icon [img]="Check" class="size-3" aria-hidden="true" /> Active </span> } @else { <span ui-badge wrap variant="secondary" class="text-muted-foreground gap-1 text-xs"> <lucide-icon [img]="Lock" class="size-3" aria-hidden="true" /> Unlocks at 3,000 pts </span> } </div> <div> <p class="text-foreground text-sm font-semibold">{{ perk.title }}</p> <p class="text-muted-foreground mt-0.5 text-xs font-medium">{{ perk.subtitle }}</p> <p class="text-muted-foreground mt-2 text-xs leading-relaxed">{{ perk.description }}</p> </div> </div> <div class="border-border/50 mt-4 border-t pt-3"> <span class="text-muted-foreground text-xs"> {{ perk.status === 'active' ? 'Included in Platinum VIP' : 'Requires Diamond Tier' }} </span> </div> </div> } </div> </div> <!-- Rewards Catalog Grid (3 unlockable rewards) --> <div class="space-y-3"> <div class="flex items-center justify-between gap-2"> <div> <h3 class="text-foreground text-base font-semibold">Rewards Catalog</h3> <p class="text-muted-foreground text-xs"> Redeem your points balance for instant discount vouchers and complimentary luxury items. </p> </div> <span ui-badge wrap variant="outline" class="shrink-0 text-xs font-medium tabular-nums"> 2 Available to Redeem </span> </div> <div class="grid grid-cols-1 gap-4 md:grid-cols-3"> @for (reward of rewards; track reward.id) { <div ui-card [class]=" cn( 'border-border/80 flex flex-col justify-between shadow-xs transition-colors', !reward.unlocked && 'bg-muted/20 opacity-90' ) " > <div ui-card-header class="pb-3"> <div class="flex items-center justify-between gap-2"> <span ui-badge wrap variant="secondary" class="min-w-0 shrink text-xs font-medium"> {{ reward.category }} </span> <div class="shrink-0 text-right"> <span class="text-foreground text-sm font-bold tabular-nums"> {{ formatPoints(reward.pointsCost) }} pts </span> </div> </div> <h3 ui-card-title class="mt-2 text-base font-semibold">{{ reward.title }}</h3> <p ui-card-description class="text-xs leading-relaxed">{{ reward.description }}</p> </div> <div ui-card-content class="pb-3"> <!-- When locked, show points shortfall --> @if (!reward.unlocked) { <div class="space-y-1.5"> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">Progress to unlock</span> <span class="text-foreground font-semibold tabular-nums">2,450 / 3,000 pts</span> </div> <div ui-progress [value]="81.6" class="h-1.5"></div> </div> } @else if (isRedeemed(reward.id)) { <div class="border-success/30 bg-success/10 text-success rounded-md border p-2.5 text-center text-xs font-medium" > <div class="flex items-center justify-center gap-1"> <lucide-icon [img]="Check" class="size-3.5" aria-hidden="true" /> <span>Redeemed! Code:</span> <span class="font-mono font-bold">{{ reward.code }}</span> </div> </div> } </div> <div ui-card-footer class="pt-0"> @if (reward.unlocked && !isRedeemed(reward.id)) { <button ui-button type="button" class="w-full gap-1.5 text-xs font-medium" (click)="redeem(reward.id)"> <lucide-icon [img]="Tag" class="size-3.5" aria-hidden="true" /> <span>Redeem Reward</span> </button> } @if (reward.unlocked && isRedeemed(reward.id)) { <button ui-button type="button" variant="outline" class="border-success/40 text-success w-full gap-1.5 text-xs font-medium" disabled > <lucide-icon [img]="Check" class="size-3.5" aria-hidden="true" /> <span>Voucher Claimed</span> </button> } @if (!reward.unlocked) { <button ui-button type="button" variant="outline" disabled class="w-full gap-1.5 text-xs font-medium"> <lucide-icon [img]="Lock" class="size-3.5" aria-hidden="true" /> <span>Locked · Need {{ reward.pointsNeeded }} pts</span> </button> } </div> </div> } </div> </div> <!-- Points History Ledger --> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex items-center justify-between gap-2"> <div> <div class="flex items-center gap-2"> <lucide-icon [img]="History" class="text-muted-foreground size-4" aria-hidden="true" /> <h3 ui-card-title class="text-base font-semibold">Points History Ledger</h3> </div> <p ui-card-description>Recent earnings and redemption transactions on your VIP account.</p> </div> <span ui-badge wrap variant="outline" class="shrink-0 text-xs font-medium tabular-nums"> Recent 4 Transactions </span> </div> </div> <div ui-card-content class="p-0 sm:p-6 sm:pt-0"> <div class="overflow-x-auto"> <div ui-table density="cozy"> <thead ui-table-header> <tr ui-table-row> <th ui-table-head>Transaction Details</th> <th ui-table-head>Type & Multiplier</th> <th ui-table-head>Date</th> <th ui-table-head class="text-right">Points Change</th> </tr> </thead> <tbody ui-table-body> @for (tx of ledgerTransactions; track tx.id) { <tr ui-table-row> <td ui-table-cell> <div class="flex items-center gap-3"> <div [class]=" cn( 'flex size-8 shrink-0 items-center justify-center rounded-lg border', tx.isPositive ? 'border-success/20 bg-success/10 text-success' : 'border-border/60 bg-muted text-muted-foreground' ) " > @if (tx.isPositive && tx.pointsChange < 500) { <lucide-icon [img]="ShoppingBag" class="size-4" aria-hidden="true" /> } @if (tx.isPositive && tx.pointsChange >= 500) { <lucide-icon [img]="Crown" class="size-4" aria-hidden="true" /> } @if (!tx.isPositive) { <lucide-icon [img]="Ticket" class="size-4" aria-hidden="true" /> } </div> <div class="min-w-0"> <p class="text-foreground truncate text-sm font-medium">{{ tx.title }}</p> <p class="text-muted-foreground truncate text-xs">{{ tx.subtitle }}</p> </div> </div> </td> <td ui-table-cell> @if (tx.type.includes('Multiplier')) { <span ui-badge wrap variant="outline" class="gap-1 text-xs font-medium"> <lucide-icon [img]="Zap" class="text-warning size-3" aria-hidden="true" /> {{ tx.type }} </span> } @else if (tx.type.includes('Milestone')) { <span ui-badge wrap variant="outline" class="border-primary/20 bg-primary/10 text-primary gap-1 text-xs font-medium" > <lucide-icon [img]="Crown" class="text-primary size-3" aria-hidden="true" /> {{ tx.type }} </span> } @else { <span ui-badge wrap variant="secondary" class="text-muted-foreground gap-1 text-xs font-medium"> <lucide-icon [img]="Ticket" class="size-3" aria-hidden="true" /> {{ tx.type }} </span> } </td> <td ui-table-cell class="text-muted-foreground text-xs whitespace-nowrap sm:text-sm"> {{ tx.date }} </td> <td ui-table-cell class="text-right text-xs font-medium tabular-nums sm:text-sm"> <span [class]="tx.isPositive ? 'text-success font-bold' : 'text-muted-foreground font-semibold'"> {{ tx.isPositive ? '+' + formatPoints(tx.pointsChange) + ' pts' : formatPoints(tx.pointsChange) + ' pts' }} </span> </td> </tr> } </tbody> </div> </div> </div> </div> `, }) export class UiLoyaltyRewardsTierHubComponent { @Input() customerName = 'Elena Rostova' @Input() tierName = 'VIP Platinum Member' @Input() currentPoints = 2450 @Input() targetPoints = 3000 @Input() pointValueDollars = 24.5 @Input() multiplier = '2x Points Multiplier' @Input('class') className?: string protected readonly cn = cn protected readonly perks = perks protected readonly rewards = rewards protected readonly ledgerTransactions = ledgerTransactions protected readonly Award = Award protected readonly Check = Check protected readonly Crown = Crown protected readonly Gift = Gift protected readonly Headphones = Headphones protected readonly History = History protected readonly Lock = Lock protected readonly ShoppingBag = ShoppingBag protected readonly Tag = Tag protected readonly Ticket = Ticket protected readonly Truck = Truck protected readonly Zap = Zap /** Reward ids the member has redeemed in this session. */ readonly redeemedMap = signal<Record<string, boolean>>({}) get hostClass(): string { return cn('mx-auto block w-full max-w-5xl space-y-6', this.className) } isRedeemed(rewardId: string): boolean { return this.redeemedMap()[rewardId] === true } redeem(rewardId: string): void { this.redeemedMap.update((prev) => ({ ...prev, [rewardId]: true })) } protected formatPoints(value: number): string { return value.toLocaleString() } protected perkIconClass(perk: LoyaltyPerk): string { return cn( 'flex size-9 shrink-0 items-center justify-center rounded-lg border', perk.id === 'perk-shipping' && 'border-success/20 bg-success/10 text-success', perk.id === 'perk-archive' && 'border-chart-1/30 bg-chart-1/10 text-chart-1', perk.id === 'perk-stylist' && 'border-info/20 bg-info/10 text-info', perk.id === 'perk-annual-gift' && 'border-border/60 bg-muted text-muted-foreground', ) } }src/app/components/blocks/loyalty-rewards-tier-hub/index.ts0.2 kB
export { UiLoyaltyRewardsTierHubComponent, type LoyaltyLedgerEntry, type LoyaltyPerk, type LoyaltyReward, } from './loyalty-rewards-tier-hub.component'
Raw manifest:https://uipkge.dev/r/angular/loyalty-rewards-tier-hub.json