website-monitor/frontend/components/landing/LiveSerpPreview.tsx

183 lines
9.7 KiB
TypeScript

'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 */}
<div className="min-h-[260px]">
<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 ? (
// 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>
</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>
)}
</AnimatePresence>
</div>
</div>
</div>
</section>
)
}