gitea
This commit is contained in:
138
frontend/components/landing/CompetitorDemoVisual.tsx
Normal file
138
frontend/components/landing/CompetitorDemoVisual.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Bell, ArrowDown } from 'lucide-react'
|
||||
|
||||
export function CompetitorDemoVisual() {
|
||||
const [phase, setPhase] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setPhase(p => (p + 1) % 2)
|
||||
}, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative h-full min-h-[200px] bg-gradient-to-br from-background via-background to-[hsl(var(--primary))]/5 rounded-xl p-4 overflow-hidden">
|
||||
{/* Browser Header */}
|
||||
<div className="mb-3 flex items-center gap-2 px-2 py-1.5 rounded-md bg-secondary/50 border border-border">
|
||||
<div className="flex gap-1">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-red-400" />
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-yellow-400" />
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-400" />
|
||||
</div>
|
||||
<div className="text-[9px] text-muted-foreground font-mono">
|
||||
competitor.com/pricing
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Table */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-bold text-foreground">Professional Plan</h4>
|
||||
|
||||
{/* Price Card */}
|
||||
<motion.div
|
||||
className="p-4 rounded-xl border-2 bg-white relative overflow-hidden"
|
||||
animate={{
|
||||
borderColor: phase === 1 ? 'hsl(var(--teal))' : 'hsl(var(--border))',
|
||||
boxShadow: phase === 1
|
||||
? '0 0 20px hsl(var(--teal) / 0.3)'
|
||||
: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{/* Shine effect on change */}
|
||||
{phase === 1 && (
|
||||
<motion.div
|
||||
initial={{ x: '-100%' }}
|
||||
animate={{ x: '200%' }}
|
||||
transition={{ duration: 0.8, ease: 'easeInOut' }}
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/50 to-transparent"
|
||||
style={{ transform: 'skewX(-20deg)' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="relative z-10 space-y-2">
|
||||
{/* Old Price */}
|
||||
<motion.div
|
||||
animate={{
|
||||
opacity: phase === 1 ? 0.4 : 1,
|
||||
scale: phase === 1 ? 0.95 : 1
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<motion.span
|
||||
className="text-3xl font-bold"
|
||||
animate={{
|
||||
textDecoration: phase === 1 ? 'line-through' : 'none',
|
||||
color: phase === 1 ? 'hsl(var(--muted-foreground))' : 'hsl(var(--foreground))'
|
||||
}}
|
||||
>
|
||||
$99
|
||||
</motion.span>
|
||||
<span className="text-sm text-muted-foreground">/month</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* New Price with animated arrow */}
|
||||
{phase === 1 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10, scale: 0.9 }}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
transition={{ delay: 0.2, type: 'spring', stiffness: 300, damping: 20 }}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowDown className="h-4 w-4 text-[hsl(var(--teal))]" />
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-4xl font-bold text-[hsl(var(--teal))]">
|
||||
$79
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">/month</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Savings Badge */}
|
||||
{phase === 1 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8, rotate: -5 }}
|
||||
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||
transition={{ delay: 0.4, type: 'spring' }}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-[hsl(var(--teal))]/10 border border-[hsl(var(--teal))]/30"
|
||||
>
|
||||
<span className="text-[9px] font-bold text-[hsl(var(--teal))] uppercase tracking-wider">
|
||||
Save $240/year
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Alert Notification */}
|
||||
{phase === 1 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="flex items-center gap-2 p-2 rounded-lg bg-[hsl(var(--burgundy))]/10 border border-[hsl(var(--burgundy))]/30"
|
||||
>
|
||||
<div className="relative flex-shrink-0">
|
||||
<Bell className="h-3 w-3 text-[hsl(var(--burgundy))]" />
|
||||
<motion.span
|
||||
animate={{ scale: [1, 1.3, 1] }}
|
||||
transition={{ duration: 1, repeat: Infinity }}
|
||||
className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-[hsl(var(--burgundy))]"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[9px] font-semibold text-[hsl(var(--burgundy))]">
|
||||
Alert sent to your team
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
915
frontend/components/landing/LandingSections.tsx
Normal file
915
frontend/components/landing/LandingSections.tsx
Normal file
@@ -0,0 +1,915 @@
|
||||
'use client'
|
||||
|
||||
import { motion, type Variants } from 'framer-motion'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Check, ArrowRight, Shield, Search, FileCheck, TrendingUp,
|
||||
Target, Filter, Bell, Eye, Slack, Webhook, History,
|
||||
Zap, Lock, ChevronRight, Star
|
||||
} from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { SEODemoVisual } from './SEODemoVisual'
|
||||
import { CompetitorDemoVisual } from './CompetitorDemoVisual'
|
||||
import { PolicyDemoVisual } from './PolicyDemoVisual'
|
||||
import { WaitlistForm } from './WaitlistForm'
|
||||
import { MagneticButton, SectionDivider } from './MagneticElements'
|
||||
|
||||
// Animation Variants
|
||||
const fadeInUp: Variants = {
|
||||
hidden: { opacity: 0, y: 30, filter: 'blur(4px)' },
|
||||
visible: (i: number = 0) => ({
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
filter: 'blur(0px)',
|
||||
transition: {
|
||||
delay: i * 0.15,
|
||||
duration: 0.7,
|
||||
ease: [0.22, 1, 0.36, 1]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const scaleIn: Variants = {
|
||||
hidden: { opacity: 0, scale: 0.95 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 1. HERO SECTION - "Track competitor changes without the noise"
|
||||
// ============================================
|
||||
export function HeroSection({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
return (
|
||||
<section className="relative overflow-hidden pt-32 pb-24 lg:pt-40 lg:pb-32 bg-[hsl(var(--section-bg-1))]">
|
||||
{/* Background Elements */}
|
||||
<div className="absolute inset-0 grain-texture" />
|
||||
<div className="absolute right-0 top-20 -z-10 h-[600px] w-[600px] rounded-full bg-[hsl(var(--primary))] opacity-8 blur-[120px]" />
|
||||
<div className="absolute left-0 bottom-0 -z-10 h-[400px] w-[400px] rounded-full bg-[hsl(var(--teal))] opacity-8 blur-[100px]" />
|
||||
|
||||
<div className="mx-auto max-w-7xl px-6 relative z-10">
|
||||
<div className="grid lg:grid-cols-[60%_40%] gap-16 items-center">
|
||||
{/* Left: Content */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="flex flex-col gap-8"
|
||||
>
|
||||
{/* Overline */}
|
||||
<motion.div variants={fadeInUp} custom={0}>
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-[hsl(var(--teal))]/10 border border-[hsl(var(--teal))]/20 px-4 py-1.5 text-sm font-medium text-[hsl(var(--teal))]">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[hsl(var(--teal))] opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-[hsl(var(--teal))]"></span>
|
||||
</span>
|
||||
For SEO & Growth Teams
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Hero Headline */}
|
||||
<motion.h1
|
||||
variants={fadeInUp}
|
||||
custom={1}
|
||||
className="text-5xl lg:text-7xl font-display font-bold leading-[1.08] tracking-tight text-foreground"
|
||||
>
|
||||
Track competitor changes{' '}
|
||||
<span className="text-[hsl(var(--primary))]">without the noise.</span>
|
||||
</motion.h1>
|
||||
|
||||
{/* Subheadline */}
|
||||
<motion.p
|
||||
variants={fadeInUp}
|
||||
custom={2}
|
||||
className="text-xl lg:text-2xl text-muted-foreground font-body leading-relaxed max-w-2xl"
|
||||
>
|
||||
Less noise. More signal. Proof included.
|
||||
</motion.p>
|
||||
|
||||
{/* Feature Bullets */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
custom={3}
|
||||
className="grid md:grid-cols-2 gap-4 max-w-2xl"
|
||||
>
|
||||
{[
|
||||
'Auto-filter cookie banners & timestamps',
|
||||
'Keyword alerts when it matters',
|
||||
'Slack/Webhook integration',
|
||||
'Audit-proof history & snapshots'
|
||||
].map((feature, i) => (
|
||||
<div key={i} className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex-shrink-0 flex h-5 w-5 items-center justify-center rounded-full bg-[hsl(var(--teal))]/20">
|
||||
<Check className="h-3 w-3 text-[hsl(var(--teal))]" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="text-foreground text-sm font-medium leading-tight">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* CTAs */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
custom={4}
|
||||
className="flex flex-wrap gap-4"
|
||||
>
|
||||
<MagneticButton strength={0.2}>
|
||||
<Link href={isAuthenticated ? "/dashboard" : "/register"}>
|
||||
<Button
|
||||
size="lg"
|
||||
className="h-14 rounded-full bg-[hsl(var(--primary))] px-8 text-white hover:bg-[hsl(var(--primary))]/90 shadow-2xl shadow-[hsl(var(--primary))]/25 transition-all hover:scale-105 hover:-translate-y-0.5 font-semibold text-base group"
|
||||
>
|
||||
{isAuthenticated ? 'Go to Dashboard' : 'Get Started Free'}
|
||||
<ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
</MagneticButton>
|
||||
</motion.div>
|
||||
|
||||
{/* Trust Signals */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
custom={5}
|
||||
className="flex flex-wrap items-center gap-6 text-sm text-muted-foreground"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Lock className="h-4 w-4" />
|
||||
<span>No credit card</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
<span>No spam, ever</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="h-4 w-4 fill-current" />
|
||||
<span>Early access bonus</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right: Animated Visual - Noise → Signal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 40 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.9, delay: 0.3 }}
|
||||
className="relative"
|
||||
>
|
||||
<NoiseToSignalVisual />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// Noise → Signal Animation Component - Enhanced
|
||||
function NoiseToSignalVisual() {
|
||||
const [phase, setPhase] = useState(0)
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
const [particles, setParticles] = useState<{ id: number; x: number; y: number }[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaused) return
|
||||
const interval = setInterval(() => {
|
||||
setPhase(p => {
|
||||
const nextPhase = (p + 1) % 4
|
||||
// Trigger particles when transitioning from phase 0 to 1
|
||||
if (p === 0 && nextPhase === 1) {
|
||||
triggerParticles()
|
||||
}
|
||||
return nextPhase
|
||||
})
|
||||
}, 2500)
|
||||
return () => clearInterval(interval)
|
||||
}, [isPaused])
|
||||
|
||||
const triggerParticles = () => {
|
||||
const newParticles = Array.from({ length: 8 }, (_, i) => ({
|
||||
id: Date.now() + i,
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100
|
||||
}))
|
||||
setParticles(newParticles)
|
||||
setTimeout(() => setParticles([]), 1000)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="relative aspect-[4/3] rounded-3xl border-2 border-border bg-card/50 backdrop-blur-sm shadow-2xl overflow-hidden cursor-pointer group"
|
||||
style={{ perspective: '1000px' }}
|
||||
whileHover={{ rotateY: 2, rotateX: -2, scale: 1.02 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
onHoverStart={() => setIsPaused(true)}
|
||||
onHoverEnd={() => setIsPaused(false)}
|
||||
>
|
||||
{/* Pulsing Glow Border */}
|
||||
{phase >= 1 && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-3xl"
|
||||
animate={{
|
||||
boxShadow: [
|
||||
'0 0 0px hsl(var(--teal))',
|
||||
'0 0 20px hsl(var(--teal) / 0.5)',
|
||||
'0 0 0px hsl(var(--teal))'
|
||||
]
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Particles */}
|
||||
{particles.map(particle => (
|
||||
<motion.div
|
||||
key={particle.id}
|
||||
className="absolute w-1 h-1 rounded-full bg-[hsl(var(--teal))]"
|
||||
initial={{ x: `${particle.x}%`, y: `${particle.y}%`, opacity: 1, scale: 1 }}
|
||||
animate={{
|
||||
y: `${particle.y - 20}%`,
|
||||
opacity: 0,
|
||||
scale: 0
|
||||
}}
|
||||
transition={{ duration: 0.8 }}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Mock Browser Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border bg-secondary/30 px-4 py-3">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-red-400" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-yellow-400" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-green-400" />
|
||||
</div>
|
||||
<div className="flex-1 mx-4 px-3 py-1 rounded-md bg-background/50 text-xs text-muted-foreground font-mono text-center">
|
||||
competitor-site.com/pricing
|
||||
</div>
|
||||
{isPaused && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-[10px] text-muted-foreground font-medium"
|
||||
>
|
||||
PAUSED
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="p-8 space-y-4 relative">
|
||||
{/* Noise Counter */}
|
||||
<motion.div
|
||||
className="absolute top-4 left-4 px-3 py-1 rounded-full bg-background/80 backdrop-blur-sm border border-border text-xs font-mono font-semibold"
|
||||
animate={{
|
||||
opacity: phase === 0 ? 1 : 0.5,
|
||||
scale: phase === 0 ? 1 : 0.95
|
||||
}}
|
||||
>
|
||||
Noise: {phase === 0 ? '67%' : '0%'}
|
||||
</motion.div>
|
||||
|
||||
{/* Phase 0: Noisy Page */}
|
||||
<motion.div
|
||||
animate={{
|
||||
opacity: phase === 0 ? 1 : 0,
|
||||
scale: phase === 0 ? 1 : 0.98,
|
||||
filter: phase === 0 ? 'blur(0px)' : 'blur(8px)'
|
||||
}}
|
||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="space-y-3"
|
||||
>
|
||||
{/* Cookie Banner - with strikethrough */}
|
||||
<motion.div
|
||||
className="flex items-center justify-between p-3 rounded-lg bg-muted/30 border border-border/40 relative overflow-hidden"
|
||||
animate={{
|
||||
x: phase >= 1 ? -10 : 0,
|
||||
opacity: phase >= 1 ? 0.3 : 1
|
||||
}}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">🍪 Cookie Banner</span>
|
||||
<span className="text-xs text-red-500 font-semibold">
|
||||
NOISE
|
||||
</span>
|
||||
{/* Strikethrough animation */}
|
||||
{phase >= 1 && (
|
||||
<motion.div
|
||||
className="absolute inset-0 border-t-2 border-red-500 top-1/2"
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Enterprise Plan Card */}
|
||||
<div className="p-4 rounded-lg bg-background border border-border">
|
||||
<p className="text-sm font-semibold text-foreground mb-2">Enterprise Plan</p>
|
||||
<p className="text-2xl font-bold text-[hsl(var(--burgundy))]">$99/mo</p>
|
||||
</div>
|
||||
|
||||
{/* Timestamp - with strikethrough */}
|
||||
<motion.div
|
||||
className="flex items-center justify-between p-3 rounded-lg bg-muted/30 border border-border/40 relative overflow-hidden"
|
||||
animate={{
|
||||
x: phase >= 1 ? -10 : 0,
|
||||
opacity: phase >= 1 ? 0.3 : 1
|
||||
}}
|
||||
transition={{ duration: 0.4, delay: 0.1 }}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">⏰ Last updated: 10:23 AM</span>
|
||||
<span className="text-xs text-red-500 font-semibold">
|
||||
NOISE
|
||||
</span>
|
||||
{/* Strikethrough animation */}
|
||||
{phase >= 1 && (
|
||||
<motion.div
|
||||
className="absolute inset-0 border-t-2 border-red-500 top-1/2"
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Phase 1-3: Filtered + Highlighted Signal */}
|
||||
{phase >= 1 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.85, rotateX: -15 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
rotateX: 0
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
ease: [0.22, 1, 0.36, 1],
|
||||
scale: { type: 'spring', stiffness: 300, damping: 20 }
|
||||
}}
|
||||
className="absolute inset-0 flex items-center justify-center p-8"
|
||||
>
|
||||
<motion.div
|
||||
className="w-full p-6 rounded-2xl bg-white border-2 border-[hsl(var(--teal))] shadow-2xl relative overflow-hidden"
|
||||
animate={{
|
||||
boxShadow: [
|
||||
'0 20px 60px hsl(var(--teal) / 0.2)',
|
||||
'0 20px 80px hsl(var(--teal) / 0.3)',
|
||||
'0 20px 60px hsl(var(--teal) / 0.2)'
|
||||
]
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
{/* Animated corner accent */}
|
||||
<motion.div
|
||||
className="absolute top-0 right-0 w-20 h-20 bg-[hsl(var(--teal))]/10 rounded-bl-full"
|
||||
animate={{ scale: [1, 1.1, 1] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
/>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<motion.span
|
||||
className="text-xs font-bold uppercase tracking-wider text-[hsl(var(--teal))]"
|
||||
animate={{ opacity: [1, 0.7, 1] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
>
|
||||
✓ SIGNAL DETECTED
|
||||
</motion.span>
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--teal))]">
|
||||
<Filter className="h-3 w-3" />
|
||||
Filtered
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-muted-foreground mb-3">Enterprise Plan</p>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<p className="text-3xl font-bold text-foreground">$99/mo</p>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, x: -10, scale: 0.9 }}
|
||||
animate={{
|
||||
opacity: phase >= 2 ? 1 : 0,
|
||||
x: phase >= 2 ? 0 : -10,
|
||||
scale: phase >= 2 ? 1 : 0.9
|
||||
}}
|
||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="text-lg text-[hsl(var(--burgundy))] font-bold flex items-center gap-1"
|
||||
>
|
||||
<span>→</span>
|
||||
<motion.span
|
||||
animate={{ scale: phase === 2 ? [1, 1.1, 1] : 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
$79/mo
|
||||
</motion.span>
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Alert badge */}
|
||||
{phase >= 3 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-4 inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[hsl(var(--burgundy))]/10 border border-[hsl(var(--burgundy))]/30"
|
||||
>
|
||||
<Bell className="h-3 w-3 text-[hsl(var(--burgundy))]" />
|
||||
<span className="text-[10px] font-bold text-[hsl(var(--burgundy))] uppercase tracking-wider">
|
||||
Alert Sent
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Phase Indicator */}
|
||||
<div className="absolute bottom-4 right-4 flex gap-1.5">
|
||||
{[0, 1, 2, 3].map(i => (
|
||||
<motion.div
|
||||
key={i}
|
||||
animate={{
|
||||
width: phase === i ? 24 : 6,
|
||||
backgroundColor: phase === i ? 'hsl(var(--teal))' : 'hsl(var(--border))'
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="h-1.5 rounded-full"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2. USE CASE SHOWCASE - SEO, Competitor, Policy
|
||||
// ============================================
|
||||
export function UseCaseShowcase() {
|
||||
const useCases = [
|
||||
{
|
||||
icon: <Search className="h-7 w-7" />,
|
||||
title: 'SEO Monitoring',
|
||||
problem: 'Your rankings drop before you know why.',
|
||||
example: 'Track when competitors update meta descriptions or add new content sections that outrank you.',
|
||||
color: 'teal',
|
||||
gradient: 'from-[hsl(var(--teal))]/10 to-transparent',
|
||||
demoComponent: <SEODemoVisual />
|
||||
},
|
||||
{
|
||||
icon: <TrendingUp className="h-7 w-7" />,
|
||||
title: 'Competitor Intelligence',
|
||||
problem: 'Competitor launches slip past your radar.',
|
||||
example: 'Monitor pricing pages, product launches, and promotional campaigns in real-time.',
|
||||
color: 'primary',
|
||||
gradient: 'from-[hsl(var(--primary))]/10 to-transparent',
|
||||
demoComponent: <CompetitorDemoVisual />
|
||||
},
|
||||
{
|
||||
icon: <FileCheck className="h-7 w-7" />,
|
||||
title: 'Policy & Compliance',
|
||||
problem: 'Regulatory updates appear without warning.',
|
||||
example: 'Track policy changes, terms updates, and legal text modifications with audit-proof history.',
|
||||
color: 'burgundy',
|
||||
gradient: 'from-[hsl(var(--burgundy))]/10 to-transparent',
|
||||
demoComponent: <PolicyDemoVisual />
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="py-32 bg-[hsl(var(--section-bg-3))] relative overflow-hidden">
|
||||
{/* Background Decor - Enhanced Grid */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,hsl(var(--border))_1px,transparent_1px),linear-gradient(to_bottom,hsl(var(--border))_1px,transparent_1px)] bg-[size:4rem_4rem] opacity-30 [mask-image:radial-gradient(ellipse_80%_50%_at_50%_50%,#000_70%,transparent_100%)]" />
|
||||
|
||||
<div className="mx-auto max-w-7xl px-6 relative z-10">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<motion.div variants={fadeInUp} className="inline-flex items-center gap-2 rounded-full bg-secondary border border-border px-4 py-1.5 text-sm font-medium text-foreground mb-6">
|
||||
<Eye className="h-4 w-4" />
|
||||
Who This Is For
|
||||
</motion.div>
|
||||
<motion.h2 variants={fadeInUp} custom={1} className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">
|
||||
Built for teams who need results,{' '}
|
||||
<span className="text-muted-foreground">not demos.</span>
|
||||
</motion.h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Use Case Cards - Diagonal Cascade */}
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
{useCases.map((useCase, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 40, rotateX: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0, rotateX: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: i * 0.15, duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
|
||||
whileHover={{ y: -12, scale: 1.02, transition: { duration: 0.3 } }}
|
||||
className="group relative glass-card rounded-3xl shadow-xl hover:shadow-2xl transition-all overflow-hidden"
|
||||
>
|
||||
{/* Gradient Background */}
|
||||
<div className={`absolute inset-0 rounded-3xl bg-gradient-to-br ${useCase.gradient} opacity-0 group-hover:opacity-100 transition-opacity`} />
|
||||
|
||||
<div className="relative z-10 p-8 space-y-6">
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
whileHover={{ rotate: 5, scale: 1.1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={`inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-[hsl(var(--${useCase.color}))]/10 text-[hsl(var(--${useCase.color}))] border border-[hsl(var(--${useCase.color}))]/20`}
|
||||
>
|
||||
{useCase.icon}
|
||||
</motion.div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-2xl font-display font-bold text-foreground">
|
||||
{useCase.title}
|
||||
</h3>
|
||||
|
||||
{/* Problem Statement */}
|
||||
<p className="text-sm font-semibold text-muted-foreground">
|
||||
{useCase.problem}
|
||||
</p>
|
||||
|
||||
{/* Animated Demo Visual */}
|
||||
<div className="!mt-6 rounded-xl overflow-hidden border border-border/50 shadow-inner">
|
||||
{useCase.demoComponent}
|
||||
</div>
|
||||
|
||||
{/* Example Scenario */}
|
||||
<div className="pt-4 border-t border-border">
|
||||
<p className="text-xs uppercase tracking-wider font-bold text-muted-foreground mb-2">
|
||||
Example:
|
||||
</p>
|
||||
<p className="text-sm text-foreground leading-relaxed">
|
||||
{useCase.example}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 3. HOW IT WORKS - 4 Stage Flow
|
||||
// ============================================
|
||||
export function HowItWorks() {
|
||||
const stages = [
|
||||
{ icon: <Target className="h-6 w-6" />, title: 'Set URL', desc: 'Add the page you want to monitor' },
|
||||
{ icon: <Zap className="h-6 w-6" />, title: 'Check regularly', desc: 'Automated checks at your chosen frequency' },
|
||||
{ icon: <Filter className="h-6 w-6" />, title: 'Remove noise', desc: 'AI filters out irrelevant changes' },
|
||||
{ icon: <Bell className="h-6 w-6" />, title: 'Get alerted', desc: 'Receive notifications that matter' }
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="py-32 bg-gradient-to-b from-[hsl(var(--section-bg-4))] to-[hsl(var(--section-bg-5))] relative overflow-hidden">
|
||||
{/* Subtle Diagonal Stripe Decoration */}
|
||||
<div className="absolute inset-0 opacity-5" style={{ backgroundImage: 'repeating-linear-gradient(45deg, hsl(var(--primary)), hsl(var(--primary)) 2px, transparent 2px, transparent 40px)' }} />
|
||||
<div className="mx-auto max-w-7xl px-6 relative z-10">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<motion.h2 variants={fadeInUp} className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">
|
||||
How it works
|
||||
</motion.h2>
|
||||
<motion.p variants={fadeInUp} custom={1} className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Four simple steps to never miss an important change again.
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* Horizontal Flow */}
|
||||
<div className="relative">
|
||||
{/* Connecting Line */}
|
||||
<div className="absolute top-1/2 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-border to-transparent -translate-y-1/2 hidden lg:block" />
|
||||
|
||||
<div className="grid lg:grid-cols-4 gap-8 lg:gap-4">
|
||||
{stages.map((stage, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: i * 0.1, duration: 0.5 }}
|
||||
className="relative flex flex-col items-center text-center group"
|
||||
>
|
||||
{/* Large Number Background */}
|
||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 text-8xl font-display font-bold text-border/20 pointer-events-none">
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</div>
|
||||
|
||||
{/* Circle Container */}
|
||||
<div className="relative z-10 mb-6 flex h-20 w-20 items-center justify-center rounded-full border-2 border-border bg-card shadow-lg group-hover:shadow-2xl group-hover:border-[hsl(var(--primary))] group-hover:bg-[hsl(var(--primary))]/5 transition-all">
|
||||
<div className="text-[hsl(var(--primary))]">
|
||||
{stage.icon}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<h3 className="text-lg font-bold text-foreground mb-2">
|
||||
{stage.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-[200px]">
|
||||
{stage.desc}
|
||||
</p>
|
||||
|
||||
{/* Arrow (not on last) */}
|
||||
{i < stages.length - 1 && (
|
||||
<div className="hidden lg:block absolute top-10 -right-4 text-border">
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 4. DIFFERENTIATORS - Why We're Better
|
||||
// ============================================
|
||||
export function Differentiators() {
|
||||
const features = [
|
||||
{ feature: 'Noise Filtering', others: 'Basic', us: 'AI-powered + custom rules', icon: <Filter className="h-5 w-5" /> },
|
||||
{ feature: 'Keyword Alerts', others: 'Limited', us: 'Regex + thresholds', icon: <Search className="h-5 w-5" /> },
|
||||
{ feature: 'Integrations', others: 'Email only', us: 'Slack, Webhooks, Teams', icon: <Slack className="h-5 w-5" /> },
|
||||
{ feature: 'History & Proof', others: '7-30 days', us: 'Unlimited snapshots', icon: <History className="h-5 w-5" /> },
|
||||
{ feature: 'Setup Time', others: '15+ min', us: '2 minutes', icon: <Zap className="h-5 w-5" /> },
|
||||
{ feature: 'Pricing', others: '$29-99/mo', us: 'Fair pay-per-use', icon: <Shield className="h-5 w-5" /> }
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="py-32 bg-[hsl(var(--section-bg-5))] relative overflow-hidden">
|
||||
{/* Radial Gradient Overlay */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_50%,hsl(var(--teal))_0%,transparent_50%)] opacity-5" />
|
||||
<div className="mx-auto max-w-6xl px-6 relative z-10">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<motion.h2 variants={fadeInUp} className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">
|
||||
Why we're{' '}
|
||||
<span className="text-[hsl(var(--teal))]">different</span>
|
||||
</motion.h2>
|
||||
<motion.p variants={fadeInUp} custom={1} className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Not all monitoring tools are created equal. Here's what sets us apart.
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* Feature Cards Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{features.map((item, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: i * 0.05, duration: 0.4 }}
|
||||
className="group relative glass-card rounded-2xl p-6 hover:border-[hsl(var(--teal))]/30 hover:shadow-xl transition-all hover:-translate-y-1"
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-[hsl(var(--teal))]/10 text-[hsl(var(--teal))] mb-4 group-hover:scale-110 transition-transform">
|
||||
{item.icon}
|
||||
</div>
|
||||
|
||||
{/* Feature Name */}
|
||||
<h3 className="text-lg font-bold text-foreground mb-4">
|
||||
{item.feature}
|
||||
</h3>
|
||||
|
||||
{/* Comparison */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-xs uppercase tracking-wider font-bold text-muted-foreground flex-shrink-0 mt-0.5">Others:</span>
|
||||
<span className="text-sm text-muted-foreground">{item.others}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 p-3 rounded-lg bg-[hsl(var(--teal))]/5 border border-[hsl(var(--teal))]/20">
|
||||
<Check className="h-4 w-4 text-[hsl(var(--teal))] flex-shrink-0 mt-0.5" strokeWidth={3} />
|
||||
<span className="text-sm font-semibold text-foreground">{item.us}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 5. SOCIAL PROOF - Testimonials (Prepared for Beta)
|
||||
// ============================================
|
||||
export function SocialProof() {
|
||||
const testimonials = [
|
||||
{
|
||||
quote: "The noise filtering alone saves me 2 hours per week. Finally, monitoring that actually works.",
|
||||
author: "[Beta User]",
|
||||
role: "SEO Manager",
|
||||
company: "[Company]",
|
||||
useCase: "SEO Monitoring"
|
||||
},
|
||||
{
|
||||
quote: "We catch competitor price changes within minutes. Game-changer for our pricing strategy.",
|
||||
author: "[Beta User]",
|
||||
role: "Growth Lead",
|
||||
company: "[Company]",
|
||||
useCase: "Competitor Intelligence"
|
||||
},
|
||||
{
|
||||
quote: "Audit-proof history saved us during compliance review. Worth every penny.",
|
||||
author: "[Beta User]",
|
||||
role: "Compliance Officer",
|
||||
company: "[Company]",
|
||||
useCase: "Policy Tracking"
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="py-32 bg-gradient-to-b from-foreground to-[hsl(var(--foreground))]/95 relative overflow-hidden text-white">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
||||
backgroundSize: '60px 60px'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-7xl px-6 relative z-10">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<motion.h2 variants={fadeInUp} className="text-4xl lg:text-5xl font-display font-bold mb-6">
|
||||
Built for teams who need results,{' '}
|
||||
<span className="text-[hsl(var(--primary))]">not demos.</span>
|
||||
</motion.h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Testimonial Cards - Minimal & Uniform */}
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{testimonials.map((testimonial, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
whileHover={{ y: -4 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: i * 0.1, duration: 0.5 }}
|
||||
className="relative group"
|
||||
>
|
||||
{/* Subtle gradient border glow */}
|
||||
<div className="absolute -inset-0.5 bg-gradient-to-br from-[hsl(var(--primary))] to-[hsl(var(--teal))] rounded-3xl blur opacity-15 group-hover:opacity-25 transition-opacity duration-300" />
|
||||
|
||||
{/* Main Card - fixed height for uniformity */}
|
||||
<div className="relative h-full flex flex-col rounded-3xl bg-white/10 backdrop-blur-sm border border-white/20 p-8 group-hover:bg-white/12 transition-colors duration-300">
|
||||
{/* Large Quote Mark */}
|
||||
<div className="text-5xl font-display text-[hsl(var(--primary))]/30 leading-none mb-3">
|
||||
"
|
||||
</div>
|
||||
|
||||
{/* Quote - flex-grow ensures cards align */}
|
||||
<p className="font-body text-base leading-relaxed mb-6 text-white/90 font-medium italic flex-grow">
|
||||
{testimonial.quote}
|
||||
</p>
|
||||
|
||||
{/* Attribution - always at bottom */}
|
||||
<div className="flex items-start justify-between mt-auto">
|
||||
<div>
|
||||
<p className="font-bold text-white text-sm">{testimonial.author}</p>
|
||||
<p className="text-xs text-white/70">{testimonial.role} at {testimonial.company}</p>
|
||||
</div>
|
||||
<div className="px-3 py-1 rounded-full bg-[hsl(var(--teal))]/20 border border-[hsl(var(--teal))]/30 text-[10px] font-bold uppercase tracking-wider text-[hsl(var(--teal))]">
|
||||
{testimonial.useCase}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Note */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mt-12 text-sm text-white/60"
|
||||
>
|
||||
Join our waitlist to become a beta tester and get featured here.
|
||||
</motion.p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 6. FINAL CTA - Get Started
|
||||
// ============================================
|
||||
export function FinalCTA({ isAuthenticated }: { isAuthenticated: boolean }) {
|
||||
return (
|
||||
<section className="relative overflow-hidden py-32">
|
||||
{/* Animated Gradient Mesh Background - More dramatic */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[hsl(var(--primary))]/30 via-[hsl(var(--burgundy))]/20 to-[hsl(var(--teal))]/30 opacity-70" />
|
||||
<div className="absolute inset-0 grain-texture" />
|
||||
|
||||
{/* Animated Orbs - Enhanced */}
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
opacity: [0.4, 0.6, 0.4],
|
||||
rotate: [0, 180, 360]
|
||||
}}
|
||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute top-1/4 -left-20 h-[500px] w-[500px] rounded-full bg-[hsl(var(--primary))] blur-[140px]"
|
||||
/>
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.4, 0.5, 0.4],
|
||||
rotate: [360, 180, 0]
|
||||
}}
|
||||
transition={{ duration: 15, repeat: Infinity, ease: "linear", delay: 2 }}
|
||||
className="absolute bottom-1/4 -right-20 h-[500px] w-[500px] rounded-full bg-[hsl(var(--teal))] blur-[140px]"
|
||||
/>
|
||||
|
||||
<div className="mx-auto max-w-4xl px-6 text-center relative z-10">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
className="space-y-8"
|
||||
>
|
||||
{/* Headline */}
|
||||
<motion.h2 variants={fadeInUp} className="text-5xl lg:text-6xl font-display font-bold leading-tight text-foreground">
|
||||
Stop missing the changes{' '}
|
||||
<span className="text-[hsl(var(--primary))]">that matter.</span>
|
||||
</motion.h2>
|
||||
|
||||
{/* Subheadline */}
|
||||
<motion.p variants={fadeInUp} custom={1} className="text-xl lg:text-2xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Join the waitlist and be first to experience monitoring that actually works.
|
||||
</motion.p>
|
||||
|
||||
{/* Waitlist Form - replaces button */}
|
||||
<motion.div variants={fadeInUp} custom={2} className="pt-4">
|
||||
{isAuthenticated ? (
|
||||
<MagneticButton strength={0.15}>
|
||||
<Link href="/dashboard">
|
||||
<Button
|
||||
size="lg"
|
||||
className="h-16 rounded-full bg-[hsl(var(--burgundy))] px-12 text-white hover:bg-[hsl(var(--burgundy))]/90 shadow-2xl shadow-[hsl(var(--burgundy))]/30 transition-all hover:scale-105 font-bold text-lg group"
|
||||
>
|
||||
Go to Dashboard
|
||||
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
</MagneticButton>
|
||||
) : (
|
||||
<WaitlistForm />
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Social Proof Indicator */}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
custom={3}
|
||||
className="flex flex-wrap items-center justify-center gap-6 text-sm text-muted-foreground"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
className="w-2 h-2 rounded-full bg-green-500"
|
||||
/>
|
||||
<span className="font-semibold text-foreground">500+ joined this week</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="h-4 w-4 fill-current text-[hsl(var(--primary))]" />
|
||||
<span>Early access: <span className="font-semibold text-foreground">50% off for 6 months</span></span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
179
frontend/components/landing/LiveStatsBar.tsx
Normal file
179
frontend/components/landing/LiveStatsBar.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Activity, TrendingUp, Zap, Shield } from 'lucide-react'
|
||||
|
||||
function AnimatedNumber({ value, suffix = '' }: { value: number; suffix?: string }) {
|
||||
const [displayValue, setDisplayValue] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const duration = 2000 // 2 seconds
|
||||
const steps = 60
|
||||
const increment = value / steps
|
||||
const stepDuration = duration / steps
|
||||
|
||||
let currentStep = 0
|
||||
const interval = setInterval(() => {
|
||||
currentStep++
|
||||
if (currentStep <= steps) {
|
||||
setDisplayValue(Math.floor(increment * currentStep))
|
||||
} else {
|
||||
setDisplayValue(value)
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, stepDuration)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<span className="font-mono text-2xl lg:text-3xl font-bold text-[hsl(var(--teal))] tabular-nums">
|
||||
{displayValue.toLocaleString()}{suffix}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function FluctuatingNumber({ base, variance }: { base: number; variance: number }) {
|
||||
const [value, setValue] = useState(base)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const fluctuation = (Math.random() - 0.5) * variance
|
||||
setValue(base + fluctuation)
|
||||
}, 1500)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [base, variance])
|
||||
|
||||
return (
|
||||
<span className="font-mono text-2xl lg:text-3xl font-bold text-[hsl(var(--teal))] tabular-nums">
|
||||
{Math.round(value)}ms
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function LiveStatsBar() {
|
||||
const stats = [
|
||||
{
|
||||
icon: <Activity className="h-5 w-5" />,
|
||||
label: 'Checks performed today',
|
||||
value: 2847,
|
||||
type: 'counter' as const
|
||||
},
|
||||
{
|
||||
icon: <TrendingUp className="h-5 w-5" />,
|
||||
label: 'Changes detected this hour',
|
||||
value: 127,
|
||||
type: 'counter' as const
|
||||
},
|
||||
{
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
label: 'Uptime',
|
||||
value: '99.9%',
|
||||
type: 'static' as const
|
||||
},
|
||||
{
|
||||
icon: <Zap className="h-5 w-5" />,
|
||||
label: 'Avg response time',
|
||||
value: '< ',
|
||||
type: 'fluctuating' as const,
|
||||
base: 42,
|
||||
variance: 10
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="border-y border-border bg-gradient-to-r from-foreground/95 via-foreground to-foreground/95 py-8 overflow-hidden">
|
||||
<div className="mx-auto max-w-7xl px-6">
|
||||
{/* Desktop: Grid */}
|
||||
<div className="hidden lg:grid lg:grid-cols-4 gap-8">
|
||||
{stats.map((stat, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: i * 0.1, duration: 0.5 }}
|
||||
className="flex flex-col items-center text-center gap-3"
|
||||
>
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
className="flex items-center justify-center w-12 h-12 rounded-full bg-[hsl(var(--teal))]/10 text-[hsl(var(--teal))]"
|
||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{stat.icon}
|
||||
</motion.div>
|
||||
|
||||
{/* Value */}
|
||||
<div>
|
||||
{stat.type === 'counter' && typeof stat.value === 'number' && (
|
||||
<AnimatedNumber value={stat.value} />
|
||||
)}
|
||||
{stat.type === 'static' && (
|
||||
<span className="font-mono text-2xl lg:text-3xl font-bold text-[hsl(var(--teal))]">
|
||||
{stat.value}
|
||||
</span>
|
||||
)}
|
||||
{stat.type === 'fluctuating' && stat.base && stat.variance && (
|
||||
<span className="font-mono text-2xl lg:text-3xl font-bold text-[hsl(var(--teal))]">
|
||||
{stat.value}<FluctuatingNumber base={stat.base} variance={stat.variance} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<p className="text-xs font-medium text-white/90 uppercase tracking-wider">
|
||||
{stat.label}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile: Horizontal Scroll */}
|
||||
<div className="lg:hidden overflow-x-auto scrollbar-thin pb-2">
|
||||
<div className="flex gap-8 min-w-max px-4">
|
||||
{stats.map((stat, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: i * 0.1, duration: 0.5 }}
|
||||
className="flex flex-col items-center text-center gap-3 min-w-[160px]"
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-[hsl(var(--teal))]/10 text-[hsl(var(--teal))]">
|
||||
{stat.icon}
|
||||
</div>
|
||||
|
||||
{/* Value */}
|
||||
<div>
|
||||
{stat.type === 'counter' && typeof stat.value === 'number' && (
|
||||
<AnimatedNumber value={stat.value} />
|
||||
)}
|
||||
{stat.type === 'static' && (
|
||||
<span className="font-mono text-2xl font-bold text-[hsl(var(--teal))]">
|
||||
{stat.value}
|
||||
</span>
|
||||
)}
|
||||
{stat.type === 'fluctuating' && stat.base && stat.variance && (
|
||||
<span className="font-mono text-2xl font-bold text-[hsl(var(--teal))]">
|
||||
{stat.value}<FluctuatingNumber base={stat.base} variance={stat.variance} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<p className="text-[10px] font-medium text-white/90 uppercase tracking-wider">
|
||||
{stat.label}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
120
frontend/components/landing/MagneticElements.tsx
Normal file
120
frontend/components/landing/MagneticElements.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
'use client'
|
||||
|
||||
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'
|
||||
import { useRef, ReactNode } from 'react'
|
||||
|
||||
interface MagneticButtonProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
strength?: number
|
||||
}
|
||||
|
||||
export function MagneticButton({
|
||||
children,
|
||||
className = '',
|
||||
onClick,
|
||||
strength = 0.3
|
||||
}: MagneticButtonProps) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const x = useMotionValue(0)
|
||||
const y = useMotionValue(0)
|
||||
|
||||
const springConfig = { stiffness: 300, damping: 20 }
|
||||
const springX = useSpring(x, springConfig)
|
||||
const springY = useSpring(y, springConfig)
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!ref.current) return
|
||||
|
||||
const rect = ref.current.getBoundingClientRect()
|
||||
const centerX = rect.left + rect.width / 2
|
||||
const centerY = rect.top + rect.height / 2
|
||||
|
||||
const deltaX = (e.clientX - centerX) * strength
|
||||
const deltaY = (e.clientY - centerY) * strength
|
||||
|
||||
x.set(deltaX)
|
||||
y.set(deltaY)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
x.set(0)
|
||||
y.set(0)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={onClick}
|
||||
style={{ x: springX, y: springY }}
|
||||
className={`inline-block ${className}`}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SectionDividerProps {
|
||||
variant?: 'wave' | 'diagonal' | 'curve'
|
||||
fromColor?: string
|
||||
toColor?: string
|
||||
flip?: boolean
|
||||
}
|
||||
|
||||
export function SectionDivider({
|
||||
variant = 'wave',
|
||||
fromColor = 'section-bg-3',
|
||||
toColor = 'section-bg-4',
|
||||
flip = false
|
||||
}: SectionDividerProps) {
|
||||
if (variant === 'wave') {
|
||||
return (
|
||||
<div className={`w-full h-20 -mt-1 overflow-hidden ${flip ? 'rotate-180' : ''}`}>
|
||||
<svg
|
||||
viewBox="0 0 1200 120"
|
||||
preserveAspectRatio="none"
|
||||
className="w-full h-full"
|
||||
>
|
||||
<path
|
||||
d="M0,0 Q300,80 600,40 T1200,0 L1200,120 L0,120 Z"
|
||||
fill={`hsl(var(--${toColor}))`}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (variant === 'diagonal') {
|
||||
return (
|
||||
<div
|
||||
className={`w-full h-16 ${flip ? '-skew-y-2' : 'skew-y-2'}`}
|
||||
style={{
|
||||
background: `linear-gradient(to bottom right, hsl(var(--${fromColor})), hsl(var(--${toColor})))`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (variant === 'curve') {
|
||||
return (
|
||||
<div className={`w-full h-24 -mt-1 overflow-hidden ${flip ? 'rotate-180' : ''}`}>
|
||||
<svg
|
||||
viewBox="0 0 1200 120"
|
||||
preserveAspectRatio="none"
|
||||
className="w-full h-full"
|
||||
>
|
||||
<path
|
||||
d="M0,60 Q300,120 600,60 T1200,60 L1200,120 L0,120 Z"
|
||||
fill={`hsl(var(--${toColor}))`}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
148
frontend/components/landing/PolicyDemoVisual.tsx
Normal file
148
frontend/components/landing/PolicyDemoVisual.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { FileCheck, Check } from 'lucide-react'
|
||||
|
||||
export function PolicyDemoVisual() {
|
||||
const [phase, setPhase] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setPhase(p => (p + 1) % 2)
|
||||
}, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative h-full min-h-[200px] bg-gradient-to-br from-background via-background to-[hsl(var(--burgundy))]/5 rounded-xl p-4 overflow-hidden">
|
||||
{/* Document Header */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCheck className="h-4 w-4 text-[hsl(var(--burgundy))]" />
|
||||
<span className="text-xs font-bold text-foreground">Terms of Service</span>
|
||||
</div>
|
||||
<motion.div
|
||||
className="px-2 py-0.5 rounded-full border text-[9px] font-bold"
|
||||
animate={{
|
||||
borderColor: phase === 1 ? 'hsl(var(--teal))' : 'hsl(var(--border))',
|
||||
backgroundColor: phase === 1 ? 'hsl(var(--teal) / 0.1)' : 'transparent',
|
||||
color: phase === 1 ? 'hsl(var(--teal))' : 'hsl(var(--muted-foreground))'
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{phase === 0 ? 'v2.1' : 'v2.2'}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Document Content */}
|
||||
<motion.div
|
||||
className="space-y-2 p-3 rounded-lg border-2 bg-white overflow-hidden"
|
||||
animate={{
|
||||
borderColor: phase === 1 ? 'hsl(var(--teal))' : 'hsl(var(--border))',
|
||||
boxShadow: phase === 1
|
||||
? '0 0 20px hsl(var(--teal) / 0.3)'
|
||||
: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{/* Section 4.2 */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[10px] font-bold text-[hsl(var(--primary))]">
|
||||
Section 4.2 - Data Retention
|
||||
</div>
|
||||
|
||||
{/* Text Lines */}
|
||||
<div className="space-y-1 text-[9px] text-muted-foreground leading-relaxed">
|
||||
<p>We will retain your personal data for</p>
|
||||
|
||||
{/* Changing text */}
|
||||
<motion.div
|
||||
className="relative rounded"
|
||||
layout
|
||||
>
|
||||
<motion.p
|
||||
animate={{
|
||||
backgroundColor: phase === 1 ? 'hsl(var(--burgundy) / 0.15)' : 'transparent',
|
||||
paddingLeft: phase === 1 ? '4px' : '0px',
|
||||
paddingRight: phase === 1 ? '4px' : '0px',
|
||||
color: phase === 1 ? 'hsl(var(--burgundy))' : 'hsl(var(--muted-foreground))',
|
||||
fontWeight: phase === 1 ? 600 : 400
|
||||
}}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="relative inline-block rounded"
|
||||
>
|
||||
{phase === 0 ? (
|
||||
'as long as necessary to fulfill purposes'
|
||||
) : (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
a minimum of 90 days after account deletion
|
||||
</motion.span>
|
||||
)}
|
||||
</motion.p>
|
||||
|
||||
{/* Change highlight indicator */}
|
||||
{phase === 1 && (
|
||||
<motion.div
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="absolute -left-1 top-0 bottom-0 w-0.5 bg-[hsl(var(--burgundy))] rounded-full origin-left"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<p>outlined in our Privacy Policy.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diff Stats */}
|
||||
{phase === 1 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="pt-2 border-t border-border flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-3 text-[8px] text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded bg-green-500/20 border border-green-500" />
|
||||
+18 words
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded bg-red-500/20 border border-red-500" />
|
||||
-7 words
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Audit Trail Badge */}
|
||||
{phase === 1 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 5, scale: 0.9 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="mt-3 flex items-center gap-2 p-2 rounded-lg bg-[hsl(var(--teal))]/10 border border-[hsl(var(--teal))]/30"
|
||||
>
|
||||
<div className="flex-shrink-0 flex items-center justify-center w-5 h-5 rounded-full bg-[hsl(var(--teal))] text-white">
|
||||
<Check className="h-3 w-3" strokeWidth={3} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-[9px] font-bold text-[hsl(var(--teal))]">
|
||||
Audit trail saved
|
||||
</div>
|
||||
<div className="text-[8px] text-muted-foreground">
|
||||
Snapshot archived for compliance
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
255
frontend/components/landing/PricingComparison.tsx
Normal file
255
frontend/components/landing/PricingComparison.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState } from 'react'
|
||||
import { TrendingDown, DollarSign } from 'lucide-react'
|
||||
|
||||
export function PricingComparison() {
|
||||
const [monitorCount, setMonitorCount] = useState(50)
|
||||
|
||||
// Pricing calculation logic
|
||||
const calculatePricing = (monitors: number) => {
|
||||
// Competitors: tiered pricing
|
||||
let competitorMin, competitorMax
|
||||
if (monitors <= 10) {
|
||||
competitorMin = 29
|
||||
competitorMax = 49
|
||||
} else if (monitors <= 50) {
|
||||
competitorMin = 79
|
||||
competitorMax = 129
|
||||
} else if (monitors <= 100) {
|
||||
competitorMin = 129
|
||||
competitorMax = 199
|
||||
} else {
|
||||
competitorMin = 199
|
||||
competitorMax = 299
|
||||
}
|
||||
|
||||
// Our pricing: simpler, fairer
|
||||
let ourPrice
|
||||
if (monitors <= 10) {
|
||||
ourPrice = 19
|
||||
} else if (monitors <= 50) {
|
||||
ourPrice = 49
|
||||
} else if (monitors <= 100) {
|
||||
ourPrice = 89
|
||||
} else {
|
||||
ourPrice = 149
|
||||
}
|
||||
|
||||
const competitorAvg = (competitorMin + competitorMax) / 2
|
||||
const savings = competitorAvg - ourPrice
|
||||
const savingsPercent = Math.round((savings / competitorAvg) * 100)
|
||||
|
||||
return {
|
||||
competitorMin,
|
||||
competitorMax,
|
||||
competitorAvg,
|
||||
ourPrice,
|
||||
savings,
|
||||
savingsPercent
|
||||
}
|
||||
}
|
||||
|
||||
const pricing = calculatePricing(monitorCount)
|
||||
|
||||
return (
|
||||
<section className="py-32 bg-gradient-to-b from-[hsl(var(--section-bg-6))] to-[hsl(var(--section-bg-3))] relative overflow-hidden">
|
||||
{/* Background Pattern - Enhanced Dot Grid */}
|
||||
<div className="absolute inset-0 opacity-8">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `radial-gradient(circle, hsl(var(--teal)) 1.5px, transparent 1.5px)`,
|
||||
backgroundSize: '30px 30px'
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-5xl px-6 relative z-10">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-[hsl(var(--teal))]/10 border border-[hsl(var(--teal))]/20 px-4 py-1.5 text-sm font-medium text-[hsl(var(--teal))] mb-6">
|
||||
<DollarSign className="h-4 w-4" />
|
||||
Fair Pricing
|
||||
</div>
|
||||
<h2 className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">
|
||||
See how much you{' '}
|
||||
<span className="text-[hsl(var(--teal))]">save</span>
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Compare our transparent pricing with typical competitors. No hidden fees, no surprises.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Interactive Comparison Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="rounded-3xl border-2 border-border bg-card p-8 lg:p-12 shadow-2xl"
|
||||
>
|
||||
{/* Monitor Count Slider */}
|
||||
<div className="mb-12">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<label className="text-sm font-bold text-muted-foreground uppercase tracking-wider">
|
||||
Number of Monitors
|
||||
</label>
|
||||
<motion.div
|
||||
key={monitorCount}
|
||||
initial={{ scale: 1.2 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="text-4xl font-bold text-foreground font-mono"
|
||||
>
|
||||
{monitorCount}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Slider */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="200"
|
||||
step="5"
|
||||
value={monitorCount}
|
||||
onChange={(e) => setMonitorCount(Number(e.target.value))}
|
||||
className="w-full h-3 bg-secondary rounded-full appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-6 [&::-webkit-slider-thumb]:h-6
|
||||
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[hsl(var(--teal))]
|
||||
[&::-webkit-slider-thumb]:shadow-lg [&::-webkit-slider-thumb]:cursor-grab
|
||||
[&::-webkit-slider-thumb]:active:cursor-grabbing [&::-webkit-slider-thumb]:hover:scale-110
|
||||
[&::-webkit-slider-thumb]:transition-transform
|
||||
[&::-moz-range-thumb]:w-6 [&::-moz-range-thumb]:h-6
|
||||
[&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-[hsl(var(--teal))]
|
||||
[&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:shadow-lg
|
||||
[&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:active:cursor-grabbing"
|
||||
/>
|
||||
{/* Tick marks - positioned by percentage based on slider range (5-200) */}
|
||||
<div className="relative mt-2 h-4">
|
||||
<span className="absolute text-xs text-muted-foreground" style={{ left: '0%', transform: 'translateX(0)' }}>5</span>
|
||||
<span className="absolute text-xs text-muted-foreground" style={{ left: `${((50 - 5) / (200 - 5)) * 100}%`, transform: 'translateX(-50%)' }}>50</span>
|
||||
<span className="absolute text-xs text-muted-foreground" style={{ left: `${((100 - 5) / (200 - 5)) * 100}%`, transform: 'translateX(-50%)' }}>100</span>
|
||||
<span className="absolute text-xs text-muted-foreground" style={{ left: '100%', transform: 'translateX(-100%)' }}>200</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price Comparison Bars */}
|
||||
<div className="grid lg:grid-cols-2 gap-8 mb-8">
|
||||
{/* Competitors */}
|
||||
<motion.div
|
||||
layout
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">
|
||||
Typical Competitors
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bar */}
|
||||
<motion.div
|
||||
className="relative h-24 rounded-2xl bg-gradient-to-r from-red-500/10 to-red-500/20 border-2 border-red-500/30 overflow-hidden"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gradient-to-r from-red-500/20 to-red-500/40"
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
|
||||
style={{ transformOrigin: 'left' }}
|
||||
/>
|
||||
<div className="relative h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<motion.div
|
||||
key={`comp-${pricing.competitorMin}-${pricing.competitorMax}`}
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="text-4xl font-bold text-red-700 font-mono"
|
||||
>
|
||||
${pricing.competitorMin}-{pricing.competitorMax}
|
||||
</motion.div>
|
||||
<div className="text-xs font-medium text-red-600">per month</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Us */}
|
||||
<motion.div
|
||||
layout
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-bold text-[hsl(var(--teal))] uppercase tracking-wider">
|
||||
Our Pricing
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bar */}
|
||||
<motion.div
|
||||
className="relative h-24 rounded-2xl bg-gradient-to-r from-[hsl(var(--teal))]/10 to-[hsl(var(--teal))]/20 border-2 border-[hsl(var(--teal))]/30 overflow-hidden"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gradient-to-r from-[hsl(var(--teal))]/20 to-[hsl(var(--teal))]/40"
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: pricing.ourPrice / pricing.competitorMax }}
|
||||
transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
|
||||
style={{ transformOrigin: 'left' }}
|
||||
/>
|
||||
<div className="relative h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<motion.div
|
||||
key={`our-${pricing.ourPrice}`}
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="text-5xl font-bold text-[hsl(var(--teal))] font-mono"
|
||||
>
|
||||
${pricing.ourPrice}
|
||||
</motion.div>
|
||||
<div className="text-xs font-medium text-[hsl(var(--teal))]">per month</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Savings Badge */}
|
||||
<motion.div
|
||||
key={`savings-${pricing.savings}`}
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
className="flex items-center justify-center gap-4 p-6 rounded-2xl bg-gradient-to-r from-[hsl(var(--primary))]/10 via-[hsl(var(--teal))]/10 to-[hsl(var(--burgundy))]/10 border-2 border-[hsl(var(--teal))]/30"
|
||||
>
|
||||
<TrendingDown className="h-8 w-8 text-[hsl(var(--teal))]" />
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-medium text-muted-foreground">You save</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-4xl font-bold text-foreground">
|
||||
${Math.round(pricing.savings)}
|
||||
</span>
|
||||
<span className="text-xl text-muted-foreground">/month</span>
|
||||
<span className="ml-2 px-3 py-1 rounded-full bg-[hsl(var(--teal))]/20 text-sm font-bold text-[hsl(var(--teal))]">
|
||||
{pricing.savingsPercent}% off
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Fine Print */}
|
||||
<p className="mt-6 text-center text-xs text-muted-foreground">
|
||||
* Based on average pricing from Visualping, Distill.io, and similar competitors as of Jan 2026
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
124
frontend/components/landing/SEODemoVisual.tsx
Normal file
124
frontend/components/landing/SEODemoVisual.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { TrendingDown, TrendingUp } from 'lucide-react'
|
||||
|
||||
export function SEODemoVisual() {
|
||||
const [phase, setPhase] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setPhase(p => (p + 1) % 2)
|
||||
}, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const oldMeta = "Best enterprise software for teams of all sizes. Try free for 30 days."
|
||||
const newMeta = "Best enterprise software for teams of all sizes. Try free for 30 days. Now with AI-powered analytics and real-time collaboration."
|
||||
|
||||
return (
|
||||
<div className="relative h-full min-h-[200px] bg-gradient-to-br from-background via-background to-[hsl(var(--teal))]/5 rounded-xl p-4 overflow-hidden">
|
||||
{/* SERP Result */}
|
||||
<div className="space-y-4">
|
||||
{/* Ranking Indicator */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-mono text-muted-foreground">
|
||||
google.com/search
|
||||
</div>
|
||||
<motion.div
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-full bg-background border border-border"
|
||||
animate={{
|
||||
borderColor: phase === 0 ? 'hsl(var(--border))' : 'hsl(var(--burgundy))',
|
||||
backgroundColor: phase === 0 ? 'hsl(var(--background))' : 'hsl(var(--burgundy) / 0.1)'
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<span className="text-xs font-bold">Ranking:</span>
|
||||
<motion.span
|
||||
key={phase}
|
||||
initial={{ y: -10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 10, opacity: 0 }}
|
||||
className="text-xs font-bold"
|
||||
>
|
||||
#{phase === 0 ? '3' : '5'}
|
||||
</motion.span>
|
||||
{phase === 1 && (
|
||||
<motion.div
|
||||
initial={{ scale: 0, rotate: -180 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
>
|
||||
<TrendingDown className="h-3 w-3 text-[hsl(var(--burgundy))]" />
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* SERP Snippet */}
|
||||
<motion.div
|
||||
className="space-y-2 p-3 rounded-lg bg-white border-2"
|
||||
animate={{
|
||||
borderColor: phase === 0 ? 'hsl(var(--border))' : 'hsl(var(--teal))',
|
||||
boxShadow: phase === 0
|
||||
? '0 1px 3px rgba(0,0,0,0.1)'
|
||||
: '0 0 20px hsl(var(--teal) / 0.3)'
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{/* URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded-full bg-primary" />
|
||||
<span className="text-[10px] text-muted-foreground font-mono">
|
||||
competitor.com/product
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h4 className="text-sm font-bold text-[hsl(var(--primary))] line-clamp-1">
|
||||
Best Enterprise Software Solution 2026
|
||||
</h4>
|
||||
|
||||
{/* Meta Description with change highlighting */}
|
||||
<motion.p
|
||||
className="text-[11px] text-muted-foreground leading-relaxed relative"
|
||||
layout
|
||||
>
|
||||
<motion.span
|
||||
animate={{
|
||||
backgroundColor: phase === 1 ? 'hsl(var(--teal) / 0.2)' : 'transparent'
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="inline-block rounded px-0.5"
|
||||
>
|
||||
{phase === 0 ? oldMeta : newMeta}
|
||||
</motion.span>
|
||||
|
||||
{/* Change indicator */}
|
||||
{phase === 1 && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: -5 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="absolute -right-2 top-0 px-1.5 py-0.5 rounded bg-[hsl(var(--burgundy))] text-[8px] font-bold text-white"
|
||||
>
|
||||
NEW
|
||||
</motion.span>
|
||||
)}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* Alert Badge */}
|
||||
{phase === 1 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 5, scale: 0.9 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
className="flex items-center justify-center gap-2 text-[10px] font-bold text-[hsl(var(--teal))] uppercase tracking-wider"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[hsl(var(--teal))] animate-pulse" />
|
||||
Meta Description Changed
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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