Newsletter Signup
blockmarketingEngineering dispatch and release radar workbench with channel tracks (Architecture, Releases, Security), live archive previews, and subscriber metrics.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/newsletter-signup.json$npx shadcn@latest add https://uipkge.dev/r/react/newsletter-signup.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/newsletter-signup.json$bunx shadcn@latest add https://uipkge.dev/r/react/newsletter-signup.jsonNamed registry:
npx shadcn@latest add @uipkge-react/newsletter-signupInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'centered' | 'split' | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ChannelOptioninterface ChannelOption {
id: string
title: string
frequency: string
description: string
badge: string
}Files installed (1)
components/blocks/NewsletterSignup.tsx11.3 kB
'use client' import * as React from 'react' import { ArrowRight, BellRing, BookOpen, Check, Mail, Send } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' import { cn } from '@/lib/utils' export interface NewsletterSignupProps { variant?: 'centered' | 'split' } interface ChannelOption { id: string title: string frequency: string description: string badge: string } const channels: 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', }, ] 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.', }, ] export function NewsletterSignup({ variant = 'split' }: NewsletterSignupProps) { const [email, setEmail] = React.useState('') const [selectedChannel, setSelectedChannel] = React.useState('architecture') const [submitted, setSubmitted] = React.useState(false) const [activePreviewIssue, setActivePreviewIssue] = React.useState(0) const [isSubmitting, setIsSubmitting] = React.useState(false) const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault() if (!email || !email.includes('@')) return setIsSubmitting(true) setTimeout(() => { setIsSubmitting(false) setSubmitted(true) }, 400) } return ( <section data-slot="newsletter-signup" className="bg-background relative overflow-hidden px-4 py-16 sm:px-6 lg:px-8" > <div className="mx-auto max-w-6xl space-y-10"> {/* Section Header */} <div className="max-w-3xl space-y-3"> <Badge variant="secondary" className="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs"> <Mail className="text-primary size-3.5" /> Engineering Dispatch & Architecture Radar </Badge> <h2 className="text-foreground text-3xl font-bold tracking-tight sm:text-4xl"> Ship notes, not marketing fluff. </h2> <p className="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 className="grid grid-cols-1 items-start gap-8 lg:grid-cols-12"> {/* Left Column: Channel Selector & Live Dispatch Form (7 Cols) */} <div className="space-y-6 lg:col-span-7"> <Card className="border-border bg-card overflow-hidden shadow-sm"> <CardHeader className="bg-muted/20 border-border/60 border-b pb-4"> <div className="flex items-center justify-between"> <CardTitle className="flex items-center gap-2 text-base font-semibold"> <BellRing className="text-primary size-4" /> Select Dispatch Track </CardTitle> <span className="text-muted-foreground font-mono text-xs">Zero Spam Guarantee</span> </div> <CardDescription className="text-xs"> Choose the telemetry streams you want delivered to your inbox. </CardDescription> </CardHeader> <CardContent className="space-y-5 p-6"> {/* Channel Radio Cards */} <div className="space-y-2.5"> {channels.map((ch) => { const isSelected = selectedChannel === ch.id return ( <div key={ch.id} className={cn( 'group cursor-pointer rounded-xl border p-3.5 transition-colors', isSelected ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1' : 'border-border bg-background hover:bg-muted/40', )} onClick={() => setSelectedChannel(ch.id)} > <div className="flex items-center justify-between gap-2"> <div className="flex items-center gap-2"> <div className={cn( 'flex size-3.5 items-center justify-center rounded-full border transition-colors', isSelected ? 'border-primary bg-primary' : 'border-muted-foreground/40', )} > {isSelected && <div className="bg-primary-foreground size-1.5 rounded-full" />} </div> <span className="text-foreground text-xs font-semibold">{ch.title}</span> </div> <Badge variant="outline" className="font-mono text-xs"> {ch.frequency} </Badge> </div> <p className="text-muted-foreground mt-1.5 pl-5.5 text-xs leading-relaxed">{ch.description}</p> </div> ) })} </div> <Separator /> {/* Dispatch Subscription Input */} {!submitted ? ( <form className="space-y-3" onSubmit={handleSubmit}> <div className="flex flex-col gap-2 sm:flex-row"> <Input type="email" placeholder="architect@company.com" value={email} onChange={(e) => setEmail(e.target.value)} required className="h-10 flex-1 font-mono text-xs" aria-label="Email address for dispatch" /> <Button type="submit" disabled={isSubmitting} className="h-10 gap-1.5 px-5 text-xs font-semibold"> <span>Subscribe to Stream</span> <Send className="size-3.5" /> </Button> </div> <div className="text-muted-foreground flex items-center justify-between pt-1 font-mono text-xs"> <span>Plain text • 1-click unsubscribe</span> <span className="text-success font-semibold">18,412 Active Subscribers</span> </div> </form> ) : ( <div className="border-success/20 bg-success/10 space-y-2 rounded-xl border p-5 text-center"> <div className="bg-success/20 text-success mx-auto flex size-9 items-center justify-center rounded-full"> <Check className="size-5" /> </div> <p className="text-foreground text-sm font-semibold">Dispatches Activated!</p> <p className="text-muted-foreground text-xs"> Verification sent to <strong className="text-foreground font-mono">{email}</strong>. Check your inbox to confirm delivery. </p> </div> )} </CardContent> </Card> </div> {/* Right Column: Recent Issue Sampler & Archives (5 Cols) */} <div className="space-y-4 lg:col-span-5"> <div className="flex items-center justify-between"> <p className="text-muted-foreground flex items-center gap-1.5 font-mono text-xs tracking-wider uppercase"> <BookOpen className="text-primary size-3.5" /> Archive Sampler </p> <span className="text-muted-foreground font-mono text-xs"> Issue #{recentIssues[activePreviewIssue].id} </span> </div> {/* Interactive Issue Cards */} <div className="space-y-3"> {recentIssues.map((issue, idx) => ( <div key={issue.id} className={cn( 'bg-card cursor-pointer rounded-xl border p-4 transition-colors', activePreviewIssue === idx ? 'border-primary/50 shadow-xs' : 'border-border hover:border-border/80', )} onClick={() => setActivePreviewIssue(idx)} > <div className="text-muted-foreground mb-1.5 flex items-center justify-between font-mono text-xs"> <span> {issue.date} • Issue #{issue.id} </span> <span className="text-foreground font-medium">{issue.reads}</span> </div> <h3 className="text-foreground mb-1.5 text-sm leading-snug font-bold">{issue.title}</h3> <p className="text-muted-foreground text-xs leading-relaxed">{issue.snippet}</p> <div className="mt-3 flex flex-wrap gap-1.5"> {issue.tags.map((tag) => ( <Badge key={tag} variant="secondary" className="px-1.5 py-0 font-mono text-xs"> #{tag} </Badge> ))} </div> </div> ))} </div> {/* RSS note */} <div className="border-border bg-muted/20 text-muted-foreground flex items-center justify-between rounded-lg border p-3.5 text-xs"> <span className="font-mono">Prefer RSS feeds?</span> <a href="https://uipkge.dev/rss.xml" target="_blank" rel="noreferrer" className="text-primary inline-flex items-center gap-1 font-mono hover:underline" > uipkge.dev/rss.xml <ArrowRight className="size-3" /> </a> </div> </div> </div> </div> </section> ) } export default NewsletterSignup
Raw manifest:https://uipkge.dev/r/react/newsletter-signup.json