Menu

Change language

GitHub repo

Lead Capture Popup

blockmarketing

High-conversion exit-intent discount popup and newsletter lead generator with live ticking countdown timer, emerald value props, star rating social proof, and coupon code reveal.

Installation

$npx uipkge-ng@latest add lead-capture-popup
Manifest:https://uipkge.dev/r/angular/lead-capture-popup.jsonInstalls to:src/app/components/blocks/lead-capture-popup/

Variants

Loading interactive previews…

npm dependencies

Files installed (2)

  • src/app/components/blocks/lead-capture-popup/lead-capture-popup.component.ts14.2 kB
    // High-conversion exit-intent discount popup and newsletter lead generator with a live
    // ticking countdown timer, value props, star-rating social proof, and coupon code reveal.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output, signal } from '@angular/core'
    import {
      ArrowRight,
      Check,
      CheckCircle2,
      Clock,
      Copy,
      Gift,
      LucideAngularModule,
      RotateCcw,
      Star,
      X,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-lead-capture-popup, [ui-lead-capture-popup]',
      standalone: true,
      // The block renders its own root <div> (form vs dismissed fallback): keep the host out of layout.
      host: { '[attr.class]': '"contents"' },
      imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiInputComponent],
      template: `
        @if (!isDismissed()) {
          <div data-slot="lead-capture-popup" [class]="rootClass">
            <div
              class="bg-card text-card-foreground border-border relative mx-auto w-full max-w-lg overflow-hidden rounded-2xl border p-6 shadow-xl sm:p-8"
            >
              <!-- Top Right Dismiss Button -->
              <button
                type="button"
                aria-label="Dismiss popup"
                class="text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute top-4 right-4 rounded-full p-1.5 transition-colors focus-visible:ring-2 focus-visible:outline-hidden"
                (click)="handleDismiss()"
              >
                <lucide-icon [img]="X" class="size-4" />
              </button>
    
              <!-- FORM STATE -->
              @if (!isSubmitted()) {
                <div class="relative z-10 space-y-6">
                  <!-- Top Badge -->
                  <div class="flex items-center">
                    <span
                      ui-badge
                      variant="secondary"
                      class="border-primary/20 bg-primary/10 text-primary inline-flex items-center gap-1.5 px-3 py-1 text-xs font-medium"
                    >
                      <lucide-icon [img]="Gift" class="text-primary size-3.5" />
                      Limited Time Welcome Gift
                    </span>
                  </div>
    
                  <!-- Urgency Countdown Bar -->
                  <div
                    class="border-warning/20 bg-warning/10 text-foreground text-warning flex items-center justify-between gap-2 rounded-lg border px-3.5 py-2 text-xs"
                  >
                    <div class="flex items-center gap-2">
                      <lucide-icon [img]="Clock" class="text-warning size-4 shrink-0" />
                      <span class="font-medium"
                        >Claim your {{ discountPercent }}% discount code before the timer expires:</span
                      >
                    </div>
                    <span
                      class="bg-warning/20 text-foreground dark:text-foreground rounded px-2 py-0.5 font-mono text-xs font-bold tabular-nums"
                    >
                      {{ formattedTime() }}
                    </span>
                  </div>
    
                  <!-- Headline & Subtitle -->
                  <div class="space-y-2">
                    <h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">
                      {{ headline }}
                    </h2>
                    <p class="text-muted-foreground text-sm leading-relaxed">
                      {{ subtitle }}
                    </p>
                  </div>
    
                  <!-- Value Bullet Points (3 items with success checkmarks) -->
                  <ul class="space-y-2.5" role="list">
                    <li class="text-muted-foreground flex items-center gap-2.5 text-xs sm:text-sm">
                      <span
                        class="bg-success/15 text-success flex size-4 shrink-0 items-center justify-center rounded-full"
                      >
                        <lucide-icon [img]="Check" class="size-2.5 stroke-[3]" />
                      </span>
                      <span>No spam ever &mdash; high-signal engineering teardowns</span>
                    </li>
                    <li class="text-muted-foreground flex items-center gap-2.5 text-xs sm:text-sm">
                      <span
                        class="bg-success/15 text-success flex size-4 shrink-0 items-center justify-center rounded-full"
                      >
                        <lucide-icon [img]="Check" class="size-2.5 stroke-[3]" />
                      </span>
                      <span>1-click unsubscribe anytime with zero friction</span>
                    </li>
                    <li class="text-muted-foreground flex items-center gap-2.5 text-xs sm:text-sm">
                      <span
                        class="bg-success/15 text-success flex size-4 shrink-0 items-center justify-center rounded-full"
                      >
                        <lucide-icon [img]="Check" class="size-2.5 stroke-[3]" />
                      </span>
                      <span>Instant coupon delivery straight to your screen &amp; inbox</span>
                    </li>
                  </ul>
    
                  <!-- Email Capture Form -->
                  <form class="space-y-3 pt-1" (submit)="handleSubmit($event)">
                    <ui-input
                      [value]="email()"
                      (valueChange)="email.set($event)"
                      type="email"
                      placeholder="engineer@company.com"
                      required
                      autoComplete="email"
                      aria-label="Email address"
                      class="border-border bg-background focus-visible:ring-primary h-11 text-sm shadow-xs focus-visible:ring-2"
                    />
    
                    <button
                      ui-button
                      type="submit"
                      size="lg"
                      class="h-11 w-full justify-center gap-2 text-sm font-semibold shadow-xs"
                    >
                      Claim My {{ discountPercent }}% Discount
                      <lucide-icon [img]="ArrowRight" class="size-4" />
                    </button>
                  </form>
    
                  <!-- Social Proof Line -->
                  <div class="text-muted-foreground flex items-center justify-center gap-1.5 text-xs">
                    <div class="text-warning flex items-center gap-0.5" aria-hidden="true">
                      <lucide-icon [img]="Star" class="size-3.5 fill-current" />
                      <lucide-icon [img]="Star" class="size-3.5 fill-current" />
                      <lucide-icon [img]="Star" class="size-3.5 fill-current" />
                      <lucide-icon [img]="Star" class="size-3.5 fill-current" />
                      <lucide-icon [img]="Star" class="size-3.5 fill-current" />
                    </div>
                    <span class="text-foreground font-medium">{{ socialProofRating }}</span>
                    <span>rating by {{ socialProofCount }}</span>
                  </div>
    
                  <!-- Dismiss / Decline link -->
                  <div class="text-center">
                    <button
                      type="button"
                      class="text-muted-foreground hover:text-foreground focus-visible:ring-ring min-h-6 rounded text-xs underline underline-offset-4 transition-colors focus-visible:ring-2 focus-visible:outline-hidden"
                      (click)="handleDismiss()"
                    >
                      No thanks, I prefer paying full price
                    </button>
                  </div>
                </div>
              } @else {
                <!-- SUCCESS STATE -->
                <div class="relative z-10 space-y-5 text-center">
                  <div class="bg-success/10 text-success mx-auto flex size-12 items-center justify-center rounded-full">
                    <lucide-icon [img]="CheckCircle2" class="size-6" />
                  </div>
    
                  <div class="space-y-1.5">
                    <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success font-medium">
                      {{ discountPercent }}% Discount Unlocked
                    </span>
                    <h3 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">You&rsquo;re all set!</h3>
                    <p class="text-muted-foreground text-xs sm:text-sm">
                      We&rsquo;ve dispatched your code and welcome gift to
                      <span class="text-foreground font-medium">{{ email() || 'engineer@company.com' }}</span
                      >.
                    </p>
                  </div>
    
                  <!-- Coupon Card Box -->
                  <div class="border-border bg-muted/40 relative space-y-3 rounded-xl border-2 border-dashed p-4">
                    <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                      Your Exclusive Coupon Code
                    </span>
                    <div class="flex items-center justify-center gap-2">
                      <code
                        class="border-border bg-background text-foreground rounded-lg border px-3 py-1.5 font-mono text-xl font-bold tracking-widest sm:text-2xl"
                      >
                        {{ discountCode }}
                      </code>
                      <button
                        ui-button
                        variant="secondary"
                        size="sm"
                        class="gap-1.5 shadow-xs"
                        [attr.aria-label]="copied() ? 'Copied code' : 'Copy discount code'"
                        (click)="copyCoupon()"
                      >
                        @if (copied()) {
                          <lucide-icon [img]="Check" class="text-success size-3.5" />
                        } @else {
                          <lucide-icon [img]="Copy" class="size-3.5" />
                        }
                        <span>{{ copied() ? 'Copied!' : 'Copy Code' }}</span>
                      </button>
                    </div>
                    <p class="text-muted-foreground text-xs">
                      Valid for the next <span class="font-mono font-medium">{{ formattedTime() }}</span> at checkout.
                    </p>
                  </div>
    
                  <div class="space-y-2 pt-2">
                    <button
                      ui-button
                      class="h-11 w-full justify-center gap-2 text-sm font-semibold shadow-xs"
                      (click)="handleDismiss()"
                    >
                      Start Exploring Components
                      <lucide-icon [img]="ArrowRight" class="size-4" />
                    </button>
                    <button
                      type="button"
                      class="text-muted-foreground hover:text-foreground focus-visible:ring-ring min-h-6 rounded text-xs underline underline-offset-4 transition-colors focus-visible:ring-2 focus-visible:outline-hidden"
                      (click)="handleReset()"
                    >
                      Enter a different email
                    </button>
                  </div>
                </div>
              }
            </div>
          </div>
        } @else {
          <!-- Reopen trigger fallback when dismissed -->
          <div class="flex flex-col items-center justify-center p-8 text-center" data-slot="lead-capture-popup-dismissed">
            <p class="text-muted-foreground text-sm">Popup was dismissed.</p>
            <button ui-button variant="outline" size="sm" class="mt-3 gap-2" (click)="handleReset()">
              <lucide-icon [img]="RotateCcw" class="text-primary size-3.5" />
              Reopen Welcome Popup
            </button>
          </div>
        }
      `,
    })
    export class UiLeadCapturePopupComponent implements OnInit, OnDestroy {
      protected readonly ArrowRight = ArrowRight
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly Gift = Gift
      protected readonly RotateCcw = RotateCcw
      protected readonly Star = Star
      protected readonly X = X
    
      @Input() headline = 'Unlock 20% Off Your First Order'
      @Input() subtitle =
        'Join 45,000+ engineers receiving our weekly curated component teardowns and architectural breakdowns.'
      @Input() discountCode = 'WELCOME20'
      @Input() discountPercent = 20
      @Input() initialSeconds = 599
      @Input() socialProofRating = '4.9/5'
      @Input() socialProofCount = '1,200+ developers'
      @Input() initialSubmitted = false
      @Input('class') className?: string
    
      @Output() readonly submit = new EventEmitter<{ email: string; discountCode: string }>()
      @Output() readonly dismiss = new EventEmitter<void>()
      @Output() readonly copy = new EventEmitter<string>()
      @Output('reset') readonly resetEmitter = new EventEmitter<void>()
    
      readonly email = signal('')
      readonly isSubmitted = signal(false)
      readonly isDismissed = signal(false)
      readonly copied = signal(false)
      readonly timeLeft = signal(599)
    
      private timer: ReturnType<typeof setInterval> | null = null
      private copyTimer: ReturnType<typeof setTimeout> | null = null
    
      ngOnInit(): void {
        this.isSubmitted.set(this.initialSubmitted)
        this.timeLeft.set(this.initialSeconds)
        this.timer = setInterval(() => {
          if (this.timeLeft() > 0) this.timeLeft.update((v) => v - 1)
          else if (this.timer) clearInterval(this.timer)
        }, 1000)
      }
    
      ngOnDestroy(): void {
        if (this.timer) clearInterval(this.timer)
        if (this.copyTimer) clearTimeout(this.copyTimer)
      }
    
      get rootClass(): string {
        return cn('w-full', this.className)
      }
    
      formattedTime(): string {
        const mins = Math.floor(this.timeLeft() / 60)
        const secs = this.timeLeft() % 60
        return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
      }
    
      handleSubmit(event?: Event): void {
        event?.preventDefault()
        const value = this.email()
        if (!value || !value.includes('@')) return
        this.isSubmitted.set(true)
        this.submit.emit({ email: value, discountCode: this.discountCode })
      }
    
      handleDismiss(): void {
        this.isDismissed.set(true)
        this.dismiss.emit()
      }
    
      handleReset(): void {
        this.isSubmitted.set(false)
        this.isDismissed.set(false)
        this.email.set('')
        this.copied.set(false)
        this.timeLeft.set(this.initialSeconds)
        this.resetEmitter.emit()
      }
    
      open(): void {
        this.isDismissed.set(false)
      }
    
      reset(): void {
        this.handleReset()
      }
    
      copyCoupon(): void {
        try {
          void navigator.clipboard?.writeText(this.discountCode)
        } catch {
          // Clipboard fallback
        }
        this.copied.set(true)
        this.copy.emit(this.discountCode)
        if (this.copyTimer) clearTimeout(this.copyTimer)
        this.copyTimer = setTimeout(() => this.copied.set(false), 2000)
      }
    }
    
  • src/app/components/blocks/lead-capture-popup/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/lead-capture-popup.json