Menu

Change language

GitHub repo

Referral Program Widget

blockmarketing

Rewardful 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

$npx uipkge-ng@latest add referral-program-widget
Manifest:https://uipkge.dev/r/angular/referral-program-widget.jsonInstalls to:src/app/components/blocks/referral-program-widget/

Variants

Loading interactive previews…

Schema

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

MilestoneTier
interface MilestoneTier {
  id: string
  tier: number
  referralsRequired: number
  title: string
  reward: string
  description: string
  status: 'completed' | 'in-progress' | 'upcoming'
}
ReferralActivity
interface 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 &amp; 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

Raw manifest:https://uipkge.dev/r/angular/referral-program-widget.json