This commit is contained in:
Timo Knuth 2026-01-20 15:54:16 +01:00
parent 9fa8045c26
commit 4733e1a1cc
10 changed files with 290 additions and 756 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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;

View File

@ -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 &amp; 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>

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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 }}