Referral Program Widget
blockmarketingRewardful and Viral Loops-style customer referral program and milestone reward tracker. Features a balance summary header with available credits and referral counts, shareable link and code copy actions with quick social sharing, a 4-tier milestone stepper with dynamic progress, and a recent referrals activity table with payout statuses.
Installation
$pnpm dlx uipkge-ng@latest add referral-program-widget$npx uipkge-ng@latest add referral-program-widget$yarn dlx uipkge-ng@latest add referral-program-widget$bunx uipkge-ng@latest add referral-program-widgetManifest:
https://uipkge.dev/r/angular/referral-program-widget.jsonInstalls to:src/app/components/blocks/referral-program-widget/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.
MilestoneTierinterface MilestoneTier {
id: string
tier: number
referralsRequired: number
title: string
reward: string
description: string
status: 'completed' | 'in-progress' | 'upcoming'
}ReferralActivityinterface ReferralActivity {
id: string
name: string
email: string
initials: string
signedUpAt: string
status: 'paid' | 'pending'
rewardAmount: string
}Files installed (2)
src/app/components/blocks/referral-program-widget/referral-program-widget.component.ts20.6 kB
// Rewardful / Viral Loops style customer referral program and milestone reward tracker: // balance summary header, shareable link + code copy actions with social sharing, a 4-tier // milestone stepper with progress, and a recent referrals activity table. Port of the // React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, inject, signal } from '@angular/core' import { Check, Clock, Copy, Gift, Lock, LucideAngularModule, Mail, Share2, Target, Trophy, Users, Wallet, } 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, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.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 MilestoneTier { id: string tier: number referralsRequired: number title: string reward: string description: string status: 'completed' | 'in-progress' | 'upcoming' } export interface ReferralActivity { id: string name: string email: string initials: string signedUpAt: string status: 'paid' | 'pending' rewardAmount: string } const tiers: MilestoneTier[] = [ { id: 'tier-1', tier: 1, referralsRequired: 1, title: '$25 Account Credit', reward: '$25 Credit', description: 'Instant credit applied directly to your renewal balance.', status: 'completed', }, { id: 'tier-2', tier: 2, referralsRequired: 5, title: '$125 Credit + Exclusive Swag Kit', reward: '$125 + Swag Kit', description: 'Premium organic cotton hoodie and engineering desk kit.', status: 'completed', }, { id: 'tier-3', tier: 3, referralsRequired: 10, title: '$250 Credit + 1-Year Free Pro Plan', reward: '$250 + Pro Plan', description: '6 of 10 referred · 4 more needed to unlock full Pro tier.', status: 'in-progress', }, { id: 'tier-4', tier: 4, referralsRequired: 25, title: '$1,000 Credit + VIP Lifetime Access', reward: '$1,000 + VIP Pass', description: 'Direct Slack channel access to core founders and private beta track.', status: 'upcoming', }, ] const recentReferrals: ReferralActivity[] = [ { id: 'ref-1', name: 'Marcus Vance', email: '[email protected]', initials: 'MV', signedUpAt: 'Oct 14, 2024', status: 'paid', rewardAmount: '$25.00', }, { id: 'ref-2', name: 'Sarah Jenkins', email: '[email protected]', initials: 'SJ', signedUpAt: 'Oct 02, 2024', status: 'paid', rewardAmount: '$25.00', }, { id: 'ref-3', name: 'Devon Lane', email: '[email protected]', initials: 'DL', signedUpAt: 'Sep 28, 2024', status: 'pending', rewardAmount: '$25.00', }, { id: 'ref-4', name: 'Priya Patel', email: '[email protected]', initials: 'PP', signedUpAt: 'Sep 15, 2024', status: 'paid', rewardAmount: '$25.00', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-referral-program-widget, [ui-referral-program-widget]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiInputComponent, UiProgressComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="referral-program-widget" [class]="rootClass"> <!-- Header Section --> <div class="space-y-4"> <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 variant="secondary" class="gap-1.5 font-medium"> <lucide-icon [img]="Gift" class="text-primary size-3.5" /> Give $25, Get $25 </span> </div> <h1 class="text-foreground mt-2 text-2xl font-bold tracking-tight sm:text-3xl"> Refer Friends & Earn Credits </h1> <p class="text-muted-foreground mt-1 max-w-2xl text-sm sm:text-base"> Give your network $25 off their first year, and earn $25 credit for every active referral. </p> </div> </div> <!-- Referral Balance Card --> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-content class="p-4 sm:p-6"> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-4"> <div class="bg-primary/10 text-primary border-primary/20 flex size-12 shrink-0 items-center justify-center rounded-xl border" > <lucide-icon [img]="Wallet" class="size-6" /> </div> <div> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Referral Balance</p> <div class="mt-0.5 flex flex-wrap items-baseline gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl"> $150.00 </span> <span class="text-muted-foreground text-sm font-medium">Available Credit</span> <span class="text-foreground text-sm font-semibold tabular-nums">6 Successful Referrals</span> </div> </div> </div> <div class="flex items-center gap-2"> <span ui-badge variant="outline" class="text-xs font-medium tabular-nums">1 Pending ($25.00)</span> <button ui-button size="sm" variant="outline">Redeem Credits</button> </div> </div> </div> </div> </div> <!-- Share Your Referral Link Card --> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex items-center gap-2"> <lucide-icon [img]="Share2" class="text-primary size-4" /> <h3 ui-card-title class="text-base font-semibold">Share Your Referral Link</h3> </div> <p ui-card-description> Send your personal invite link or share directly across social channels to earn rewards instantly. </p> </div> <div ui-card-content class="space-y-4"> <!-- Referral URL & Copy Action --> <div class="flex flex-col gap-2 sm:flex-row"> <div class="relative flex-1"> <ui-input [value]="referralUrl" [readOnly]="true" class="bg-muted/30 font-mono text-xs sm:text-sm" aria-label="Your custom referral URL" /> </div> <button ui-button type="button" [variant]="copiedLink() ? 'outline' : 'default'" class="shrink-0 gap-1.5" (click)="copyToClipboard(referralUrl, 'link')" > @if (copiedLink()) { <lucide-icon [img]="Check" class="text-success size-4" /> } @else { <lucide-icon [img]="Copy" class="size-4" /> } <span>{{ copiedLink() ? 'Copied Link' : 'Copy Link' }}</span> </button> </div> <ui-separator /> <!-- Social Share & Code Bar --> <div class="flex flex-wrap items-center justify-between gap-2.5"> <div class="flex flex-wrap items-center gap-2"> <a ui-button [href]="twitterShareUrl" target="_blank" rel="noopener noreferrer" variant="outline" size="sm" class="gap-1.5 text-xs font-medium" > <svg class="size-3.5 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <path d="M18.24 2.25h3.31l-7.23 8.26 8.5 11.24h-6.66l-5.21-6.82-5.97 6.82H1.68l7.73-8.84L1.25 2.25h6.83l4.71 6.23zm-1.16 17.52h1.83L7.08 4.13H5.12z" /> </svg> Share to X </a> <a ui-button [href]="linkedinShareUrl" target="_blank" rel="noopener noreferrer" variant="outline" size="sm" class="gap-1.5 text-xs font-medium" > <svg class="size-3.5 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.06 2.06 0 110-4.13 2.06 2.06 0 010 4.13zm1.78 13.02H3.56V9h3.56v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.73C24 .77 23.2 0 22.22 0z" /> </svg> Share to LinkedIn </a> <a ui-button [href]="emailShareUrl" variant="outline" size="sm" class="gap-1.5 text-xs font-medium"> <lucide-icon [img]="Mail" class="size-3.5 shrink-0" /> Share via Email </a> </div> <button ui-button type="button" variant="outline" size="sm" class="gap-1.5 text-xs font-medium" (click)="copyToClipboard(referralCode, 'code')" > @if (copiedCode()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="text-muted-foreground size-3.5" /> } <span>Invite Code:</span> <span class="text-foreground font-mono font-semibold">{{ referralCode }}</span> @if (copiedCode()) { <span class="text-success text-xs font-medium">Copied!</span> } </button> </div> </div> </div> <!-- Milestone Rewards Progress Stepper --> <div ui-card class="border-border/80 shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <div class="flex items-center gap-2"> <lucide-icon [img]="Trophy" class="text-warning size-4" /> <h3 ui-card-title class="text-base font-semibold">Milestone Rewards</h3> </div> <p ui-card-description> Reach referral milestones to unlock credit bonuses, swag, and exclusive plan access. </p> </div> <span ui-badge variant="outline" class="w-fit gap-1 text-xs tabular-nums"> <lucide-icon [img]="Target" class="text-primary size-3" /> Tier 3 In Progress · 6 / 10 Referrals </span> </div> </div> <div ui-card-content class="space-y-6"> <!-- Progress Stepper Track Header --> <div class="border-border/70 bg-muted/40 space-y-2 rounded-lg border p-4"> <div class="flex flex-wrap items-center justify-between gap-2 text-xs sm:text-sm"> <span class="text-foreground font-medium"> Current Target: Tier 3 ($250 Credit + 1-Year Free Pro Plan) </span> <span class="text-muted-foreground font-semibold tabular-nums">60% (6 of 10 referrals)</span> </div> <ui-progress [value]="60" class="h-2" /> <p class="text-muted-foreground text-xs">4 more successful referrals needed to unlock Tier 3 rewards.</p> </div> <!-- 4 Milestone Tiers Grid --> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4"> @for (tier of tiers; track tier.id) { <div [class]="tierClass(tier)"> <div class="space-y-2.5"> <div class="flex items-center justify-between gap-2"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Tier {{ tier.tier }} · {{ tier.referralsRequired }} {{ tier.referralsRequired === 1 ? 'Referral' : 'Referrals' }} </span> @if (tier.status === 'completed') { <span ui-badge variant="success" class="gap-1 text-xs"> <lucide-icon [img]="Check" class="size-3" /> Completed </span> } @else if (tier.status === 'in-progress') { <span ui-badge variant="default" class="gap-1 text-xs">In Progress (6/10)</span> } @else { <span ui-badge variant="secondary" class="text-muted-foreground gap-1 text-xs"> <lucide-icon [img]="Lock" class="size-3" /> Upcoming </span> } </div> <div> <p class="text-foreground text-sm font-semibold">{{ tier.title }}</p> <p class="text-muted-foreground mt-1 text-xs leading-relaxed">{{ tier.description }}</p> </div> </div> <div class="border-border/50 mt-4 border-t pt-3"> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">Reward Perk:</span> <span class="text-foreground font-semibold tabular-nums">{{ tier.reward }}</span> </div> </div> </div> } </div> </div> </div> <!-- Recent Referrals Activity Table --> <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]="Users" class="text-primary size-4" /> <h3 ui-card-title class="text-base font-semibold">Recent Referrals</h3> </div> <p ui-card-description>Track recent invitees, activation statuses, and credited referral payouts.</p> </div> <span ui-badge variant="outline" class="text-xs font-medium tabular-nums">4 Recent Signups</span> </div> </div> <div ui-card-content class="p-0 sm:p-6 sm:pt-0"> <div class="overflow-x-auto"> <table ui-table density="cozy"> <thead ui-table-header> <tr ui-table-row> <th ui-table-head>Invitee</th> <th ui-table-head>Signed Up</th> <th ui-table-head>Status</th> <th ui-table-head class="text-right">Reward Amount</th> </tr> </thead> <tbody ui-table-body> @for (referral of recentReferrals; track referral.id) { <tr ui-table-row> <td ui-table-cell> <div class="flex items-center gap-3"> <ui-avatar class="size-8 shrink-0"> <ui-avatar-fallback class="text-xs font-semibold">{{ referral.initials }}</ui-avatar-fallback> </ui-avatar> <div class="min-w-0"> <p class="text-foreground truncate text-sm font-medium">{{ referral.name }}</p> <p class="text-muted-foreground truncate text-xs">{{ referral.email }}</p> </div> </div> </td> <td ui-table-cell class="text-muted-foreground text-xs whitespace-nowrap sm:text-sm"> {{ referral.signedUpAt }} </td> <td ui-table-cell> @if (referral.status === 'paid') { <span ui-badge variant="success" class="gap-1 text-xs"> <lucide-icon [img]="Check" class="size-3" /> Reward Paid $25 </span> } @else { <span ui-badge variant="warning" class="gap-1 text-xs"> <lucide-icon [img]="Clock" class="size-3" /> Pending Activation </span> } </td> <td ui-table-cell class="text-right text-xs font-medium tabular-nums sm:text-sm"> <span [class]="referral.status === 'paid' ? 'text-success font-semibold' : 'text-muted-foreground'" > {{ referral.status === 'paid' ? '+' + referral.rewardAmount : referral.rewardAmount }} </span> </td> </tr> } </tbody> </table> </div> </div> </div> </div> `, }) export class UiReferralProgramWidgetComponent { protected readonly Check = Check protected readonly Clock = Clock protected readonly Copy = Copy protected readonly Gift = Gift protected readonly Lock = Lock protected readonly Mail = Mail protected readonly Share2 = Share2 protected readonly Target = Target protected readonly Trophy = Trophy protected readonly Users = Users protected readonly Wallet = Wallet protected readonly tiers = tiers protected readonly recentReferrals = recentReferrals @Input() referralUrl = 'https://uipkge.dev/invite/elena-rostova' @Input() referralCode = 'ELENA25' @Input('class') className?: string readonly copiedLink = signal(false) readonly copiedCode = signal(false) private timers: ReturnType<typeof setTimeout>[] = [] constructor() { inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t))) } get rootClass(): string { return cn('mx-auto w-full max-w-5xl space-y-6', this.className) } get twitterShareUrl(): string { const text = 'Get $25 off your first year with my invite link:' return `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(this.referralUrl)}` } get linkedinShareUrl(): string { return `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(this.referralUrl)}` } get emailShareUrl(): string { const subject = 'Get $25 off your subscription' const body = `Hey,\n\nUse my invite link to get $25 off your first year:\n${this.referralUrl}\n\nUse invite code ${this.referralCode} at checkout!` return `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}` } tierClass(tier: MilestoneTier): string { return cn( 'relative flex flex-col justify-between rounded-lg border p-4 transition-colors', tier.status === 'completed' && 'border-success/30 bg-success/5', tier.status === 'in-progress' && 'border-primary/50 bg-primary/5 ring-primary/20 shadow-xs ring-1', tier.status === 'upcoming' && 'border-border/60 bg-muted/20 opacity-80', ) } async copyToClipboard(text: string, type: 'link' | 'code'): Promise<void> { try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(text) } } catch { // Fallback if clipboard API is unavailable } if (type === 'link') { this.copiedLink.set(true) this.timers.push(setTimeout(() => this.copiedLink.set(false), 2000)) } else { this.copiedCode.set(true) this.timers.push(setTimeout(() => this.copiedCode.set(false), 2000)) } } }src/app/components/blocks/referral-program-widget/index.ts0.1 kB
export { UiReferralProgramWidgetComponent, type MilestoneTier, type ReferralActivity, } from './referral-program-widget.component'
Raw manifest:https://uipkge.dev/r/angular/referral-program-widget.json