Production ready

This commit is contained in:
2026-02-09 22:31:22 +01:00
parent fd6e7c44e1
commit 7814548e11
82 changed files with 3390 additions and 2026 deletions

View File

@@ -6,14 +6,15 @@ import PostHogPageView from './PostHogPageView'
export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (typeof window !== 'undefined' && !posthog.__loaded) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY || 'phc_placeholder_key', {
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
if (typeof window !== 'undefined' && !posthog.__loaded && posthogKey) {
posthog.init(posthogKey, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
capture_pageview: false, // Disable automatic pageview capture, as we handle it manually
capture_pageview: false,
capture_pageleave: true,
persistence: 'localStorage+cookie',
opt_out_capturing_by_default: true,
debug: true,
debug: process.env.NODE_ENV === 'development',
})
}
}, [])

View File

@@ -10,20 +10,20 @@ export function CookieBanner() {
const [show, setShow] = useState(false)
useEffect(() => {
const optedIn = posthog.has_opted_in_capturing()
const optedOut = posthog.has_opted_out_capturing()
if (!optedIn && !optedOut) {
const cookieConsent = localStorage.getItem('cookie_consent')
if (!cookieConsent) {
setShow(true)
}
}, [])
const handleAccept = () => {
localStorage.setItem('cookie_consent', 'accepted')
posthog.opt_in_capturing()
setShow(false)
}
const handleDecline = () => {
localStorage.setItem('cookie_consent', 'declined')
posthog.opt_out_capturing()
setShow(false)
}
@@ -45,7 +45,7 @@ export function CookieBanner() {
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground mb-2">We value your privacy</h3>
<p className="text-sm text-muted-foreground mb-4 leading-relaxed">
We use cookies to enhance your browsing experience and analyze our traffic. By clicking "Accept", you consent to our use of cookies.
We use cookies to enhance your browsing experience and analyze our traffic. By clicking &quot;Accept&quot;, you consent to our use of cookies.
Read our <Link href="/privacy" className="underline hover:text-foreground">Privacy Policy</Link>.
</p>
<div className="flex flex-col gap-2 sm:flex-row">

View File

@@ -0,0 +1,112 @@
'use client'
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
export function BackgroundGradient() {
return (
<div className="fixed inset-0 -z-30 overflow-hidden pointer-events-none">
<div
className="absolute inset-x-0 -top-40 -z-30 transform-gpu overflow-hidden blur-3xl sm:-top-80"
aria-hidden="true"
>
<div
className="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-[hsl(var(--primary))] to-[hsl(var(--teal))] opacity-20 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]"
style={{
clipPath:
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
}}
/>
</div>
<div
className="absolute inset-x-0 top-[calc(100%-13rem)] -z-30 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]"
aria-hidden="true"
>
<div
className="relative left-[calc(50%+3rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 bg-gradient-to-tr from-[hsl(var(--burgundy))] to-[hsl(var(--primary))] opacity-20 sm:left-[calc(50%+36rem)] sm:w-[72.1875rem]"
style={{
clipPath:
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
}}
/>
</div>
</div>
)
}
export function FloatingElements() {
return (
<div className="fixed inset-0 -z-20 pointer-events-none overflow-hidden">
{[...Array(6)].map((_, i) => (
<motion.div
key={i}
className="absolute h-64 w-64 rounded-full bg-gradient-to-br from-primary/5 to-transparent blur-3xl"
animate={{
x: [Math.random() * 100 + '%', Math.random() * 100 + '%'],
y: [Math.random() * 100 + '%', Math.random() * 100 + '%'],
scale: [1, 1.2, 1],
opacity: [0.3, 0.5, 0.3],
}}
transition={{
duration: 20 + Math.random() * 10,
repeat: Infinity,
ease: "linear",
}}
style={{
left: Math.random() * 100 + '%',
top: Math.random() * 100 + '%',
}}
/>
))}
</div>
)
}
export function InteractiveGrid() {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePos({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return (
<div className="fixed inset-0 -z-30 pointer-events-none">
<div
className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px]"
/>
<div
className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent"
/>
<motion.div
className="absolute inset-0 bg-[radial-gradient(600px_at_var(--x)_var(--y),hsl(var(--primary)/0.08),transparent_80%)]"
style={{
// @ts-ignore
'--x': mousePos.x + 'px',
'--y': mousePos.y + 'px',
}}
/>
</div>
)
}
export function GlowEffect() {
return (
<div className="fixed inset-0 -z-20 pointer-events-none">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-[120px] mix-blend-screen" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-teal/10 rounded-full blur-[120px] mix-blend-screen" />
</div>
)
}
export function SectionDivider() {
return (
<div className="relative h-px w-full">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-border to-transparent" />
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-primary/20 to-transparent blur-sm" />
</div>
)
}

View File

@@ -1,11 +1,25 @@
'use client'
import { motion } from 'framer-motion'
import { useState, useEffect } from 'react'
import { useState, useEffect, useMemo } from 'react'
import { Bell, ArrowDown } from 'lucide-react'
function resolveHsl(cssVar: string): string {
if (typeof window === 'undefined') return 'transparent'
const value = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim()
return value ? `hsl(${value})` : 'transparent'
}
export function CompetitorDemoVisual() {
const [phase, setPhase] = useState(0)
const [colors, setColors] = useState({ burgundy: '#993350', border: '#27272a' })
useEffect(() => {
setColors({
burgundy: resolveHsl('--burgundy'),
border: resolveHsl('--border'),
})
}, [])
useEffect(() => {
const interval = setInterval(() => {
@@ -15,7 +29,7 @@ export function CompetitorDemoVisual() {
}, [])
return (
<div className="relative h-full min-h-[200px] bg-gradient-to-br from-background via-background to-[hsl(var(--primary))]/5 rounded-xl p-4 overflow-hidden">
<div className="relative h-full bg-gradient-to-br from-background via-background to-[hsl(var(--primary))]/5 rounded-xl p-4 overflow-hidden">
{/* Browser Header */}
<div className="mb-3 flex items-center gap-2 px-2 py-1.5 rounded-md bg-secondary/50 border border-border">
<div className="flex gap-1">
@@ -36,9 +50,9 @@ export function CompetitorDemoVisual() {
<motion.div
className="p-4 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 relative overflow-hidden shadow-xl"
animate={{
borderColor: phase === 1 ? 'hsl(var(--burgundy))' : '#27272a',
borderColor: phase === 1 ? colors.burgundy : colors.border,
boxShadow: phase === 1
? '0 0 20px hsl(var(--burgundy) / 0.2)'
? `0 0 20px ${colors.burgundy}33`
: '0 1px 3px rgba(0,0,0,0.5)'
}}
transition={{ duration: 0.5 }}
@@ -67,7 +81,7 @@ export function CompetitorDemoVisual() {
className="text-3xl font-bold"
animate={{
textDecoration: phase === 1 ? 'line-through' : 'none',
color: phase === 1 ? 'hsl(var(--burgundy))' : '#f4f4f5'
color: phase === 1 ? colors.burgundy : '#f4f4f5'
}}
>
$99

View File

@@ -6,14 +6,15 @@ import { Button } from '@/components/ui/button'
import {
Check, ArrowRight, Shield, Search, FileCheck, TrendingUp,
Target, Filter, Bell, Eye, Slack, Webhook, History,
Zap, Lock, ChevronRight, Star
Zap, Lock, ChevronRight, Star, Accessibility
} from 'lucide-react'
import { useState, useEffect } from 'react'
import { SEODemoVisual } from './SEODemoVisual'
import { CompetitorDemoVisual } from './CompetitorDemoVisual'
import { PolicyDemoVisual } from './PolicyDemoVisual'
import { WaitlistForm } from './WaitlistForm'
import { MagneticButton, SectionDivider } from './MagneticElements'
import { MagneticButton } from './MagneticElements'
import { BackgroundGradient, FloatingElements, InteractiveGrid, GlowEffect, SectionDivider } from './BackgroundEffects'
// Animation Variants
const fadeInUp: Variants = {
@@ -30,21 +31,12 @@ const fadeInUp: Variants = {
})
}
const scaleIn: Variants = {
hidden: { opacity: 0, scale: 0.95 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] }
}
}
// ============================================
// 1. HERO SECTION - "Track competitor changes without the noise"
// 1. HERO SECTION
// ============================================
export function HeroSection() {
return (
<section id="hero" className="relative overflow-hidden pt-32 pb-24 lg:pt-40 lg:pb-32 bg-[hsl(var(--section-bg-1))]">
<section id="hero" className="relative overflow-hidden pt-32 pb-24 lg:pt-40 lg:pb-32 gradient-velvet">
{/* Background Elements */}
<div className="absolute inset-0 grain-texture" />
<div className="absolute right-0 top-20 -z-10 h-[600px] w-[600px] rounded-full bg-[hsl(var(--primary))] opacity-8 blur-[120px]" />
@@ -65,7 +57,7 @@ export function HeroSection() {
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[hsl(var(--teal))] opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-[hsl(var(--teal))]"></span>
</span>
For SEO & Growth Teams
Website Change Monitor for SEO & Growth Teams
</div>
</motion.div>
@@ -75,8 +67,8 @@ export function HeroSection() {
custom={1}
className="text-5xl lg:text-7xl font-display font-bold leading-[1.08] tracking-tight text-foreground"
>
Track competitor changes{' '}
<span className="text-[hsl(var(--primary))]">without the noise.</span>
Monitor website changes &{' '}
<span className="text-[hsl(var(--primary))]">price drops automatically.</span>
</motion.h1>
{/* Subheadline */}
@@ -85,7 +77,7 @@ export function HeroSection() {
custom={2}
className="text-xl lg:text-2xl text-muted-foreground font-body leading-relaxed max-w-2xl"
>
Less noise. More signal. Proof included.
Less noise. More signal. Visual proof included.
</motion.p>
{/* Feature Bullets */}
@@ -156,7 +148,44 @@ export function HeroSection() {
)
}
// Noise → Signal Animation Component - Enhanced
// ============================================
// 1b. TRUST SECTION - "As seen on..."
// ============================================
function TrustSectionDeprecated() {
const logos = [
{ name: 'SEO Clarity', color: 'text-muted-foreground' },
{ name: 'Search Engine Journal', color: 'text-muted-foreground' },
{ name: 'Moz', color: 'text-muted-foreground' },
{ name: 'Ahrefs', color: 'text-muted-foreground' },
{ name: 'Semrush', color: 'text-muted-foreground' }
]
return (
<section className="py-12 border-y border-border/50 bg-secondary/10">
<div className="mx-auto max-w-7xl px-6">
<p className="text-center text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground/80 mb-8">
The Essential Toolkit for Industry Leaders
</p>
<div className="flex flex-wrap justify-center items-center gap-x-12 gap-y-8 opacity-40 grayscale hover:grayscale-0 transition-all duration-700">
{logos.map((logo, i) => (
<motion.div
key={i}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1, duration: 0.8 }}
className={`text-xl font-display font-black tracking-tighter ${logo.color}`}
>
{logo.name}
</motion.div>
))}
</div>
</div>
</section>
)
}
// Noise → Signal Animation Component
function NoiseToSignalVisual() {
const [phase, setPhase] = useState(0)
const [isPaused, setIsPaused] = useState(false)
@@ -167,7 +196,6 @@ function NoiseToSignalVisual() {
const interval = setInterval(() => {
setPhase(p => {
const nextPhase = (p + 1) % 4
// Trigger particles when transitioning from phase 0 to 1
if (p === 0 && nextPhase === 1) {
triggerParticles()
}
@@ -189,7 +217,7 @@ function NoiseToSignalVisual() {
return (
<motion.div
className="relative aspect-[4/3] rounded-3xl border-2 border-border bg-card/50 backdrop-blur-sm shadow-2xl overflow-hidden cursor-pointer group"
className="relative aspect-[4/3] min-h-[320px] rounded-3xl border-2 border-border bg-card/50 backdrop-blur-sm shadow-2xl overflow-hidden cursor-pointer group"
style={{ perspective: '1000px' }}
whileHover={{ rotateY: 2, rotateX: -2, scale: 1.02 }}
transition={{ duration: 0.3 }}
@@ -249,7 +277,6 @@ function NoiseToSignalVisual() {
{/* Content Area */}
<div className="p-8 space-y-4 relative">
{/* Noise Counter */}
<motion.div
className="absolute top-4 left-4 px-3 py-1 rounded-full bg-background/80 backdrop-blur-sm border border-border text-xs font-mono font-semibold"
animate={{
@@ -266,176 +293,80 @@ function NoiseToSignalVisual() {
opacity: phase === 0 ? 1 : 0,
scale: phase === 0 ? 1 : 0.98
}}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
transition={{ duration: 0.5 }}
className="space-y-3"
>
{/* Cookie Banner - with strikethrough */}
<motion.div
className="flex items-center justify-between p-3 rounded-lg bg-muted/30 border border-border/40 relative overflow-hidden"
animate={{
x: phase >= 1 ? -10 : 0,
opacity: phase >= 1 ? 0.3 : 1
}}
transition={{ duration: 0.4 }}
>
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/30 border border-border/40 relative overflow-hidden">
<span className="text-xs text-muted-foreground">🍪 Cookie Banner</span>
<span className="text-xs text-red-500 font-semibold">
NOISE
</span>
{/* Strikethrough animation */}
{phase >= 1 && (
<motion.div
className="absolute inset-0 border-t-2 border-red-500 top-1/2"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.3 }}
/>
)}
</motion.div>
{/* Enterprise Plan Card */}
<span className="text-xs text-red-500 font-semibold">NOISE</span>
</div>
<div className="p-4 rounded-lg bg-background border border-border">
<p className="text-sm font-semibold text-foreground mb-2">Enterprise Plan</p>
<p className="text-2xl font-bold text-[hsl(var(--burgundy))]">$99/mo</p>
</div>
{/* Timestamp - with strikethrough */}
<motion.div
className="flex items-center justify-between p-3 rounded-lg bg-muted/30 border border-border/40 relative overflow-hidden"
animate={{
x: phase >= 1 ? -10 : 0,
opacity: phase >= 1 ? 0.3 : 1
}}
transition={{ duration: 0.4, delay: 0.1 }}
>
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/30 border border-border/40 relative overflow-hidden">
<span className="text-xs text-muted-foreground"> Last updated: 10:23 AM</span>
<span className="text-xs text-red-500 font-semibold">
NOISE
</span>
{/* Strikethrough animation */}
{phase >= 1 && (
<motion.div
className="absolute inset-0 border-t-2 border-red-500 top-1/2"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ duration: 0.3, delay: 0.1 }}
/>
)}
</motion.div>
<span className="text-xs text-red-500 font-semibold">NOISE</span>
</div>
</motion.div>
{/* Phase 1-3: Filtered + Highlighted Signal */}
{/* Phase 1-3: Signal */}
{phase >= 1 && (
<motion.div
initial={{ opacity: 0, scale: 0.85, rotateX: -15 }}
animate={{
opacity: 1,
scale: 1,
rotateX: 0
}}
transition={{
duration: 0.6,
ease: [0.22, 1, 0.36, 1],
scale: { type: 'spring', stiffness: 300, damping: 20 }
}}
initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: 1, scale: 1 }}
className="absolute inset-0 flex items-center justify-center p-8"
>
<motion.div
className="w-full p-6 rounded-2xl bg-white dark:bg-zinc-950 border-2 border-[hsl(var(--teal))] dark:border-zinc-800 shadow-2xl relative overflow-hidden"
animate={{
boxShadow: [
'0 20px 60px rgba(20, 184, 166, 0.1)',
'0 20px 80px rgba(20, 184, 166, 0.2)',
'0 20px 60px rgba(20, 184, 166, 0.1)'
]
}}
transition={{ duration: 2, repeat: Infinity }}
>
{/* Animated corner accent */}
<motion.div
className="absolute top-0 right-0 w-20 h-20 bg-[hsl(var(--teal))]/5 rounded-bl-full"
animate={{ scale: [1, 1.1, 1] }}
transition={{ duration: 2, repeat: Infinity }}
/>
<div className="relative z-10">
<div className="flex items-center justify-between mb-2">
<motion.span
className="text-xs font-bold uppercase tracking-wider text-[hsl(var(--teal))] dark:text-[hsl(var(--teal))]"
animate={{ opacity: [1, 0.7, 1] }}
transition={{ duration: 1.5, repeat: Infinity }}
>
SIGNAL DETECTED
</motion.span>
<div className="flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--teal))] dark:text-[hsl(var(--teal))]">
<Filter className="h-3 w-3" />
Filtered
</div>
<div className="w-full p-6 rounded-2xl bg-white dark:bg-zinc-950 border-2 border-[hsl(var(--teal))] shadow-2xl relative">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-bold uppercase tracking-wider text-[hsl(var(--teal))]"> SIGNAL DETECTED</span>
<div className="flex items-center gap-1.5 text-xs text-[hsl(var(--teal))]">
<Filter className="h-3 w-3" /> Filtered
</div>
<p className="text-sm font-semibold text-muted-foreground dark:text-zinc-400 mb-3">Enterprise Plan</p>
<div className="flex items-baseline gap-3">
<p className="text-3xl font-bold text-foreground dark:text-zinc-600/50">$99/mo</p>
</div>
<p className="text-sm font-semibold text-muted-foreground mb-3">Enterprise Plan</p>
<div className="flex items-baseline gap-3">
<p className="text-3xl font-bold text-foreground">$99/mo</p>
{phase >= 2 && (
<motion.p
initial={{ opacity: 0, x: -10, scale: 0.9 }}
animate={{
opacity: phase >= 2 ? 1 : 0,
x: phase >= 2 ? 0 : -10,
scale: phase >= 2 ? 1 : 0.9
}}
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="text-lg text-[hsl(var(--burgundy))] dark:text-red-500 font-bold flex items-center gap-1"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="text-3xl text-[hsl(var(--burgundy))] font-bold"
>
<span></span>
<motion.span
animate={{ scale: phase === 2 ? [1, 1.1, 1] : 1 }}
transition={{ duration: 0.5 }}
className="text-3xl"
>
$79/mo
</motion.span>
$79/mo
</motion.p>
</div>
{/* Alert badge */}
{phase >= 3 && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[hsl(var(--burgundy))]/10 border border-[hsl(var(--burgundy))]/30 dark:bg-red-500/10 dark:border-red-500/20"
>
<Bell className="h-3 w-3 text-[hsl(var(--burgundy))] dark:text-red-500" />
<span className="text-[10px] font-bold text-[hsl(var(--burgundy))] dark:text-red-500 uppercase tracking-wider">
Alert Sent
</span>
</motion.div>
)}
</div>
</motion.div>
{phase >= 3 && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[hsl(var(--burgundy))]/10 border border-[hsl(var(--burgundy))]/30"
>
<Bell className="h-3 w-3 text-[hsl(var(--burgundy))]" />
<span className="text-[10px] font-bold text-[hsl(var(--burgundy))] uppercase tracking-wider">Alert Sent</span>
</motion.div>
)}
</div>
</motion.div>
)}
{/* Phase Indicator */}
<div className="absolute bottom-4 right-4 flex gap-1.5">
{[0, 1, 2, 3].map(i => (
<motion.div
<div
key={i}
animate={{
width: phase === i ? 24 : 6,
backgroundColor: phase === i ? 'hsl(var(--teal))' : 'hsl(var(--border))'
}}
transition={{ duration: 0.3 }}
className="h-1.5 rounded-full"
className={`h-1.5 rounded-full transition-all duration-300 ${phase === i ? 'w-6 bg-[hsl(var(--teal))]' : 'w-1.5 bg-border'}`}
/>
))}
</div>
</div >
</motion.div >
</div>
</motion.div>
)
}
// ============================================
// 2. USE CASE SHOWCASE - SEO, Competitor, Policy
// 2. USE CASE SHOWCASE
// ============================================
export function UseCaseShowcase() {
const useCases = [
@@ -445,7 +376,6 @@ export function UseCaseShowcase() {
problem: 'Your rankings drop before you know why.',
example: 'Track when competitors update meta descriptions or add new content sections that outrank you.',
color: 'teal',
gradient: 'from-[hsl(var(--teal))]/10 to-transparent',
demoComponent: <SEODemoVisual />
},
{
@@ -454,7 +384,6 @@ export function UseCaseShowcase() {
problem: 'Competitor launches slip past your radar.',
example: 'Monitor pricing pages, product launches, and promotional campaigns in real-time.',
color: 'primary',
gradient: 'from-[hsl(var(--primary))]/10 to-transparent',
demoComponent: <CompetitorDemoVisual />
},
{
@@ -463,83 +392,55 @@ export function UseCaseShowcase() {
problem: 'Regulatory updates appear without warning.',
example: 'Track policy changes, terms updates, and legal text modifications with audit-proof history.',
color: 'burgundy',
gradient: 'from-[hsl(var(--burgundy))]/10 to-transparent',
demoComponent: <PolicyDemoVisual />
}
]
return (
<section className="py-32 bg-[hsl(var(--section-bg-3))] relative overflow-hidden">
{/* Background Decor - Enhanced Grid */}
<section className="py-32 relative overflow-hidden">
<div className="absolute inset-0 bg-[linear-gradient(to_right,hsl(var(--border))_1px,transparent_1px),linear-gradient(to_bottom,hsl(var(--border))_1px,transparent_1px)] bg-[size:4rem_4rem] opacity-30 [mask-image:radial-gradient(ellipse_80%_50%_at_50%_50%,#000_70%,transparent_100%)]" />
<div className="mx-auto max-w-7xl px-6 relative z-10">
{/* Section Header */}
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
className="text-center mb-20"
>
<motion.div variants={fadeInUp} className="inline-flex items-center gap-2 rounded-full bg-secondary border border-border px-4 py-1.5 text-sm font-medium text-foreground mb-6">
<Eye className="h-4 w-4" />
Who This Is For
<div className="text-center mb-20">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="inline-flex items-center gap-2 rounded-full bg-secondary border border-border px-4 py-1.5 text-sm font-medium text-foreground mb-6"
>
<Eye className="h-4 w-4" /> Who This Is For
</motion.div>
<motion.h2 variants={fadeInUp} custom={1} className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">
Built for teams who need results,{' '}
<span className="text-muted-foreground">not demos.</span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6"
>
Built for teams who need results, <span className="text-muted-foreground">not demos.</span>
</motion.h2>
</motion.div>
</div>
{/* Use Case Cards - Diagonal Cascade */}
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
{useCases.map((useCase, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 40, rotateX: 10 }}
whileInView={{ opacity: 1, y: 0, rotateX: 0 }}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.15, duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
whileHover={{ y: -12, scale: 1.02, transition: { duration: 0.3 } }}
className="group relative glass-card rounded-3xl shadow-xl hover:shadow-2xl transition-all overflow-hidden"
transition={{ delay: i * 0.15 }}
className="group relative glass-card rounded-3xl p-8 shadow-xl transition-all"
>
{/* Gradient Background */}
<div className={`absolute inset-0 rounded-3xl bg-gradient-to-br ${useCase.gradient} opacity-0 group-hover:opacity-100 transition-opacity`} />
<div className="relative z-10 p-8 space-y-6">
{/* Icon */}
<motion.div
whileHover={{ rotate: 5, scale: 1.1 }}
transition={{ duration: 0.2 }}
className={`inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-[hsl(var(--${useCase.color}))]/10 text-[hsl(var(--${useCase.color}))] border border-[hsl(var(--${useCase.color}))]/20`}
>
{useCase.icon}
</motion.div>
{/* Title */}
<h3 className="text-2xl font-display font-bold text-foreground">
{useCase.title}
</h3>
{/* Problem Statement */}
<p className="text-sm font-semibold text-muted-foreground">
{useCase.problem}
</p>
{/* Animated Demo Visual */}
<div className="!mt-6 rounded-xl overflow-hidden border border-border/50 shadow-inner">
{useCase.demoComponent}
</div>
{/* Example Scenario */}
<div className="pt-4 border-t border-border">
<p className="text-xs uppercase tracking-wider font-bold text-muted-foreground mb-2">
Example:
</p>
<p className="text-sm text-foreground leading-relaxed">
{useCase.example}
</p>
</div>
<div className={`inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-[hsl(var(--${useCase.color}))]/10 text-[hsl(var(--${useCase.color}))] border border-[hsl(var(--${useCase.color}))]/20 mb-6`}>
{useCase.icon}
</div>
<h3 className="text-2xl font-display font-bold text-foreground mb-4">{useCase.title}</h3>
<p className="text-sm font-semibold text-muted-foreground mb-6">{useCase.problem}</p>
<div className="rounded-xl overflow-hidden border border-border/50 shadow-inner mb-6 h-[280px]">
{useCase.demoComponent}
</div>
<div className="pt-4 border-t border-border">
<p className="text-xs uppercase tracking-wider font-bold text-muted-foreground mb-2">Example:</p>
<p className="text-sm text-foreground leading-relaxed">{useCase.example}</p>
</div>
</motion.div>
))}
@@ -550,7 +451,7 @@ export function UseCaseShowcase() {
}
// ============================================
// 3. HOW IT WORKS - 4 Stage Flow
// 3. HOW IT WORKS
// ============================================
export function HowItWorks() {
const stages = [
@@ -561,69 +462,33 @@ export function HowItWorks() {
]
return (
<section className="py-32 bg-gradient-to-b from-[hsl(var(--section-bg-4))] to-[hsl(var(--section-bg-5))] relative overflow-hidden">
{/* Subtle Diagonal Stripe Decoration */}
<div className="absolute inset-0 opacity-5" style={{ backgroundImage: 'repeating-linear-gradient(45deg, hsl(var(--primary)), hsl(var(--primary)) 2px, transparent 2px, transparent 40px)' }} />
<section className="py-32 relative overflow-hidden">
<div className="mx-auto max-w-7xl px-6 relative z-10">
{/* Section Header */}
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="text-center mb-20"
>
<motion.h2 variants={fadeInUp} className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">
How it works
</motion.h2>
<motion.p variants={fadeInUp} custom={1} className="text-xl text-muted-foreground max-w-2xl mx-auto">
Four simple steps to never miss an important change again.
</motion.p>
</motion.div>
<div className="text-center mb-20">
<h2 className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">How it works</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">Four simple steps to never miss an important change again.</p>
</div>
{/* Horizontal Flow */}
<div className="relative">
{/* Connecting Line */}
<div className="absolute top-1/2 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-border to-transparent -translate-y-1/2 hidden lg:block" />
<div className="grid lg:grid-cols-4 gap-8 lg:gap-4">
{stages.map((stage, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1, duration: 0.5 }}
className="relative flex flex-col items-center text-center group"
>
{/* Large Number Background */}
<div className="absolute -top-4 left-1/2 -translate-x-1/2 text-8xl font-display font-bold text-border/20 pointer-events-none">
{String(i + 1).padStart(2, '0')}
</div>
{/* Circle Container */}
<div className="relative z-10 mb-6 flex h-20 w-20 items-center justify-center rounded-full border-2 border-border bg-card shadow-lg group-hover:shadow-2xl group-hover:border-[hsl(var(--primary))] group-hover:bg-[hsl(var(--primary))]/5 transition-all">
<div className="text-[hsl(var(--primary))]">
{stage.icon}
</div>
</div>
{/* Text */}
<h3 className="text-lg font-bold text-foreground mb-2">
{stage.title}
</h3>
<p className="text-sm text-muted-foreground max-w-[200px]">
{stage.desc}
</p>
{/* Arrow (not on last) */}
{i < stages.length - 1 && (
<div className="hidden lg:block absolute top-10 -right-4 text-border">
<ChevronRight className="h-6 w-6" />
</div>
)}
</motion.div>
))}
</div>
<div className="grid lg:grid-cols-4 gap-8">
{stages.map((stage, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1 }}
className="relative flex flex-col items-center text-center group"
>
<div className="absolute -top-4 left-1/2 -translate-x-1/2 text-8xl font-display font-bold text-border/20 pointer-events-none">
{String(i + 1).padStart(2, '0')}
</div>
<div className="relative z-10 mb-6 flex h-20 w-20 items-center justify-center rounded-full border-2 border-border bg-card shadow-lg transition-all group-hover:border-[hsl(var(--primary))]">
<div className="text-[hsl(var(--primary))]">{stage.icon}</div>
</div>
<h3 className="text-lg font-bold text-foreground mb-2">{stage.title}</h3>
<p className="text-sm text-muted-foreground max-w-[200px]">{stage.desc}</p>
</motion.div>
))}
</div>
</div>
</section>
@@ -631,7 +496,7 @@ export function HowItWorks() {
}
// ============================================
// 4. DIFFERENTIATORS - Why We're Better
// 4. DIFFERENTIATORS
// ============================================
export function Differentiators() {
const features = [
@@ -640,31 +505,19 @@ export function Differentiators() {
{ feature: 'Integrations', others: 'Email only', us: 'Slack, Webhooks, Teams', icon: <Slack className="h-5 w-5" /> },
{ feature: 'History & Proof', others: '7-30 days', us: 'Unlimited snapshots', icon: <History className="h-5 w-5" /> },
{ feature: 'Setup Time', others: '15+ min', us: '2 minutes', icon: <Zap className="h-5 w-5" /> },
{ feature: 'Pricing', others: '$29-99/mo', us: 'Fair pay-per-use', icon: <Shield className="h-5 w-5" /> }
{ feature: 'Pricing', others: '$29-99/mo', us: 'Free plan + fair scaling', icon: <Shield className="h-5 w-5" /> }
]
return (
<section className="py-32 bg-[hsl(var(--section-bg-5))] relative overflow-hidden">
{/* Radial Gradient Overlay */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_50%,hsl(var(--teal))_0%,transparent_50%)] opacity-5" />
<section className="py-32 relative overflow-hidden">
<div className="mx-auto max-w-6xl px-6 relative z-10">
{/* Section Header */}
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="text-center mb-20"
>
<motion.h2 variants={fadeInUp} className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">
Why we're{' '}
<span className="text-[hsl(var(--teal))]">different</span>
</motion.h2>
<motion.p variants={fadeInUp} custom={1} className="text-xl text-muted-foreground max-w-2xl mx-auto">
Not all monitoring tools are created equal. Here's what sets us apart.
</motion.p>
</motion.div>
<div className="text-center mb-20">
<h2 className="text-4xl lg:text-5xl font-display font-bold text-foreground mb-6">
{"Why we're"} <span className="text-[hsl(var(--teal))]">different</span>
</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">{"Not all monitoring tools are created equal. Here's what sets us apart."}</p>
</div>
{/* Feature Cards Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((item, i) => (
<motion.div
@@ -672,20 +525,13 @@ export function Differentiators() {
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: i * 0.05, duration: 0.4 }}
className="group relative glass-card rounded-2xl p-6 hover:border-[hsl(var(--teal))]/30 hover:shadow-xl transition-all hover:-translate-y-1"
transition={{ delay: i * 0.05 }}
className="glass-card rounded-2xl p-6 hover:border-[hsl(var(--teal))]/30 transition-all"
>
{/* Icon */}
<div className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-[hsl(var(--teal))]/10 text-[hsl(var(--teal))] mb-4 group-hover:scale-110 transition-transform">
<div className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-[hsl(var(--teal))]/10 text-[hsl(var(--teal))] mb-4">
{item.icon}
</div>
{/* Feature Name */}
<h3 className="text-lg font-bold text-foreground mb-4">
{item.feature}
</h3>
{/* Comparison */}
<h3 className="text-lg font-bold text-foreground mb-4">{item.feature}</h3>
<div className="space-y-3">
<div className="flex items-start gap-2">
<span className="text-xs uppercase tracking-wider font-bold text-muted-foreground flex-shrink-0 mt-0.5">Others:</span>
@@ -705,69 +551,36 @@ export function Differentiators() {
}
// ============================================
// 6. FINAL CTA - Get Started
// 6. FINAL CTA
// ============================================
export function FinalCTA() {
return (
<section className="relative overflow-hidden py-32">
{/* Animated Gradient Mesh Background - More dramatic */}
<div className="absolute inset-0 bg-gradient-to-br from-[hsl(var(--primary))]/30 via-[hsl(var(--burgundy))]/20 to-[hsl(var(--teal))]/30 opacity-70" />
<div className="absolute inset-0 gradient-velvet opacity-90" />
<div className="absolute inset-0 grain-texture" />
{/* Animated Orbs - Enhanced */}
<motion.div
animate={{
scale: [1, 1.3, 1],
opacity: [0.4, 0.6, 0.4],
rotate: [0, 180, 360]
}}
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
className="absolute top-1/4 -left-20 h-[500px] w-[500px] rounded-full bg-[hsl(var(--primary))] blur-[60px]"
/>
<motion.div
animate={{
scale: [1, 1.2, 1],
opacity: [0.4, 0.5, 0.4],
rotate: [360, 180, 0]
}}
transition={{ duration: 15, repeat: Infinity, ease: "linear", delay: 2 }}
className="absolute bottom-1/4 -right-20 h-[500px] w-[500px] rounded-full bg-[hsl(var(--teal))] blur-[60px]"
/>
<div className="mx-auto max-w-4xl px-6 text-center relative z-10">
<motion.div
initial="hidden"
whileInView="visible"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="space-y-8"
>
{/* Headline */}
<motion.h2 variants={fadeInUp} className="text-5xl lg:text-6xl font-display font-bold leading-tight text-foreground">
Stop missing the changes{' '}
<span className="text-[hsl(var(--primary))]">that matter.</span>
</motion.h2>
{/* Subheadline */}
<motion.p variants={fadeInUp} custom={1} className="text-xl lg:text-2xl text-muted-foreground max-w-2xl mx-auto">
<h2 className="text-5xl lg:text-6xl font-display font-bold leading-tight text-foreground">
Stop missing the changes <span className="text-[hsl(var(--primary))]">that matter.</span>
</h2>
<p className="text-xl lg:text-2xl text-muted-foreground max-w-2xl mx-auto">
Join the waitlist and be first to experience monitoring that actually works.
</motion.p>
{/* Waitlist Form */}
<motion.div variants={fadeInUp} custom={2} className="pt-4 max-w-lg mx-auto">
</p>
<div className="pt-4 max-w-lg mx-auto">
<WaitlistForm />
</motion.div>
{/* Social Proof Indicator */}
<motion.div
variants={fadeInUp}
custom={3}
className="flex flex-wrap items-center justify-center gap-6 text-sm text-muted-foreground"
>
</div>
<div className="flex flex-wrap items-center justify-center gap-6 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Star className="h-4 w-4 fill-current text-[hsl(var(--primary))]" />
<span>Early access</span>
<span>Join the waitlist for early access</span>
</div>
</motion.div>
</div>
</motion.div>
</div>
</section>

View File

@@ -1,178 +1,182 @@
'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>
'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 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 */}
<div className="min-h-[260px]">
<AnimatePresence mode="wait">
{data && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="text-center space-y-4"
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"
>
<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?
{/* 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 ? (
// Dynamic external favicon URLs are not known at build time.
// eslint-disable-next-line @next/next/no-img-element
<img src={data.favicon} alt="Favicon" className="w-6 h-6 object-contain" width="24" height="24" />
) : (
<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>
<Button
variant="outline"
className="border-[hsl(var(--primary))] text-[hsl(var(--primary))] hover:bg-[hsl(var(--primary))]/10"
onClick={() => document.getElementById('hero')?.scrollIntoView({ behavior: 'smooth' })}
>
Get notified on changes
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</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('hero')?.scrollIntoView({ behavior: 'smooth' })}
>
Get notified on changes
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</motion.div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
)}
</AnimatePresence>
</div>
</div>
</div>
</section>
)
}
)
}

View File

@@ -4,8 +4,24 @@ import { motion } from 'framer-motion'
import { useState, useEffect } from 'react'
import { FileCheck, Check } from 'lucide-react'
function resolveHsl(cssVar: string): string {
if (typeof window === 'undefined') return 'transparent'
const value = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim()
return value ? `hsl(${value})` : 'transparent'
}
export function PolicyDemoVisual() {
const [phase, setPhase] = useState(0)
const [colors, setColors] = useState({ burgundy: '#993350', teal: '#2e6b6a', border: '#27272a', mutedFg: '#aba49d' })
useEffect(() => {
setColors({
burgundy: resolveHsl('--burgundy'),
teal: resolveHsl('--teal'),
border: resolveHsl('--border'),
mutedFg: resolveHsl('--muted-foreground'),
})
}, [])
useEffect(() => {
const interval = setInterval(() => {
@@ -15,7 +31,7 @@ export function PolicyDemoVisual() {
}, [])
return (
<div className="relative h-full min-h-[200px] bg-gradient-to-br from-background via-background to-[hsl(var(--burgundy))]/5 rounded-xl p-4 overflow-hidden">
<div className="relative h-full bg-gradient-to-br from-background via-background to-[hsl(var(--burgundy))]/5 rounded-xl p-4 overflow-hidden">
{/* Document Header */}
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -25,9 +41,9 @@ export function PolicyDemoVisual() {
<motion.div
className="px-2 py-0.5 rounded-full border text-[9px] font-bold"
animate={{
borderColor: phase === 1 ? 'hsl(var(--teal))' : 'hsl(var(--border))',
backgroundColor: phase === 1 ? 'hsl(var(--teal) / 0.1)' : 'transparent',
color: phase === 1 ? 'hsl(var(--teal))' : 'hsl(var(--muted-foreground))'
borderColor: phase === 1 ? colors.teal : colors.border,
backgroundColor: phase === 1 ? `${colors.teal}1a` : 'rgba(0,0,0,0)',
color: phase === 1 ? colors.teal : colors.mutedFg
}}
transition={{ duration: 0.5 }}
>
@@ -39,9 +55,9 @@ export function PolicyDemoVisual() {
<motion.div
className="space-y-2 p-3 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 overflow-hidden"
animate={{
borderColor: phase === 1 ? 'hsl(var(--burgundy))' : '#27272a',
borderColor: phase === 1 ? colors.burgundy : colors.border,
boxShadow: phase === 1
? '0 0 20px hsl(var(--burgundy) / 0.2)'
? `0 0 20px ${colors.burgundy}33`
: '0 1px 3px rgba(0,0,0,0.2)'
}}
transition={{ duration: 0.5 }}
@@ -63,10 +79,10 @@ export function PolicyDemoVisual() {
>
<motion.p
animate={{
backgroundColor: phase === 1 ? 'hsl(var(--burgundy) / 0.1)' : 'transparent',
backgroundColor: phase === 1 ? `${colors.burgundy}1a` : 'rgba(0,0,0,0)',
paddingLeft: phase === 1 ? '4px' : '0px',
paddingRight: phase === 1 ? '4px' : '0px',
color: phase === 1 ? 'hsl(var(--burgundy))' : 'inherit',
color: phase === 1 ? colors.burgundy : 'inherit',
fontWeight: phase === 1 ? 600 : 400
}}
transition={{ duration: 0.4 }}

View File

@@ -4,8 +4,23 @@ import { motion } from 'framer-motion'
import { useState, useEffect } from 'react'
import { TrendingDown, TrendingUp } from 'lucide-react'
function resolveHsl(cssVar: string): string {
if (typeof window === 'undefined') return 'transparent'
const value = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim()
return value ? `hsl(${value})` : 'transparent'
}
export function SEODemoVisual() {
const [phase, setPhase] = useState(0)
const [colors, setColors] = useState({ burgundy: '#993350', border: '#27272a', background: '#0c0b09' })
useEffect(() => {
setColors({
burgundy: resolveHsl('--burgundy'),
border: resolveHsl('--border'),
background: resolveHsl('--background'),
})
}, [])
useEffect(() => {
const interval = setInterval(() => {
@@ -18,7 +33,7 @@ export function SEODemoVisual() {
const newMeta = "Best enterprise software for teams of all sizes. Try free for 30 days. Now with AI-powered analytics and real-time collaboration."
return (
<div className="relative h-full min-h-[200px] bg-gradient-to-br from-background via-background to-[hsl(var(--teal))]/5 rounded-xl p-4 overflow-hidden">
<div className="relative h-full bg-gradient-to-br from-background via-background to-[hsl(var(--teal))]/5 rounded-xl p-4 overflow-hidden">
{/* SERP Result */}
<div className="space-y-4">
{/* Ranking Indicator */}
@@ -29,8 +44,8 @@ export function SEODemoVisual() {
<motion.div
className="flex items-center gap-1 px-2 py-1 rounded-full bg-background border border-border"
animate={{
borderColor: phase === 0 ? 'hsl(var(--border))' : 'hsl(var(--burgundy))',
backgroundColor: phase === 0 ? 'hsl(var(--background))' : 'hsl(var(--burgundy) / 0.1)'
borderColor: phase === 0 ? colors.border : colors.burgundy,
backgroundColor: phase === 0 ? colors.background : `${colors.burgundy}1a`
}}
transition={{ duration: 0.5 }}
>
@@ -59,10 +74,10 @@ export function SEODemoVisual() {
<motion.div
className="space-y-2 p-3 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950"
animate={{
borderColor: phase === 0 ? '#27272a' : 'hsl(var(--burgundy))',
borderColor: phase === 0 ? colors.border : colors.burgundy,
boxShadow: phase === 0
? '0 1px 3px rgba(0,0,0,0.2)'
: '0 0 20px hsl(var(--burgundy) / 0.2)'
: `0 0 20px ${colors.burgundy}33`
}}
transition={{ duration: 0.5 }}
>
@@ -86,8 +101,8 @@ export function SEODemoVisual() {
>
<motion.span
animate={{
backgroundColor: phase === 1 ? 'hsl(var(--burgundy) / 0.1)' : 'transparent',
color: phase === 1 ? 'hsl(var(--burgundy))' : 'inherit'
backgroundColor: phase === 1 ? `${colors.burgundy}1a` : 'rgba(0,0,0,0)',
color: phase === 1 ? colors.burgundy : 'inherit'
}}
transition={{ duration: 0.5 }}
className="inline-block rounded px-0.5"

View File

@@ -4,6 +4,7 @@ import { motion, AnimatePresence } from 'framer-motion'
import { useState } from 'react'
import { Check, ArrowRight, Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { MagneticButton } from './MagneticElements'
interface WaitlistFormProps {
id?: string
@@ -138,14 +139,14 @@ export function WaitlistForm({ id }: WaitlistFormProps) {
</motion.div>
{/* Success Message */}
<motion.h3
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-3 text-3xl font-display font-bold text-foreground"
>
You're on the list!
</motion.h3>
<motion.h3
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-3 text-3xl font-display font-bold text-foreground"
>
{"You're on the list!"}
</motion.h3>
<motion.p
initial={{ opacity: 0 }}
@@ -203,24 +204,26 @@ export function WaitlistForm({ id }: WaitlistFormProps) {
</motion.div>
{/* Submit Button */}
<Button
type="submit"
disabled={isSubmitting || !email}
size="lg"
className="h-14 rounded-full bg-[hsl(var(--burgundy))] px-8 text-white hover:bg-[hsl(var(--burgundy))]/90 shadow-2xl shadow-[hsl(var(--burgundy))]/30 transition-all hover:scale-105 disabled:hover:scale-100 disabled:opacity-50 disabled:cursor-not-allowed font-bold text-base group whitespace-nowrap"
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Joining...
</>
) : (
<>
Reserve Your Spot
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</>
)}
</Button>
<MagneticButton strength={0.3}>
<Button
type="submit"
disabled={isSubmitting || !email}
size="lg"
className="h-14 rounded-full bg-[hsl(var(--burgundy))] px-8 text-white hover:bg-[hsl(var(--burgundy))]/90 shadow-2xl shadow-[hsl(var(--burgundy))]/30 transition-all disabled:opacity-50 disabled:cursor-not-allowed font-bold text-base group whitespace-nowrap"
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Joining...
</>
) : (
<>
Reserve Your Spot
<ArrowRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
</>
)}
</Button>
</MagneticButton>
</div>
{/* Error Message - Visibility Improved */}

View File

@@ -1,65 +1,65 @@
import Link from 'next/link'
import Image from 'next/image'
import { Globe } from 'lucide-react'
export function Footer() {
return (
<footer className="border-t border-border bg-background py-12 text-sm">
<div className="mx-auto max-w-7xl px-6">
<div className="grid gap-12 md:grid-cols-4 lg:grid-cols-5">
<div className="md:col-span-2">
<div className="mb-6 flex items-center gap-2">
<div className="relative h-8 w-8">
<Image src="/logo.png" alt="Alertify Logo" fill className="object-contain" />
</div>
<span className="text-lg font-bold text-foreground">Alertify</span>
</div>
<p className="text-muted-foreground max-w-xs mb-6">
The modern platform for uptime monitoring, change detection, and performance tracking.
</p>
<div className="flex gap-4">
{/* Social icons placeholders */}
<div className="h-8 w-8 rounded-full bg-secondary hover:bg-border transition-colors cursor-pointer flex items-center justify-center text-muted-foreground hover:text-foreground">
<Globe className="h-4 w-4" />
</div>
</div>
</div>
<div>
<h4 className="mb-4 font-semibold text-foreground">Product</h4>
<ul className="space-y-3 text-muted-foreground">
<li><Link href="/#features" className="hover:text-primary transition-colors">Features</Link></li>
<li><Link href="/#use-cases" className="hover:text-primary transition-colors">Use Cases</Link></li>
</ul>
</div>
<div>
<h4 className="mb-4 font-semibold text-foreground">Company</h4>
<ul className="space-y-3 text-muted-foreground">
<li><Link href="/blog" className="hover:text-primary transition-colors">Blog</Link></li>
</ul>
</div>
<div>
<h4 className="mb-4 font-semibold text-foreground">Legal</h4>
<ul className="space-y-3 text-muted-foreground">
<li><Link href="/privacy" className="hover:text-primary transition-colors">Privacy</Link></li>
<li><Link href="/admin" className="hover:text-primary transition-colors opacity-50 text-xs">Admin</Link></li>
</ul>
</div>
</div>
<div className="mt-12 flex flex-col items-center justify-between gap-4 border-t border-border pt-8 text-sm text-muted-foreground sm:flex-row">
<p>© 2026 Alertify. All rights reserved.</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
System Operational
</div>
</div>
</div>
</footer>
)
}
import Link from 'next/link'
import Image from 'next/image'
import { Globe } from 'lucide-react'
export function Footer() {
return (
<footer className="border-t border-border bg-background py-12 text-sm">
<div className="mx-auto max-w-7xl px-6">
<div className="grid gap-12 md:grid-cols-4 lg:grid-cols-5">
<div className="md:col-span-2">
<div className="mb-6 flex items-center gap-2">
<div className="relative h-8 w-8">
<Image src="/logo.png" alt="SiteChangeMonitor Logo" fill className="object-contain" />
</div>
<span className="text-lg font-bold text-foreground">SiteChangeMonitor</span>
</div>
<p className="mb-6 max-w-xs text-muted-foreground">
The modern platform for uptime monitoring, change detection, and performance tracking.
</p>
<div className="flex gap-4">
{/* Social icons placeholders */}
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-secondary text-muted-foreground transition-colors hover:bg-border hover:text-foreground">
<Globe className="h-4 w-4" />
</div>
</div>
</div>
<div>
<h4 className="mb-4 font-semibold text-foreground">Product</h4>
<ul className="space-y-3 text-muted-foreground">
<li><Link href="/features" className="transition-colors hover:text-primary">Features</Link></li>
<li><Link href="/use-cases" className="transition-colors hover:text-primary">Use Cases</Link></li>
</ul>
</div>
<div>
<h4 className="mb-4 font-semibold text-foreground">Company</h4>
<ul className="space-y-3 text-muted-foreground">
<li><Link href="/blog" className="transition-colors hover:text-primary">Blog</Link></li>
</ul>
</div>
<div>
<h4 className="mb-4 font-semibold text-foreground">Legal</h4>
<ul className="space-y-3 text-muted-foreground">
<li><Link href="/privacy" className="transition-colors hover:text-primary">Privacy</Link></li>
<li><Link href="/admin" className="text-xs opacity-50 transition-colors hover:text-primary">Admin</Link></li>
</ul>
</div>
</div>
<div className="mt-12 flex flex-col items-center justify-between gap-4 border-t border-border pt-8 text-sm text-muted-foreground sm:flex-row">
<p>(c) 2026 SiteChangeMonitor. All rights reserved.</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
</span>
System Operational
</div>
</div>
</div>
</footer>
)
}

View File

@@ -1,70 +1,70 @@
'use client'
import { useEffect, useState } from 'react'
import { Moon, Sun } from 'lucide-react'
import { motion } from 'framer-motion'
export function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
// Check for stored preference or system preference
const stored = localStorage.getItem('theme')
if (stored === 'dark' || stored === 'light') {
setTheme(stored)
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
setTheme('dark')
}
}, [])
useEffect(() => {
if (!mounted) return
const root = document.documentElement
if (theme === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
localStorage.setItem('theme', theme)
}, [theme, mounted])
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
// Prevent hydration mismatch
if (!mounted) {
return (
<button className="p-2 rounded-lg bg-secondary/50 text-muted-foreground">
<Sun className="h-5 w-5" />
</button>
)
}
return (
<motion.button
onClick={toggleTheme}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="relative p-2 rounded-lg bg-secondary/50 hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
<motion.div
initial={false}
animate={{ rotate: theme === 'dark' ? 180 : 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
{theme === 'light' ? (
<Moon className="h-5 w-5" />
) : (
<Sun className="h-5 w-5" />
)}
</motion.div>
</motion.button>
)
}
export default ThemeToggle
'use client'
import { useEffect, useState } from 'react'
import { Moon, Sun } from 'lucide-react'
import { motion } from 'framer-motion'
export function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
// Check for stored preference or system preference
const stored = localStorage.getItem('theme')
if (stored === 'dark' || stored === 'light') {
setTheme(stored)
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
setTheme('dark')
}
}, [])
useEffect(() => {
if (!mounted) return
const root = document.documentElement
if (theme === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
localStorage.setItem('theme', theme)
}, [theme, mounted])
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
// Prevent hydration mismatch
if (!mounted) {
return (
<button className="p-2 rounded-lg bg-secondary/50 text-muted-foreground">
<Sun className="h-5 w-5" />
</button>
)
}
return (
<motion.button
onClick={toggleTheme}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="relative p-2 rounded-lg bg-secondary/50 hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
<motion.div
initial={false}
animate={{ rotate: theme === 'dark' ? 180 : 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
{theme === 'light' ? (
<Moon className="h-5 w-5" />
) : (
<Sun className="h-5 w-5" />
)}
</motion.div>
</motion.button>
)
}
export default ThemeToggle

View File

@@ -7,12 +7,13 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
hint?: string
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, label, error, hint, id, ...props }, ref) => {
const inputId = id || React.useId()
return (
<div className="w-full">
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, label, error, hint, id, ...props }, ref) => {
const generatedId = React.useId()
const inputId = id ?? generatedId
return (
<div className="w-full">
{label && (
<label
htmlFor={inputId}

View File

@@ -8,12 +8,13 @@ export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElemen
options: { value: string | number; label: string }[]
}
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, label, error, hint, id, options, ...props }, ref) => {
const selectId = id || React.useId()
return (
<div className="w-full">
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, label, error, hint, id, options, ...props }, ref) => {
const generatedId = React.useId()
const selectId = id ?? generatedId
return (
<div className="w-full">
{label && (
<label
htmlFor={selectId}