Release notes by email
One address on the release-notes list, subscribed through the server — a refusal marked on the field, a confirmation in its place; reads shippedLine().
Preview
"use client"
import * as React from "react"
import { type Result } from "@/lib/sample-data"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Widget } from "@/components/ui/widget"
import { subscribe } from "../actions"
const EMAIL_ID = "changelog-subscribe-email"
const MESSAGE_ID = "changelog-subscribe-error"
/**
* One address on the release-notes list.
*
* The action's answer is the only state: `useActionState` holds the pending
* flag, the refusal and the confirmation together, so there is nothing to keep
* in step. A refusal renders against the field it names — which is where the
* reader is already looking — rather than as a banner somewhere above it.
*/
export function SubscribeForm({ shipped }: { shipped: string }) {
const [email, setEmail] = React.useState("")
const [result, formAction, pending] = React.useActionState<
Result<{ email: string }> | null,
FormData
>((_previous, formData) => subscribe(formData), null)
const error = result && !result.ok ? result.error : null
return (
<Widget
data-widget="widget-support-changelog-subscribe-form"
title="Release notes by email"
description="One message per release, nothing else"
footer={shipped}
>
{result?.ok ? (
<p role="status" className="text-sm text-muted-foreground">
<span className="text-foreground">{result.data.email}</span> is on the list. Every
release note will arrive there the day it ships; every message carries a link to stop.
</p>
) : (
<form action={formAction} noValidate className="flex flex-col gap-3 sm:max-w-md">
<div className="flex flex-col gap-1.5">
<Label htmlFor={EMAIL_ID}>Email</Label>
<Input
id={EMAIL_ID}
name="email"
type="email"
autoComplete="email"
placeholder="you@example.com"
required
value={email}
onChange={(event) => setEmail(event.target.value)}
aria-invalid={error ? true : undefined}
aria-describedby={error ? MESSAGE_ID : undefined}
/>
{error ? (
<p id={MESSAGE_ID} role="alert" className="text-sm text-danger">
{error.message}
</p>
) : null}
</div>
<Button type="submit" disabled={pending} className="sm:self-start">
{pending ? "Subscribing…" : "Subscribe"}
</Button>
</form>
)}
</Widget>
)
}Install
$
npx shadcn@latest add @vibra/widget-support-changelog-subscribe-formNeeds the @vibra registry in your components.json — set it up once.
Source
"use client"
import * as React from "react"
import { type Result } from "@/lib/sample-data"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Widget } from "@/components/ui/widget"
import { subscribe } from "../actions"
const EMAIL_ID = "changelog-subscribe-email"
const MESSAGE_ID = "changelog-subscribe-error"
/**
* One address on the release-notes list.
*
* The action's answer is the only state: `useActionState` holds the pending
* flag, the refusal and the confirmation together, so there is nothing to keep
* in step. A refusal renders against the field it names — which is where the
* reader is already looking — rather than as a banner somewhere above it.
*/
export function SubscribeForm({ shipped }: { shipped: string }) {
const [email, setEmail] = React.useState("")
const [result, formAction, pending] = React.useActionState<
Result<{ email: string }> | null,
FormData
>((_previous, formData) => subscribe(formData), null)
const error = result && !result.ok ? result.error : null
return (
<Widget
data-widget="widget-support-changelog-subscribe-form"
title="Release notes by email"
description="One message per release, nothing else"
footer={shipped}
>
{result?.ok ? (
<p role="status" className="text-sm text-muted-foreground">
<span className="text-foreground">{result.data.email}</span> is on the list. Every
release note will arrive there the day it ships; every message carries a link to stop.
</p>
) : (
<form action={formAction} noValidate className="flex flex-col gap-3 sm:max-w-md">
<div className="flex flex-col gap-1.5">
<Label htmlFor={EMAIL_ID}>Email</Label>
<Input
id={EMAIL_ID}
name="email"
type="email"
autoComplete="email"
placeholder="you@example.com"
required
value={email}
onChange={(event) => setEmail(event.target.value)}
aria-invalid={error ? true : undefined}
aria-describedby={error ? MESSAGE_ID : undefined}
/>
{error ? (
<p id={MESSAGE_ID} role="alert" className="text-sm text-danger">
{error.message}
</p>
) : null}
</div>
<Button type="submit" disabled={pending} className="sm:self-start">
{pending ? "Subscribing…" : "Subscribe"}
</Button>
</form>
)}
</Widget>
)
}"use server"
import { mockAuthAdapter } from "@/lib/auth-adapter"
import { invalidInput, isForm, type Result } from "@/lib/sample-data"
import { isEmail } from "@/lib/validation"
/**
* Puts an address on the release-notes list.
*
* There is no subscriber entity, and inventing one for a demo would be a
* fiction with rows behind it; this validates the address and answers, which
* is the whole of the contract a consumer replaces. Swap the body for a write
* to your own list and nothing above this file moves.
*/
export async function subscribe(formData: FormData): Promise<Result<{ email: string }>> {
if (!isForm(formData)) return invalidInput("Send the address as the form sends it.", "email")
const raw = formData.get("email")
const email = typeof raw === "string" ? raw.trim() : ""
if (!isEmail(email)) {
return {
ok: false,
error: { code: "invalid_input", field: "email", message: "Enter a valid email address." },
}
}
return { ok: true, data: { email } }
}
/**
* The other thing this page changes: signing out. A server action so the page
* can stay a server component and still hand the shell something to call.
*/
export async function signOut(): Promise<Result<{ signedOut: true }>> {
await mockAuthAdapter.signOut()
return { ok: true, data: { signedOut: true } }
}Its page
On its page the card sits among the rest of the dashboard and shares its range and its data with them.
From the Product changelog page