Menu

Change language

GitHub repo

Waitlist

blockmarketing

Pre-launch signup page. Split layout with product pitch, logo mark, three benefit bullets on the left and an email capture card on the right that flips to a queue-position confirmation with referral hint after submit.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/waitlist.json
Manifest:https://uipkge.dev/r/lit/waitlist.jsonInstalls to:src/uip/blocks/waitlist/

Variants

Loading interactive previews…

Schema

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

HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-waitlist': UipWaitlist
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/waitlist/waitlist.ts6.8 kB
    import { LitElement, css, html } from 'lit'
    import { Check, Layers, Link2, ShieldCheck, Zap } from 'lucide'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    import { icon } from '../../lib/icon'
    // Side-effect imports: register the composed elements for standalone use
    // (consumers who install only this block still get working primitives).
    import '../../components/button/button'
    import '../../components/card/card'
    import '../../components/input/input'
    import type { UipInput } from '../../components/input/input'
    
    const benefits = [
      { iconName: 'zap', title: 'Ships in minutes', copy: 'Drop-in blocks and primitives, not boilerplate to rewrite.' },
      {
        iconName: 'shield-check',
        title: 'Own the code',
        copy: 'Every file is copied into your repo. Edit it freely, no lock-in.',
      },
      {
        iconName: 'layers',
        title: 'Dual framework',
        copy: 'Vue and React mirrors stay in lockstep across every component.',
      },
    ] as const
    
    /**
     * <uip-waitlist> — the registry Waitlist block as a web component.
     *
     * Class strings are React's Waitlist verbatim. React state (useState) becomes
     * @state fields. React has no submit callback; the form dispatches a
     * `subscribe` CustomEvent (detail: { email }, bubbles, composed). The benefit
     * icons resolve by name (React renders component references; lucide nodes
     * don't carry their kebab-case name, which icon() needs).
     */
    export class UipWaitlist extends LitElement {
      static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
      // :host display can't be a utility class (the host has no template of its own).
      static styles = [tailwind, css`:host { display: block; }`]
    
      static properties = {
        email: { state: true },
        submitted: { state: true },
        position: { state: true },
      }
    
      private email = ''
      private submitted = false
      private position = 214
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'waitlist')
      }
    
      private benefitIcon(name: string, className: string) {
        // Mirrors React's <benefit.icon>: Zap / ShieldCheck / Layers in row order.
        if (name === 'shield-check') return icon(ShieldCheck, 'shield-check', className)
        if (name === 'layers') return icon(Layers, 'layers', className)
        return icon(Zap, 'zap', className)
      }
    
      private handleSubmit(e: Event) {
        e.preventDefault()
        if (!this.email.includes('@')) return
        this.submitted = true
        this.dispatchEvent(new CustomEvent('subscribe', { detail: { email: this.email }, bubbles: true, composed: true }))
      }
    
      render() {
        return html`
          <section data-slot="waitlist" class="bg-background grid min-h-screen lg:grid-cols-2">
            <div class="bg-muted/40 relative flex items-center px-6 py-16 lg:border-r lg:px-16">
              <div class="max-w-lg">
                <div class="flex items-center gap-3">
                  <div class="bg-primary text-primary-foreground grid size-10 place-items-center rounded-lg shadow-sm">
                    ${icon(Layers, 'layers', 'size-5')}
                  </div>
                  <span class="text-sm font-semibold tracking-tight">Northwind</span>
                </div>
    
                <h1 class="mt-8 text-3xl font-semibold tracking-tight sm:text-4xl">Be first in line when we launch</h1>
                <p class="text-muted-foreground mt-4 text-base sm:text-lg">
                  We&rsquo;re onboarding a small cohort of teams each week. Join the waitlist and we&rsquo;ll save your
                  spot.
                </p>
    
                <ul class="mt-10 space-y-6">
                  ${benefits.map(
                    (benefit) => html`
                      <li class="flex gap-4">
                        <div class="bg-primary/10 text-primary grid size-9 shrink-0 place-items-center rounded-lg">
                          ${this.benefitIcon(benefit.iconName, 'size-4.5')}
                        </div>
                        <div>
                          <p class="text-sm font-medium">${benefit.title}</p>
                          <p class="text-muted-foreground mt-1 text-sm">${benefit.copy}</p>
                        </div>
                      </li>
                    `,
                  )}
                </ul>
              </div>
            </div>
    
            <div class="flex items-center justify-center px-6 py-16">
              <uip-card class="w-full max-w-md">
                ${!this.submitted
                  ? html`
                      <uip-card-header>
                        <uip-card-title>Join the waitlist</uip-card-title>
                        <uip-card-description>
                          Early access rolls out weekly. No spam &mdash; one email when it&rsquo;s your turn.
                        </uip-card-description>
                      </uip-card-header>
                      <uip-card-content>
                        <form class="space-y-3" @submit=${this.handleSubmit}>
                          <uip-input
                            type="email"
                            placeholder="[email protected]"
                            .value=${this.email}
                            required
                            @input=${(e: Event) => {
                              this.email = (e.target as UipInput).value
                            }}
                          ></uip-input>
                          <uip-button type="submit" class="w-full">Request access</uip-button>
                        </form>
                        <p class="text-muted-foreground mt-4 text-xs">2,300+ teams already in line</p>
                      </uip-card-content>
                    `
                  : html`
                      <uip-card-content class="pt-6 text-center">
                        <div class="bg-primary/10 text-primary mx-auto grid size-12 place-items-center rounded-full">
                          ${icon(Check, 'check', 'size-6')}
                        </div>
                        <h2 class="mt-5 text-2xl font-semibold tracking-tight">You&rsquo;re #${this.position} in line</h2>
                        <p class="text-muted-foreground mt-2 text-sm">
                          We&rsquo;ll email ${this.email} as soon as your spot opens up.
                        </p>
                        <div class="bg-muted/50 mt-6 flex items-start gap-3 rounded-lg border p-4 text-left">
                          ${icon(Link2, 'link-2', 'text-muted-foreground mt-0.5 size-4 shrink-0')}
                          <p class="text-muted-foreground text-xs">
                            Want to skip the queue? Share your invite link after launch &mdash; every friend who signs up
                            moves you up 5 spots.
                          </p>
                        </div>
                      </uip-card-content>
                    `}
              </uip-card>
            </div>
          </section>
        `
      }
    }
    
    customElements.get('uip-waitlist') || customElements.define('uip-waitlist', UipWaitlist)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-waitlist': UipWaitlist
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/waitlist.json