Menu

Change language

GitHub repo

Exit Intent — Offer

blockmarketing

Dialog triggered when the pointer leaves toward the browser chrome, offering an extended trial once per session, with a dismissal that is remembered.

Installation

$npx uipkge-ng@latest add exit-intent-offer
Manifest:https://uipkge.dev/r/angular/exit-intent-offer.jsonInstalls to:src/app/components/blocks/exit-intent-offer/

Variants

Loading interactive previews…

Files installed (2)

  • src/app/components/blocks/exit-intent-offer/exit-intent-offer.component.ts3.7 kB
    // Extended-trial offer fired once per session when the pointer leaves toward the browser
    // chrome (top edge only — sideways exits are usually the scrollbar or another window).
    // Port of the Vue/React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, signal } from '@angular/core'
    import { ArrowRight, Clock, LucideAngularModule } from 'lucide-angular'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiDialogComponent,
      UiDialogContentComponent,
      UiDialogDescriptionComponent,
      UiDialogFooterComponent,
      UiDialogHeaderComponent,
      UiDialogTitleComponent,
    } from '@/ui/dialog/dialog.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-exit-intent-offer, [ui-exit-intent-offer]',
      standalone: true,
      // The block renders its own root <div> (and the dialog portals to body): keep the host out of layout.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiDialogComponent,
        UiDialogContentComponent,
        UiDialogDescriptionComponent,
        UiDialogFooterComponent,
        UiDialogHeaderComponent,
        UiDialogTitleComponent,
        UiSeparatorComponent,
      ],
      template: `
        <div data-slot="exit-intent-offer">
          <ui-dialog [open]="open()" (openChange)="open.set($event)">
            <ui-dialog-content class="sm:max-w-md">
              <div ui-dialog-header>
                <span ui-badge variant="secondary" class="w-fit gap-1.5">
                  <lucide-icon [img]="Clock" class="size-3" aria-hidden="true" />
                  Before you go
                </span>
                <h2 ui-dialog-title class="mt-3 text-xl leading-snug text-balance">Take 30 days instead of 14</h2>
                <p ui-dialog-description class="leading-relaxed">
                  A full close cycle fits in 30 days and 14 does not, which is the actual reason most trials stall. No card,
                  no call, cancel from the dashboard.
                </p>
              </div>
    
              <ui-separator />
    
              <ul class="text-muted-foreground space-y-1.5 text-sm">
                <li>· Everything on the Business plan</li>
                <li>· Connect your real warehouse, read-only</li>
                <li>· Keep the definitions you author either way</li>
              </ul>
    
              <div ui-dialog-footer class="sm:justify-start">
                <button ui-button>
                  Start the 30-day trial
                  <lucide-icon [img]="ArrowRight" class="ml-2 size-4" aria-hidden="true" />
                </button>
                <button ui-button variant="ghost" (click)="open.set(false)">No thanks</button>
              </div>
            </ui-dialog-content>
          </ui-dialog>
        </div>
      `,
    })
    export class UiExitIntentOfferComponent implements OnInit, OnDestroy {
      protected readonly ArrowRight = ArrowRight
      protected readonly Clock = Clock
    
      @Input() storageKey = 'uipkge:exit-intent-seen'
    
      readonly open = signal(false)
    
      private readonly onPointerOut = (event: MouseEvent): void => {
        if (event.relatedTarget || event.clientY > 4) return
        if (this.show()) document.removeEventListener('mouseout', this.onPointerOut)
      }
    
      ngOnInit(): void {
        document.addEventListener('mouseout', this.onPointerOut)
      }
    
      ngOnDestroy(): void {
        document.removeEventListener('mouseout', this.onPointerOut)
      }
    
      show(): boolean {
        try {
          if (window.sessionStorage.getItem(this.storageKey) === '1') return false
          window.sessionStorage.setItem(this.storageKey, '1')
        } catch {
          // Blocked storage: it can show once more this page view, never in a loop.
        }
        this.open.set(true)
        return true
      }
    }
    
  • src/app/components/blocks/exit-intent-offer/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/exit-intent-offer.json