website-monitor/frontend/components/landing/WaitlistForm.tsx

265 lines
9.3 KiB
TypeScript

'use client'
import { motion, AnimatePresence } from 'framer-motion'
import { useState } from 'react'
import { Check, ArrowRight, Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { MagneticButton } from './MagneticElements'
interface WaitlistFormProps {
id?: string
}
export function WaitlistForm({ id }: WaitlistFormProps) {
const [email, setEmail] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [error, setError] = useState('')
const [confetti, setConfetti] = useState<Array<{ id: number; x: number; y: number; rotation: number; color: string }>>([])
const validateEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
const triggerConfetti = () => {
const colors = ['hsl(var(--primary))', 'hsl(var(--teal))', 'hsl(var(--burgundy))', '#fbbf24', '#f97316']
const particles = Array.from({ length: 50 }, (_, i) => ({
id: Date.now() + i,
x: 50 + (Math.random() - 0.5) * 40, // Center around 50%
y: 50,
rotation: Math.random() * 360,
color: colors[Math.floor(Math.random() * colors.length)]
}))
setConfetti(particles)
setTimeout(() => setConfetti([]), 3000)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
if (!email) {
setError('Please enter your email')
return
}
if (!validateEmail(email)) {
setError('Please enter a valid email')
return
}
setIsSubmitting(true)
try {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'
const response = await fetch(`${apiUrl}/api/waitlist`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
source: 'landing_page',
referrer: typeof window !== 'undefined' ? document.referrer : undefined,
}),
})
const data = await response.json()
if (data.success) {
setIsSubmitting(false)
setIsSuccess(true)
triggerConfetti()
} else {
setIsSubmitting(false)
setError(data.message || 'Something went wrong. Please try again.')
}
} catch (err) {
console.error('Waitlist signup error:', err)
setIsSubmitting(false)
setError('Connection error. Please try again.')
}
}
if (isSuccess) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="relative max-w-md mx-auto"
>
{/* Confetti */}
{confetti.map(particle => (
<motion.div
key={particle.id}
className="absolute w-2 h-2 rounded-full"
style={{
backgroundColor: particle.color,
left: `${particle.x}%`,
top: `${particle.y}%`
}}
initial={{ opacity: 1, scale: 1 }}
animate={{
y: [-20, window.innerHeight / 4],
x: [(Math.random() - 0.5) * 200],
opacity: [1, 1, 0],
rotate: [particle.rotation, particle.rotation + 720],
scale: [1, 0.5, 0]
}}
transition={{
duration: 2 + Math.random(),
ease: [0.45, 0, 0.55, 1]
}}
/>
))}
{/* Success Card */}
<motion.div
initial={{ y: 20 }}
animate={{ y: 0 }}
className="relative overflow-hidden rounded-3xl border-2 border-[hsl(var(--teal))] bg-white shadow-2xl shadow-[hsl(var(--teal))]/20 p-8 text-center"
>
{/* Animated background accent */}
<motion.div
className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-[hsl(var(--primary))] via-[hsl(var(--teal))] to-[hsl(var(--burgundy))]"
animate={{
backgroundPosition: ['0% 50%', '100% 50%', '0% 50%']
}}
transition={{ duration: 3, repeat: Infinity }}
style={{ backgroundSize: '200% 100%' }}
/>
{/* Success Icon */}
<motion.div
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 20, delay: 0.2 }}
className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-[hsl(var(--teal))]/10 border-2 border-[hsl(var(--teal))]"
>
<Check className="h-10 w-10 text-[hsl(var(--teal))]" strokeWidth={3} />
</motion.div>
{/* Success Message */}
<motion.h3
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-3 text-3xl font-display font-bold text-foreground"
>
{"You're on the list!"}
</motion.h3>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="mb-6 text-muted-foreground"
>
Check your inbox for confirmation
</motion.p>
{/* Bonus Badge */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="mt-6 inline-flex items-center gap-2 rounded-full bg-[hsl(var(--burgundy))]/10 border border-[hsl(var(--burgundy))]/30 px-4 py-2"
>
<span className="text-sm font-bold text-[hsl(var(--burgundy))]">
🎉 Early access
</span>
</motion.div>
</motion.div>
</motion.div>
)
}
return (
<motion.form
id={id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
onSubmit={handleSubmit}
className="max-w-md mx-auto"
>
<div className="flex flex-col gap-3">
<div className="flex flex-col sm:flex-row gap-3">
{/* Email Input */}
<motion.div
className="flex-1 relative"
>
<input
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value)
setError('')
}}
placeholder="Enter your email"
disabled={isSubmitting}
className={`w-full h-14 rounded-full px-6 text-base border-2 transition-all outline-none ${error
? 'border-red-500 bg-red-50 focus:border-red-500 focus:ring-4 focus:ring-red-500/20'
: 'border-border bg-background focus:border-[hsl(var(--primary))] focus:ring-4 focus:ring-[hsl(var(--primary))]/20'
} disabled:opacity-50 disabled:cursor-not-allowed`}
/>
</motion.div>
{/* Submit Button */}
<MagneticButton strength={0.3}>
<Button
type="submit"
disabled={isSubmitting || !email}
size="lg"
className="h-14 rounded-full bg-[hsl(var(--burgundy))] px-8 text-white hover:bg-[hsl(var(--burgundy))]/90 shadow-2xl shadow-[hsl(var(--burgundy))]/30 transition-all disabled:opacity-50 disabled:cursor-not-allowed font-bold text-base group whitespace-nowrap"
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Joining...
</>
) : (
<>
Reserve Your Spot
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</>
)}
</Button>
</MagneticButton>
</div>
{/* Error Message - Visibility Improved */}
<AnimatePresence>
{error && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="text-red-600 bg-red-50 px-4 py-2 rounded-lg text-sm font-medium border border-red-100 flex items-center gap-2"
>
<div className="h-1.5 w-1.5 rounded-full bg-red-500 flex-shrink-0" />
{error}
</motion.div>
)}
</AnimatePresence>
</div>
{/* Trust Signals Below Form */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
className="mt-6 flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground"
>
<div className="flex items-center gap-2">
<Check className="h-4 w-4 text-[hsl(var(--teal))]" />
<span>No credit card needed</span>
</div>
<span className="hidden sm:inline"></span>
<div className="flex items-center gap-2">
<Check className="h-4 w-4 text-[hsl(var(--teal))]" />
<span>No spam, ever</span>
</div>
</motion.div>
</motion.form>
)
}