Menu

Change language

GitHub repo

Newsletter Signup

blockmarketing

Engineering dispatch and release radar workbench with channel tracks (Architecture, Releases, Security), live archive previews, and subscriber metrics.

Installation

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

Variants

Loading interactive previews…

Schema

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

ChannelOption
interface ChannelOption {
  id: string
  title: string
  frequency: string
  description: string
  badge: string
}

Files installed (2)

  • src/app/components/blocks/newsletter-signup/newsletter-signup.component.ts12.9 kB
    // Engineering dispatch signup: channel track selector, live dispatch form with
    // success state, archive sampler, and RSS note. Port of the Vue/React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import { ArrowRight, BellRing, BookOpen, Check, LucideAngularModule, Mail, Send } 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,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    export type NewsletterSignupVariant = 'centered' | 'split'
    
    interface ChannelOption {
      id: string
      title: string
      frequency: string
      description: string
      badge: string
    }
    
    export const newsletterChannels: ChannelOption[] = [
      {
        id: 'architecture',
        title: 'Architecture & Token Deep Dives',
        frequency: 'Bi-Weekly',
        description: 'Technical breakdowns on OKLCH color science, headless AST transformers, and zero-lockin paradigms.',
        badge: 'Flagship',
      },
      {
        id: 'releases',
        title: 'Registry Ship Notes & RFCs',
        frequency: 'Monthly',
        description: 'Direct changelogs, new workbench releases, breaking primitive refactors, and roadmap discussions.',
        badge: 'Changelog',
      },
      {
        id: 'security',
        title: 'Security & Dependency Bulletins',
        frequency: 'As-Needed',
        description: 'Zero-day vulnerability alerts, upstream headless primitive fixes, and patch notifications.',
        badge: 'Critical',
      },
    ]
    
    export const recentIssues = [
      {
        id: 48,
        date: 'Aug 2026',
        title: 'The Death of Monolithic npm UI Packages',
        reads: '4 min read',
        tags: ['Architecture', 'Registry', 'AST'],
        snippet:
          'Why the next decade of frontend engineering belongs to unbundled registries where the consumer owns the source code.',
      },
      {
        id: 47,
        date: 'Jul 2026',
        title: 'OKLCH Theming in Tailwind CSS v4',
        reads: '6 min read',
        tags: ['Tailwind v4', 'Color Science', 'Tokens'],
        snippet:
          'Mastering perceptual lightness, chroma gamut mapping, and dynamic theme switching without runtime CSS bloat.',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-newsletter-signup, [ui-newsletter-signup]',
      standalone: true,
      // The block renders its own root <section>: keep the host out of layout.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiInputComponent,
        UiSeparatorComponent,
      ],
      template: `
        <section data-slot="newsletter-signup" [class]="rootClass">
          <div class="mx-auto max-w-6xl space-y-10">
            <!-- Section Header -->
            <div class="max-w-3xl space-y-3">
              <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs">
                <lucide-icon [img]="Mail" class="text-primary size-3.5" />
                Engineering Dispatch & Architecture Radar
              </span>
              <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
                Ship notes, not marketing fluff.
              </h2>
              <p class="text-muted-foreground text-sm leading-relaxed sm:text-base">
                Join 18,400+ frontend architects, design engineers, and systems builders. Plain-text insights into component
                architecture, token systems, and dual-framework engineering.
              </p>
            </div>
    
            <!-- Main Layout -->
            <div class="grid grid-cols-1 items-start gap-8 lg:grid-cols-12">
              <!-- Left Column: Channel Selector & Live Dispatch Form (7 Cols) -->
              <div class="space-y-6 lg:col-span-7">
                <div ui-card class="border-border bg-card overflow-hidden shadow-sm">
                  <div ui-card-header class="bg-muted/20 border-border/60 border-b pb-4">
                    <div class="flex items-center justify-between">
                      <h3 ui-card-title class="flex items-center gap-2 text-base font-semibold">
                        <lucide-icon [img]="BellRing" class="text-primary size-4" />
                        Select Dispatch Track
                      </h3>
                      <span class="text-muted-foreground font-mono text-xs">Zero Spam Guarantee</span>
                    </div>
                    <p ui-card-description class="text-xs">
                      Choose the telemetry streams you want delivered to your inbox.
                    </p>
                  </div>
    
                  <div ui-card-content class="space-y-5 p-6">
                    <!-- Channel Radio Cards -->
                    <div class="space-y-2.5">
                      @for (ch of channels; track ch.id) {
                        <div
                          class="group cursor-pointer rounded-xl border p-3.5 transition-colors"
                          [class]="channelCardClass(ch.id)"
                          (click)="selectedChannel.set(ch.id)"
                          (keydown.enter)="selectedChannel.set(ch.id)"
                          tabindex="0"
                          role="radio"
                          [attr.aria-checked]="selectedChannel() === ch.id"
                        >
                          <div class="flex items-center justify-between gap-2">
                            <div class="flex items-center gap-2">
                              <div
                                class="flex size-3.5 items-center justify-center rounded-full border transition-colors"
                                [class]="channelDotClass(ch.id)"
                              >
                                @if (selectedChannel() === ch.id) {
                                  <div class="bg-primary-foreground size-1.5 rounded-full"></div>
                                }
                              </div>
                              <span class="text-foreground text-xs font-semibold">{{ ch.title }}</span>
                            </div>
                            <span ui-badge variant="outline" class="font-mono text-xs">{{ ch.frequency }}</span>
                          </div>
                          <p class="text-muted-foreground mt-1.5 pl-5.5 text-xs leading-relaxed">{{ ch.description }}</p>
                        </div>
                      }
                    </div>
    
                    <ui-separator />
    
                    <!-- Dispatch Subscription Input -->
                    @if (!submitted()) {
                      <form class="space-y-3" (submit)="handleSubmit($event)">
                        <div class="flex flex-col gap-2 sm:flex-row">
                          <ui-input
                            [value]="email()"
                            (valueChange)="email.set($event)"
                            type="email"
                            placeholder="[email protected]"
                            required
                            class="h-10 flex-1 font-mono text-xs"
                            aria-label="Email address for dispatch"
                          />
                          <button
                            ui-button
                            type="submit"
                            [disabled]="isSubmitting()"
                            class="h-10 gap-1.5 px-5 text-xs font-semibold"
                          >
                            <span>Subscribe to Stream</span>
                            <lucide-icon [img]="Send" class="size-3.5" />
                          </button>
                        </div>
                        <div class="text-muted-foreground flex items-center justify-between pt-1 font-mono text-xs">
                          <span>Plain text &bull; 1-click unsubscribe</span>
                          <span class="text-success font-semibold">18,412 Active Subscribers</span>
                        </div>
                      </form>
                    } @else {
                      <div class="border-success/20 bg-success/10 space-y-2 rounded-xl border p-5 text-center">
                        <div
                          class="bg-success/20 text-success mx-auto flex size-9 items-center justify-center rounded-full"
                        >
                          <lucide-icon [img]="Check" class="size-5" />
                        </div>
                        <p class="text-foreground text-sm font-semibold">Dispatches Activated!</p>
                        <p class="text-muted-foreground text-xs">
                          Verification sent to <strong class="text-foreground font-mono">{{ email() }}</strong
                          >. Check your inbox to confirm delivery.
                        </p>
                      </div>
                    }
                  </div>
                </div>
              </div>
    
              <!-- Right Column: Recent Issue Sampler & Archives (5 Cols) -->
              <div class="space-y-4 lg:col-span-5">
                <div class="flex items-center justify-between">
                  <p class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs tracking-wider uppercase">
                    <lucide-icon [img]="BookOpen" class="text-primary size-3.5" /> Archive Sampler
                  </p>
                  <span class="text-muted-foreground font-mono text-xs">Issue #{{ issues[activePreviewIssue()].id }}</span>
                </div>
    
                <!-- Interactive Issue Cards -->
                <div class="space-y-3">
                  @for (issue of issues; track issue.id; let idx = $index) {
                    <div
                      class="bg-card cursor-pointer rounded-xl border p-4 transition-colors"
                      [class]="previewCardClass(idx)"
                      (click)="activePreviewIssue.set(idx)"
                      (keydown.enter)="activePreviewIssue.set(idx)"
                      tabindex="0"
                      role="button"
                      [attr.aria-pressed]="activePreviewIssue() === idx"
                    >
                      <div class="text-muted-foreground mb-1.5 flex items-center justify-between font-mono text-xs">
                        <span>{{ issue.date }} &bull; Issue #{{ issue.id }}</span>
                        <span class="text-foreground font-medium">{{ issue.reads }}</span>
                      </div>
                      <h3 class="text-foreground mb-1.5 text-sm leading-snug font-bold">{{ issue.title }}</h3>
                      <p class="text-muted-foreground text-xs leading-relaxed">{{ issue.snippet }}</p>
    
                      <div class="mt-3 flex flex-wrap gap-1.5">
                        @for (tag of issue.tags; track tag) {
                          <span ui-badge variant="secondary" class="px-1.5 py-0 font-mono text-xs">#{{ tag }}</span>
                        }
                      </div>
                    </div>
                  }
                </div>
    
                <!-- RSS / Mastodon / CLI note -->
                <div
                  class="border-border bg-muted/20 text-muted-foreground flex items-center justify-between rounded-lg border p-3.5 text-xs"
                >
                  <span class="font-mono">Prefer RSS feeds?</span>
                  <a
                    href="https://uipkge.dev/rss.xml"
                    target="_blank"
                    rel="noreferrer"
                    class="text-primary inline-flex items-center gap-1 font-mono hover:underline"
                  >
                    uipkge.dev/rss.xml <lucide-icon [img]="ArrowRight" class="size-3" />
                  </a>
                </div>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiNewsletterSignupComponent {
      protected readonly ArrowRight = ArrowRight
      protected readonly BellRing = BellRing
      protected readonly BookOpen = BookOpen
      protected readonly Check = Check
      protected readonly Mail = Mail
      protected readonly Send = Send
    
      protected readonly channels = newsletterChannels
      protected readonly issues = recentIssues
    
      @Input() variant: NewsletterSignupVariant = 'split'
      @Input('class') className?: string
    
      readonly email = signal('')
      readonly selectedChannel = signal('architecture')
      readonly submitted = signal(false)
      readonly activePreviewIssue = signal(0)
      readonly isSubmitting = signal(false)
    
      get rootClass(): string {
        return cn('bg-background relative overflow-hidden px-4 py-16 sm:px-6 lg:px-8', this.className)
      }
    
      channelCardClass(id: string): string {
        return cn(
          this.selectedChannel() === id
            ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1'
            : 'border-border bg-background hover:bg-muted/40',
        )
      }
    
      channelDotClass(id: string): string {
        return cn(this.selectedChannel() === id ? 'border-primary bg-primary' : 'border-muted-foreground/40')
      }
    
      previewCardClass(idx: number): string {
        return cn(
          this.activePreviewIssue() === idx ? 'border-primary/50 shadow-xs' : 'border-border hover:border-border/80',
        )
      }
    
      handleSubmit(event?: Event): void {
        event?.preventDefault()
        if (!this.email() || !this.email().includes('@')) return
        this.isSubmitting.set(true)
        setTimeout(() => {
          this.isSubmitting.set(false)
          this.submitted.set(true)
        }, 400)
      }
    }
    
  • src/app/components/blocks/newsletter-signup/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/newsletter-signup.json