179 lines
9.1 KiB
TypeScript
179 lines
9.1 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 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>
|
|
)
|
|
}
|