components/forms/newsletter-form.tsx "use client";
// Icon Library
import { IconChevronRight, IconLoader2 } from "@tabler/icons-react";
// Tanstack Form: https://ui.shadcn.com/docs/forms/tanstack-form
import { useForm } from "@tanstack/react-form";
// Toaster for alerts: https://ui.shadcn.com/docs/components/radix/sonner
import { toast } from "sonner";
import * as z from "zod";
// Components by shadcn/ui
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
const formSchema = z.object({
email: z.email({
error: "Please enter a valid email."
}),
});
export default function NewsletterForm() {
const form = useForm({
defaultValues: {
email: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
try {
// Simulate a network request; replace with your actual API call
// e.g. await fetch("/api/newsletter", { method: "POST", body: JSON.stringify(value) })
await new Promise((resolve) => setTimeout(resolve, 2000))
// On success, you can trigger additional side effects here
// e.g. router.push("/thank-you") or analytics.track("newsletter_subscribed")
toast.success("Subscribed!", {
description: `With the email '${value.email}'`
})
} catch {
// Handle specific server errors here if needed
// e.g. show a different message for 409 (already subscribed) vs. network failures
toast.error(
"Subscription failed. Check your email address and try again.",
);
}
},
});
return (
<div className="w-full">
<h3 className="text-sm text-muted-foreground mb-4">Subscribe to our <span className="text-foreground">Newsletter</span></h3>
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<FieldGroup className="shrink-0 flex-1 relative">
<form.Field name="email">
{(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid;
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name} className="sr-only">Email</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
autoComplete="off"
placeholder={"yourname@email.com"}
className="h-12 bg-background"
/>
{isInvalid && (
<FieldError className="text-xs" errors={field.state.meta.errors} />
)}
</Field>
);
}}
</form.Field>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<Button
variant={"inverted"}
size={"icon"}
className="shrink-0 absolute right-1.5 top-1.5"
type="submit"
disabled={!canSubmit}
>
{isSubmitting ? <IconLoader2 className="animate-spin" /> : <IconChevronRight />}
</Button>
)}
</form.Subscribe>
</FieldGroup>
</form>
</div >
);
}