Marketing
Newsletter Signup
A customizable newsletter signup form with validation and success message.
Preview
Code
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { Send } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import confetti from "canvas-confetti";
export interface NewsletterSignupProps {
onSubmit: (email: string) => Promise<void>;
className?: string;
}
export const NewsletterSignup: React.FC<NewsletterSignupProps> = ({
onSubmit,
className = "",
}) => {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (!email) {
setError("Email is required");
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
setError("Please enter a valid email address");
return;
}
setIsSubmitting(true);
try {
await onSubmit(email);
setIsSubmitted(true);
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
});
} catch (err) {
setError("An error occurred. Please try again.");
} finally {
setIsSubmitting(false);
}
};
return (
<div
className={`bg-secondary/50 border border-primary/10 rounded-lg p-6 ${className}`}
>
<AnimatePresence mode="wait">
{!isSubmitted ? (
<motion.form
key="form"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
onSubmit={handleSubmit}
className="flex flex-col gap-4"
>
<div className="flex items-start justify-center gap-1 flex-col overflow-y-hidden">
<motion.h2
className="text-2xl font-bold text-foreground"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
Subscribe to our newsletter
</motion.h2>
<motion.p
className="text-muted-foreground text-sm"
initial={{ opacity: 0, y: 10, filter: "blur(3px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{ delay: 0.4 }}
>
Stay up to date with our latest news and updates.
</motion.p>
</div>
<div className="space-y-2">
<motion.label
initial={{ opacity: 0, filter: "blur(3px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
transition={{ delay: 0.6 }}
className="font-medium text-sm"
htmlFor="email"
>
Email address
</motion.label>
<motion.div
className="flex gap-2"
initial={{ opacity: 0, filter: "blur(3px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
transition={{ delay: 0.7 }}
>
<Input
type="email"
id="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 border rounded-md focus-visible:ring-0 focus-within:ring-0 focus:outline-white/10 "
/>
<Button
type="submit"
disabled={isSubmitting}
className="relative overflow-hidden"
>
<motion.div
key="default"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex items-center px-4"
>
<Send className="h-4 w-4" />
<span className="ml-2">Subscribe</span>
</motion.div>
</Button>
</motion.div>
</div>
<AnimatePresence>
{error && (
<motion.p
className="text-red-500 text-sm"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
>
{error}
</motion.p>
)}
</AnimatePresence>
</motion.form>
) : (
<motion.div
key="success"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
className="text-center"
>
<h2 className="text-2xl font-bold text-foreground mb-2">
Thank you for subscribing!
</h2>
<p className="text-muted-foreground">
We've sent a confirmation email to your inbox.
</p>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
Usage
import { NewsletterSignup } from "@/components/library/marketing/NewsletterSignup";
const MyComponent = () => {
const handleSubmit = async (email: string) => {
// Handle form submission
await submitToAPI(email);
};
return <NewsletterSignup onSubmit={handleSubmit} />;
};
Props
Prop | Type | Default |
---|---|---|
onSubmit | (email: string) => Promise<void> | - |
className? | string | - |
Edit on GitHub
Last updated on