Menu

Change language

GitHub repo

Waitlist — Referral Position

blockmarketing

Waitlist capture that swaps on submit to show queue position, the referral link that moves it, and a progress meter to the next tier of early access.

Installation

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

Variants

Loading interactive previews…

Files installed (2)

  • src/app/components/blocks/waitlist-referral-position/waitlist-referral-position.component.ts6.6 kB
    // Waitlist capture that swaps on submit to show queue position, the referral link
    // that moves it, and a progress meter to the next tier of early access.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'
    import { Check, Copy, LucideAngularModule, Users } 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 } 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'
    
    const referralLink = 'https://northwind.dev/w/8f2a41'
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-waitlist-referral-position, [ui-waitlist-referral-position]',
      standalone: true,
      // React renders the root <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiInputComponent,
        UiProgressComponent,
        UiSeparatorComponent,
      ],
      template: `
        <section data-slot="waitlist-referral-position" [class]="rootClass">
          <div class="mx-auto max-w-xl px-6 py-20 text-center lg:py-28">
            <span ui-badge variant="secondary" class="gap-1.5">
              <lucide-icon [img]="Users" class="size-3" aria-hidden="true" />
              8,400 waiting
            </span>
    
            <h2 class="mt-5 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
              Early access opens a cohort at a time
            </h2>
            <p class="text-muted-foreground mt-3 text-lg">
              Join the list, then move up it. Every person who joins through your link moves you forward.
            </p>
    
            <!-- One card that swaps contents rather than two stacked states, so the
                 section height barely changes between before and after. -->
            <div ui-card class="mt-8 text-left">
              <div ui-card-content class="p-6">
                @if (!joined()) {
                  <form (submit)="join($event)">
                    <label for="waitlist-email" class="text-sm font-medium">Work email</label>
                    <div class="mt-2 flex gap-2">
                      <ui-input
                        id="waitlist-email"
                        [value]="email()"
                        (valueChange)="email.set($event)"
                        type="email"
                        required
                        placeholder="you@company.com"
                        autoComplete="email"
                      />
                      <button ui-button type="submit" class="shrink-0">Join</button>
                    </div>
                    <p class="text-muted-foreground mt-2 text-xs">
                      No product emails while you wait — one message when your cohort opens.
                    </p>
                  </form>
                } @else {
                  <div aria-live="polite">
                    <div class="flex items-baseline justify-between gap-4">
                      <div>
                        <p class="text-muted-foreground text-xs tracking-wide uppercase">Your position</p>
                        <p class="font-display mt-1 text-3xl font-bold tracking-tight">#{{ position.toLocaleString() }}</p>
                      </div>
                      <p class="text-muted-foreground text-right text-xs">
                        Next cohort opens<br />at #{{ nextTierAt.toLocaleString() }}
                      </p>
                    </div>
    
                    <div ui-progress [value]="progress()" class="mt-4 h-1.5" aria-label="Progress to the next cohort"></div>
    
                    <ui-separator class="my-5" />
    
                    <p class="text-sm font-medium">Move up the list</p>
                    <p class="text-muted-foreground mt-1 text-xs">
                      Each signup through your link moves you forward 12 places.
                    </p>
                    <div class="border-border bg-muted/30 mt-3 flex items-stretch overflow-hidden rounded-md border">
                      <code class="min-w-0 flex-1 truncate px-3 py-2 font-mono text-xs">{{ referralLink }}</code>
                      <button
                        ui-button
                        variant="ghost"
                        class="border-border h-auto shrink-0 rounded-none border-l px-3"
                        aria-label="Copy referral link"
                        (click)="copyLink()"
                      >
                        @if (copied()) {
                          <lucide-icon [img]="Check" class="text-success size-4" aria-hidden="true" />
                        } @else {
                          <lucide-icon [img]="Copy" class="size-4" aria-hidden="true" />
                        }
                      </button>
                    </div>
                  </div>
                }
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiWaitlistReferralPositionComponent {
      protected readonly Check = Check
      protected readonly Copy = Copy
      protected readonly Users = Users
    
      protected readonly referralLink = referralLink
    
      @Input() set position(v: number) {
        this.positionSig.set(v ?? 1284)
      }
      get position(): number {
        return this.positionSig()
      }
      @Input() set nextTierAt(v: number) {
        this.nextTierAtSig.set(v ?? 1000)
      }
      get nextTierAt(): number {
        return this.nextTierAtSig()
      }
      @Input('class') className?: string
    
      private readonly positionSig = signal(1284)
      private readonly nextTierAtSig = signal(1000)
    
      readonly email = signal('')
      readonly joined = signal(false)
      readonly copied = signal(false)
      private copyTimer: ReturnType<typeof setTimeout> | undefined
    
      constructor() {
        inject(DestroyRef).onDestroy(() => clearTimeout(this.copyTimer))
      }
    
      // How far from the current position to the next access tier, as a percentage.
      readonly progress = computed(() => {
        const position = this.positionSig()
        const span = position - this.nextTierAtSig()
        if (span <= 0) return 100
        return Math.round(((position - span) / position) * 100)
      })
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    
      join(event: Event): void {
        event.preventDefault()
        if (!this.email().trim()) return
        this.joined.set(true)
      }
    
      async copyLink(): Promise<void> {
        try {
          await navigator.clipboard.writeText(referralLink)
          this.copied.set(true)
          clearTimeout(this.copyTimer)
          this.copyTimer = setTimeout(() => this.copied.set(false), 1600)
        } catch {
          // Clipboard blocked: the link stays visible and selectable.
        }
      }
    }
    
  • src/app/components/blocks/waitlist-referral-position/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/waitlist-referral-position.json