gitea
This commit is contained in:
256
frontend/components/landing/WaitlistForm.tsx
Normal file
256
frontend/components/landing/WaitlistForm.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
'use client'
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Check, ArrowRight, Loader2, Sparkles } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export function WaitlistForm() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [queuePosition, setQueuePosition] = useState(0)
|
||||
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)
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
// Generate a random queue position
|
||||
const position = Math.floor(Math.random() * 500) + 400
|
||||
|
||||
setQueuePosition(position)
|
||||
setIsSubmitting(false)
|
||||
setIsSuccess(true)
|
||||
triggerConfetti()
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
{/* Queue Position */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.5, type: 'spring' }}
|
||||
className="inline-flex items-center gap-3 rounded-full bg-gradient-to-r from-[hsl(var(--primary))]/10 to-[hsl(var(--teal))]/10 border border-[hsl(var(--teal))]/30 px-6 py-3"
|
||||
>
|
||||
<Sparkles className="h-5 w-5 text-[hsl(var(--primary))]" />
|
||||
<div className="text-left">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
Your position
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
#{queuePosition}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 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: 50% off for 6 months
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.form
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
onSubmit={handleSubmit}
|
||||
className="max-w-md mx-auto"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
{/* Email Input */}
|
||||
<motion.div
|
||||
className="flex-1 relative"
|
||||
animate={error ? { x: [-10, 10, -10, 10, 0] } : {}}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<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`}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="absolute -bottom-6 left-4 text-xs font-medium text-red-500"
|
||||
>
|
||||
{error}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<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 hover:scale-105 disabled:hover:scale-100 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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user