Waitlist — Referral Position
blockmarketingWaitlist 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
$pnpm dlx uipkge-ng@latest add waitlist-referral-position$npx uipkge-ng@latest add waitlist-referral-position$yarn dlx uipkge-ng@latest add waitlist-referral-position$bunx uipkge-ng@latest add waitlist-referral-positionManifest:
https://uipkge.dev/r/angular/waitlist-referral-position.jsonInstalls to:src/app/components/blocks/waitlist-referral-position/Variants
Loading interactive previews…
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="[email protected]" 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
export { UiWaitlistReferralPositionComponent } from './waitlist-referral-position.component'
Raw manifest:https://uipkge.dev/r/angular/waitlist-referral-position.json