This commit is contained in:
parent
9fa8045c26
commit
4733e1a1cc
|
|
@ -9,10 +9,10 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bullmq": "^5.1.0",
|
"bullmq": "^5.1.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.1.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bullmq": "^5.1.0",
|
"bullmq": "^5.1.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.1.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
|
|
||||||
|
|
@ -42,12 +42,14 @@ app.get('/health', async (_req, res) => {
|
||||||
|
|
||||||
import testRoutes from './routes/test';
|
import testRoutes from './routes/test';
|
||||||
import waitlistRoutes from './routes/waitlist';
|
import waitlistRoutes from './routes/waitlist';
|
||||||
|
import { toolsRouter } from './routes/tools';
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use('/api/auth', authLimiter, authRoutes);
|
app.use('/api/auth', authLimiter, authRoutes);
|
||||||
app.use('/api/monitors', authMiddleware, monitorRoutes);
|
app.use('/api/monitors', authMiddleware, monitorRoutes);
|
||||||
app.use('/api/settings', authMiddleware, settingsRoutes);
|
app.use('/api/settings', authMiddleware, settingsRoutes);
|
||||||
app.use('/api/waitlist', waitlistRoutes); // Public route - no auth required
|
app.use('/api/waitlist', waitlistRoutes); // Public route - no auth required
|
||||||
|
app.use('/api/tools', toolsRouter); // Public tools
|
||||||
app.use('/test', testRoutes);
|
app.use('/test', testRoutes);
|
||||||
|
|
||||||
// 404 handler
|
// 404 handler
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
const previewSchema = z.object({
|
||||||
|
url: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/meta-preview', async (req, res) => {
|
||||||
|
try {
|
||||||
|
let { url } = previewSchema.parse(req.body);
|
||||||
|
|
||||||
|
// Add protocol if missing
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
url = `https://${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (compatible; WebsiteMonitorBot/1.0; +https://websitemonitor.com)'
|
||||||
|
},
|
||||||
|
timeout: 5000,
|
||||||
|
validateStatus: (status) => status < 500 // Resolve even if 404/403 to avoid crashing flow immediately
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
const title = $('title').text() || $('meta[property="og:title"]').attr('content') || '';
|
||||||
|
const description = $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || '';
|
||||||
|
|
||||||
|
// Attempt to find favicon
|
||||||
|
let favicon = '';
|
||||||
|
const linkIcon = $('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]').attr('href');
|
||||||
|
if (linkIcon) {
|
||||||
|
if (linkIcon.startsWith('http')) {
|
||||||
|
favicon = linkIcon;
|
||||||
|
} else if (linkIcon.startsWith('//')) {
|
||||||
|
favicon = `https:${linkIcon}`;
|
||||||
|
} else {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
favicon = `${urlObj.protocol}//${urlObj.host}${linkIcon.startsWith('/') ? '' : '/'}${linkIcon}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
favicon = `${urlObj.protocol}//${urlObj.host}/favicon.ico`;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
title: title.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
favicon,
|
||||||
|
url: url
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Meta preview error:', error);
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return res.status(400).json({ error: 'Invalid URL provided' });
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: 'Failed to fetch page metadata' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toolsRouter = router;
|
||||||
|
|
@ -1,19 +1,42 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ThemeToggle } from '@/components/ui/ThemeToggle'
|
import { ThemeToggle } from '@/components/ui/ThemeToggle'
|
||||||
import { HeroSection, UseCaseShowcase, HowItWorks, Differentiators, SocialProof, FinalCTA } from '@/components/landing/LandingSections'
|
import { HeroSection } from '@/components/landing/LandingSections'
|
||||||
import { LiveStatsBar } from '@/components/landing/LiveStatsBar'
|
|
||||||
import { PricingComparison } from '@/components/landing/PricingComparison'
|
|
||||||
import { SectionDivider } from '@/components/landing/MagneticElements'
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { Check, ChevronDown, Monitor, Globe, Shield, Clock, Zap, Menu } from 'lucide-react'
|
import { Check, ChevronDown, Monitor, Globe, Shield, Clock, Zap, Menu } from 'lucide-react'
|
||||||
|
|
||||||
|
// Dynamic imports for performance optimization (lazy loading)
|
||||||
|
const UseCaseShowcase = dynamic(
|
||||||
|
() => import('@/components/landing/LandingSections').then(mod => ({ default: mod.UseCaseShowcase })),
|
||||||
|
{ ssr: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
const HowItWorks = dynamic(
|
||||||
|
() => import('@/components/landing/LandingSections').then(mod => ({ default: mod.HowItWorks })),
|
||||||
|
{ ssr: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
const Differentiators = dynamic(
|
||||||
|
() => import('@/components/landing/LandingSections').then(mod => ({ default: mod.Differentiators })),
|
||||||
|
{ ssr: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
const FinalCTA = dynamic(
|
||||||
|
() => import('@/components/landing/LandingSections').then(mod => ({ default: mod.FinalCTA })),
|
||||||
|
{ ssr: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
const LiveSerpPreview = dynamic(
|
||||||
|
() => import('@/components/landing/LiveSerpPreview').then(mod => ({ default: mod.LiveSerpPreview })),
|
||||||
|
{ ssr: false }
|
||||||
|
)
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [openFaq, setOpenFaq] = useState<number | null>(null)
|
const [openFaq, setOpenFaq] = useState<number | null>(null)
|
||||||
const [billingPeriod, setBillingPeriod] = useState<'monthly' | 'yearly'>('monthly')
|
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
const [scrollProgress, setScrollProgress] = useState(0)
|
const [scrollProgress, setScrollProgress] = useState(0)
|
||||||
|
|
||||||
|
|
@ -62,7 +85,7 @@ export default function Home() {
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="hidden items-center gap-6 md:flex">
|
<nav className="hidden items-center gap-6 md:flex">
|
||||||
<Link href="#features" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">Features</Link>
|
<Link href="#features" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">Features</Link>
|
||||||
<Link href="#pricing" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">Pricing</Link>
|
<Link href="#use-cases" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">Use Cases</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
@ -96,7 +119,7 @@ export default function Home() {
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Link href="#features" onClick={() => setMobileMenuOpen(false)} className="text-sm font-medium text-muted-foreground hover:text-foreground">Features</Link>
|
<Link href="#features" onClick={() => setMobileMenuOpen(false)} className="text-sm font-medium text-muted-foreground hover:text-foreground">Features</Link>
|
||||||
<Link href="#pricing" onClick={() => setMobileMenuOpen(false)} className="text-sm font-medium text-muted-foreground hover:text-foreground">Pricing</Link>
|
<Link href="#use-cases" onClick={() => setMobileMenuOpen(false)} className="text-sm font-medium text-muted-foreground hover:text-foreground">Use Cases</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
|
|
@ -122,187 +145,18 @@ export default function Home() {
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
|
||||||
{/* Live Stats Bar */}
|
{/* Live SERP Preview Tool */}
|
||||||
<LiveStatsBar />
|
<LiveSerpPreview />
|
||||||
|
|
||||||
{/* Use Case Showcase */}
|
{/* Use Case Showcase */}
|
||||||
<UseCaseShowcase />
|
<UseCaseShowcase />
|
||||||
|
|
||||||
{/* Section Divider: Use Cases -> How It Works */}
|
|
||||||
<SectionDivider variant="wave" toColor="section-bg-4" />
|
|
||||||
|
|
||||||
{/* How It Works */}
|
{/* How It Works */}
|
||||||
<HowItWorks />
|
<HowItWorks />
|
||||||
|
|
||||||
{/* Differentiators */}
|
{/* Differentiators */}
|
||||||
<Differentiators />
|
<Differentiators />
|
||||||
|
|
||||||
{/* Section Divider: Differentiators -> Pricing */}
|
|
||||||
<SectionDivider variant="curve" toColor="section-bg-6" />
|
|
||||||
|
|
||||||
{/* Pricing Comparison */}
|
|
||||||
<PricingComparison />
|
|
||||||
|
|
||||||
{/* Social Proof */}
|
|
||||||
<SocialProof />
|
|
||||||
|
|
||||||
{/* Pricing Section */}
|
|
||||||
< section id="pricing" className="border-t border-border/40 bg-[hsl(var(--section-bg-2))] py-24" >
|
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
|
||||||
<div className="mb-16 text-center">
|
|
||||||
<h2 className="mb-4 text-3xl font-bold sm:text-4xl text-foreground">
|
|
||||||
Simple pricing, no hidden fees
|
|
||||||
</h2>
|
|
||||||
<p className="mb-8 text-lg text-muted-foreground">
|
|
||||||
Start for free and scale as you grow. Change plans anytime.
|
|
||||||
</p>
|
|
||||||
<div className="inline-flex items-center rounded-full bg-background p-1.5 shadow-sm border border-border">
|
|
||||||
<button
|
|
||||||
onClick={() => setBillingPeriod('monthly')}
|
|
||||||
className={`rounded-full px-6 py-2 text-sm font-medium transition-all duration-200 ${billingPeriod === 'monthly' ? 'bg-foreground text-background shadow' : 'text-muted-foreground hover:bg-secondary/50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Monthly
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setBillingPeriod('yearly')}
|
|
||||||
className={`rounded-full px-6 py-2 text-sm font-medium transition-all duration-200 ${billingPeriod === 'yearly' ? 'bg-foreground text-background shadow' : 'text-muted-foreground hover:bg-secondary/50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Yearly <span className="ml-1 text-[10px] opacity-80">(Save 20%)</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-8 md:grid-cols-3 max-w-6xl mx-auto">
|
|
||||||
{/* Starter Plan */}
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ y: -5 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="rounded-3xl border border-border bg-card p-8 shadow-sm hover:shadow-xl hover:border-primary/20 transition-all"
|
|
||||||
>
|
|
||||||
<h3 className="mb-2 text-xl font-bold text-foreground">Starter</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mb-6">Perfect for side projects</p>
|
|
||||||
<div className="mb-8">
|
|
||||||
<span className="text-5xl font-bold tracking-tight text-foreground">$0</span>
|
|
||||||
<span className="text-muted-foreground ml-2">/mo</span>
|
|
||||||
</div>
|
|
||||||
<ul className="mb-8 space-y-4">
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-secondary flex items-center justify-center text-primary">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
3 monitors
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-secondary flex items-center justify-center text-primary">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
Hourly checks
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-secondary flex items-center justify-center text-primary">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
Email alerts
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Button variant="outline" className="w-full rounded-xl h-11 border-border hover:bg-secondary/50 hover:text-foreground">
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Pro Plan */}
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ y: -5 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="relative rounded-3xl border-2 border-primary bg-card p-8 shadow-2xl shadow-primary/10 z-10 scale-105"
|
|
||||||
>
|
|
||||||
<div className="absolute -top-4 left-1/2 -translate-x-1/2 rounded-full bg-primary px-4 py-1 text-xs font-bold text-primary-foreground shadow-lg">
|
|
||||||
MOST POPULAR
|
|
||||||
</div>
|
|
||||||
<h3 className="mb-2 text-xl font-bold text-foreground">Pro</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mb-6">For serious businesses</p>
|
|
||||||
<div className="mb-8">
|
|
||||||
<span className="text-5xl font-bold tracking-tight text-foreground">${billingPeriod === 'monthly' ? '29' : '24'}</span>
|
|
||||||
<span className="text-muted-foreground ml-2">/mo</span>
|
|
||||||
</div>
|
|
||||||
<ul className="mb-8 space-y-4">
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-primary/20 flex items-center justify-center text-primary">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
50 monitors
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-primary/20 flex items-center justify-center text-primary">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
1-minute checks
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-primary/20 flex items-center justify-center text-primary">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
All alert channels (Slack/SMS)
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-primary/20 flex items-center justify-center text-primary">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
SSL monitoring
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Button className="w-full bg-primary hover:bg-primary/90 text-primary-foreground rounded-xl h-11 shadow-lg shadow-primary/20 font-semibold">
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Enterprise Plan */}
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ y: -5 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="rounded-3xl border border-border bg-card p-8 shadow-sm hover:shadow-xl hover:border-border transition-all"
|
|
||||||
>
|
|
||||||
<h3 className="mb-2 text-xl font-bold text-foreground">Enterprise</h3>
|
|
||||||
<p className="text-sm text-muted-foreground mb-6">Custom solutions</p>
|
|
||||||
<div className="mb-8">
|
|
||||||
<span className="text-4xl font-bold tracking-tight text-foreground">Custom</span>
|
|
||||||
</div>
|
|
||||||
<ul className="mb-8 space-y-4">
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-secondary flex items-center justify-center text-muted-foreground">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
Unlimited monitors
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-secondary flex items-center justify-center text-muted-foreground">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
30-second checks
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-secondary flex items-center justify-center text-muted-foreground">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
SSO & SAML
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3 text-sm text-foreground">
|
|
||||||
<div className="h-5 w-5 rounded-full bg-secondary flex items-center justify-center text-muted-foreground">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
Dedicated support
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Button variant="outline" className="w-full rounded-xl h-11 border-border hover:bg-secondary/50 hover:text-foreground">
|
|
||||||
Contact Sales
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section >
|
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
< section id="faq" className="border-t border-border/40 py-24 bg-background" >
|
< section id="faq" className="border-t border-border/40 py-24 bg-background" >
|
||||||
<div className="mx-auto max-w-3xl px-6">
|
<div className="mx-auto max-w-3xl px-6">
|
||||||
|
|
@ -373,7 +227,7 @@ export default function Home() {
|
||||||
<h4 className="mb-4 font-semibold text-foreground">Product</h4>
|
<h4 className="mb-4 font-semibold text-foreground">Product</h4>
|
||||||
<ul className="space-y-3 text-muted-foreground">
|
<ul className="space-y-3 text-muted-foreground">
|
||||||
<li><Link href="#features" className="hover:text-primary transition-colors">Features</Link></li>
|
<li><Link href="#features" className="hover:text-primary transition-colors">Features</Link></li>
|
||||||
<li><Link href="#pricing" className="hover:text-primary transition-colors">Pricing</Link></li>
|
<li><Link href="#use-cases" className="hover:text-primary transition-colors">Use Cases</Link></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -264,8 +264,7 @@ function NoiseToSignalVisual() {
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{
|
animate={{
|
||||||
opacity: phase === 0 ? 1 : 0,
|
opacity: phase === 0 ? 1 : 0,
|
||||||
scale: phase === 0 ? 1 : 0.98,
|
scale: phase === 0 ? 1 : 0.98
|
||||||
filter: phase === 0 ? 'blur(0px)' : 'blur(8px)'
|
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className="space-y-3"
|
className="space-y-3"
|
||||||
|
|
@ -705,110 +704,6 @@ export function Differentiators() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 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-[hsl(var(--section-bg-2))] relative overflow-hidden">
|
|
||||||
{/* Background Pattern - Subtle dots for light theme */}
|
|
||||||
<div className="absolute inset-0 opacity-[0.03]" style={{
|
|
||||||
backgroundImage: `radial-gradient(hsl(var(--foreground)) 1px, transparent 1px)`,
|
|
||||||
backgroundSize: '24px 24px'
|
|
||||||
}} />
|
|
||||||
|
|
||||||
<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">
|
|
||||||
Built for teams who need results,{' '}
|
|
||||||
<span className="text-[hsl(var(--primary))]">not demos.</span>
|
|
||||||
</motion.h2>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Testimonial Cards - Light Theme */}
|
|
||||||
<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 h-full"
|
|
||||||
>
|
|
||||||
{/* Card Container */}
|
|
||||||
<div className="h-full flex flex-col rounded-3xl bg-white border border-zinc-200 shadow-sm p-8 hover:shadow-xl transition-all duration-300">
|
|
||||||
|
|
||||||
{/* Quote Mark */}
|
|
||||||
<div className="text-5xl font-display text-zinc-300 leading-none mb-4">
|
|
||||||
"
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quote */}
|
|
||||||
<p className="font-body text-base leading-relaxed mb-8 text-zinc-900 font-medium italic flex-grow">
|
|
||||||
{testimonial.quote}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Attribution */}
|
|
||||||
<div className="flex items-start justify-between mt-auto pt-6 border-t border-zinc-100">
|
|
||||||
<div>
|
|
||||||
<p className="font-bold text-zinc-900 text-sm">{testimonial.author}</p>
|
|
||||||
<p className="text-xs text-zinc-500">{testimonial.role} at {testimonial.company}</p>
|
|
||||||
</div>
|
|
||||||
<div className="px-3 py-1 rounded-full bg-zinc-100 border border-zinc-200 text-[10px] font-bold uppercase tracking-wider text-zinc-600">
|
|
||||||
{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
|
// 6. FINAL CTA - Get Started
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -827,7 +722,7 @@ export function FinalCTA() {
|
||||||
rotate: [0, 180, 360]
|
rotate: [0, 180, 360]
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
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]"
|
className="absolute top-1/4 -left-20 h-[500px] w-[500px] rounded-full bg-[hsl(var(--primary))] blur-[60px]"
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{
|
animate={{
|
||||||
|
|
@ -836,7 +731,7 @@ export function FinalCTA() {
|
||||||
rotate: [360, 180, 0]
|
rotate: [360, 180, 0]
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 15, repeat: Infinity, ease: "linear", delay: 2 }}
|
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]"
|
className="absolute bottom-1/4 -right-20 h-[500px] w-[500px] rounded-full bg-[hsl(var(--teal))] blur-[60px]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mx-auto max-w-4xl px-6 text-center relative z-10">
|
<div className="mx-auto max-w-4xl px-6 text-center relative z-10">
|
||||||
|
|
@ -868,15 +763,6 @@ export function FinalCTA() {
|
||||||
custom={3}
|
custom={3}
|
||||||
className="flex flex-wrap items-center justify-center gap-6 text-sm text-muted-foreground"
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<Star className="h-4 w-4 fill-current text-[hsl(var(--primary))]" />
|
<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>
|
<span>Early access: <span className="font-semibold text-foreground">50% off for 6 months</span></span>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { Search, Loader2, Globe, AlertCircle, ArrowRight } from 'lucide-react'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface PreviewData {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
favicon: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LiveSerpPreview() {
|
||||||
|
const [url, setUrl] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [data, setData] = useState<PreviewData | null>(null)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const handleAnalyze = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!url) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setError('')
|
||||||
|
setData(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3002'}/api/tools/meta-preview`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch preview')
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
setData(result)
|
||||||
|
} catch (err) {
|
||||||
|
setError('Could not analyze this URL. Please check if it represents a valid, publicly accessible website.')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-gradient-to-b from-background to-[hsl(var(--section-bg-2))] relative overflow-hidden">
|
||||||
|
{/* Background Gradients */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_0%,hsl(var(--primary))_0%,transparent_50%)] opacity-5" />
|
||||||
|
|
||||||
|
<div className="mx-auto max-w-4xl px-6 relative z-10">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<div 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">
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
Free Tool
|
||||||
|
</div>
|
||||||
|
<h2 className="text-4xl font-display font-bold text-foreground mb-4">
|
||||||
|
See how Google sees you
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground text-lg">
|
||||||
|
Enter your URL to get an instant SERP preview.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-xl mx-auto space-y-8">
|
||||||
|
{/* Input Form */}
|
||||||
|
<form onSubmit={handleAnalyze} className="relative group">
|
||||||
|
<div className="absolute -inset-1 bg-gradient-to-r from-[hsl(var(--primary))] to-[hsl(var(--teal))] rounded-xl opacity-20 group-hover:opacity-40 blur transition duration-500" />
|
||||||
|
<div className="relative flex gap-2 p-2 bg-card border border-border rounded-xl shadow-xl">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
placeholder="website.com"
|
||||||
|
className="pl-9 h-12 bg-transparent border-none shadow-none focus-visible:ring-0 text-base"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !url}
|
||||||
|
className="h-12 px-6 bg-[hsl(var(--primary))] hover:bg-[hsl(var(--primary))]/90 text-white font-semibold rounded-lg transition-all"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
'Analyze'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{error && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
className="flex items-center gap-2 p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-500 text-sm"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
{error}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Result Preview */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{data && (
|
||||||
|
<motion.div
|
||||||
|
key="result"
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
{/* Google Result Card */}
|
||||||
|
<div className="p-6 rounded-xl bg-white dark:bg-[#1a1c20] border border-border shadow-2xl">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<div className="p-1 rounded-full bg-gray-100 dark:bg-gray-800">
|
||||||
|
{data.favicon ? (
|
||||||
|
<img src={data.favicon} alt="Favicon" className="w-6 h-6 object-contain" />
|
||||||
|
) : (
|
||||||
|
<Globe className="w-6 h-6 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm text-[#202124] dark:text-[#dadce0] font-normal leading-tight">
|
||||||
|
{new URL(data.url).hostname}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-[#5f6368] dark:text-[#bdc1c6] leading-tight">
|
||||||
|
{data.url}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl text-[#1a0dab] dark:text-[#8ab4f8] font-normal hover:underline cursor-pointer mb-1 leading-snug break-words">
|
||||||
|
{data.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[#4d5156] dark:text-[#bdc1c6] leading-normal">
|
||||||
|
{data.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upsell / CTA */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="text-center space-y-4"
|
||||||
|
>
|
||||||
|
<div className="inline-block p-4 rounded-2xl bg-[hsl(var(--primary))]/5 border border-[hsl(var(--primary))]/20">
|
||||||
|
<p className="text-sm font-medium text-foreground mb-3">
|
||||||
|
Want to know when this changes?
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-[hsl(var(--primary))] text-[hsl(var(--primary))] hover:bg-[hsl(var(--primary))]/10"
|
||||||
|
onClick={() => document.getElementById('waitlist-form')?.scrollIntoView({ behavior: 'smooth' })}
|
||||||
|
>
|
||||||
|
Get notified on changes
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
'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 dark:from-secondary dark:via-secondary dark:to-secondary 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
'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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState } from 'react'
|
||||||
import { Check, ArrowRight, Loader2, Sparkles } from 'lucide-react'
|
import { Check, ArrowRight, Loader2 } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
export function WaitlistForm() {
|
export function WaitlistForm() {
|
||||||
|
|
@ -10,7 +10,6 @@ export function WaitlistForm() {
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [isSuccess, setIsSuccess] = useState(false)
|
const [isSuccess, setIsSuccess] = useState(false)
|
||||||
const [error, setError] = useState('')
|
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 [confetti, setConfetti] = useState<Array<{ id: number; x: number; y: number; rotation: number; color: string }>>([])
|
||||||
|
|
||||||
const validateEmail = (email: string) => {
|
const validateEmail = (email: string) => {
|
||||||
|
|
@ -62,7 +61,6 @@ export function WaitlistForm() {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setQueuePosition(data.position || Math.floor(Math.random() * 500) + 430)
|
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
setIsSuccess(true)
|
setIsSuccess(true)
|
||||||
triggerConfetti()
|
triggerConfetti()
|
||||||
|
|
@ -154,24 +152,6 @@ export function WaitlistForm() {
|
||||||
Check your inbox for confirmation
|
Check your inbox for confirmation
|
||||||
</motion.p>
|
</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 */}
|
{/* Bonus Badge */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue