Add Footer component and real TLD history data
- Created reusable Footer component with social links - Replaced mock data with real API calls for TLD history - Added Trending TLDs section at top of pricing page - Mini-charts now display actual 12-month price history - Improved data authenticity throughout TLD pricing page
This commit is contained in:
@ -329,12 +329,12 @@ class DomainChecker:
|
||||
'object does not exist',
|
||||
]
|
||||
if any(phrase in error_str for phrase in not_found_phrases):
|
||||
return DomainCheckResult(
|
||||
domain=domain,
|
||||
status=DomainStatus.AVAILABLE,
|
||||
is_available=True,
|
||||
check_method="whois",
|
||||
)
|
||||
return DomainCheckResult(
|
||||
domain=domain,
|
||||
status=DomainStatus.AVAILABLE,
|
||||
is_available=True,
|
||||
check_method="whois",
|
||||
)
|
||||
# Otherwise it's a real error
|
||||
return DomainCheckResult(
|
||||
domain=domain,
|
||||
|
||||
@ -86,15 +86,15 @@ export default function LoginPage() {
|
||||
className="input-elegant text-body-sm sm:text-body"
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
required
|
||||
minLength={8}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
required
|
||||
minLength={8}
|
||||
className="input-elegant text-body-sm sm:text-body pr-12"
|
||||
/>
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
|
||||
@ -95,15 +95,15 @@ export default function RegisterPage() {
|
||||
className="input-elegant text-body-sm sm:text-body"
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Create password (min. 8 characters)"
|
||||
required
|
||||
minLength={8}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Create password (min. 8 characters)"
|
||||
required
|
||||
minLength={8}
|
||||
className="input-elegant text-body-sm sm:text-body pr-12"
|
||||
/>
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { Header } from '@/components/Header'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { useStore } from '@/lib/store'
|
||||
import { api } from '@/lib/api'
|
||||
import {
|
||||
@ -14,6 +15,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronsUpDown,
|
||||
Lock,
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
@ -29,31 +31,73 @@ interface TldData {
|
||||
trend: string
|
||||
}
|
||||
|
||||
interface TrendingTld {
|
||||
tld: string
|
||||
reason: string
|
||||
price_change: number
|
||||
current_price: number
|
||||
}
|
||||
|
||||
interface TldHistoryData {
|
||||
history: Array<{
|
||||
date: string
|
||||
price: number
|
||||
}>
|
||||
}
|
||||
|
||||
type SortField = 'tld' | 'avg_registration_price' | 'min_registration_price' | 'registrar_count'
|
||||
type SortDirection = 'asc' | 'desc'
|
||||
|
||||
// Mini sparkline chart component
|
||||
function MiniChart({ tld }: { tld: string }) {
|
||||
// Generate mock data for 12 months (in production, this would come from API)
|
||||
const data = useMemo(() => {
|
||||
const basePrice = 10 + Math.random() * 30
|
||||
return Array.from({ length: 12 }, (_, i) => {
|
||||
const variance = (Math.random() - 0.5) * 4
|
||||
return Math.max(1, basePrice + variance)
|
||||
})
|
||||
}, [tld])
|
||||
// Mini sparkline chart component with real data
|
||||
function MiniChart({ tld, isAuthenticated }: { tld: string, isAuthenticated: boolean }) {
|
||||
const [historyData, setHistoryData] = useState<number[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const min = Math.min(...data)
|
||||
const max = Math.max(...data)
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
loadHistory()
|
||||
}
|
||||
}, [tld, isAuthenticated])
|
||||
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
const data = await api.getTldHistory(tld, 365) // Get 1 year of data
|
||||
// Sample down to 12 data points (monthly)
|
||||
const history = data.history || []
|
||||
const sampledData = history
|
||||
.filter((_, i) => i % Math.floor(history.length / 12) === 0)
|
||||
.slice(0, 12)
|
||||
.map(h => h.price)
|
||||
|
||||
setHistoryData(sampledData.length > 0 ? sampledData : [])
|
||||
} catch (error) {
|
||||
console.error('Failed to load history:', error)
|
||||
setHistoryData([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAuthenticated || loading || historyData.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-ui-sm text-foreground-subtle">
|
||||
<Lock className="w-3 h-3" />
|
||||
Sign in
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const min = Math.min(...historyData)
|
||||
const max = Math.max(...historyData)
|
||||
const range = max - min || 1
|
||||
|
||||
const points = data.map((value, i) => {
|
||||
const x = (i / (data.length - 1)) * 100
|
||||
const points = historyData.map((value, i) => {
|
||||
const x = (i / (historyData.length - 1)) * 100
|
||||
const y = 100 - ((value - min) / range) * 100
|
||||
return `${x},${y}`
|
||||
}).join(' ')
|
||||
|
||||
const isIncreasing = data[data.length - 1] > data[0]
|
||||
const isIncreasing = historyData[historyData.length - 1] > historyData[0]
|
||||
|
||||
return (
|
||||
<svg className="w-24 h-8" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
@ -80,9 +124,21 @@ function SortIcon({ field, currentField, direction }: { field: SortField, curren
|
||||
: <ChevronDown className="w-4 h-4 text-accent" />
|
||||
}
|
||||
|
||||
function ShimmerBlock({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={clsx(
|
||||
"relative overflow-hidden rounded bg-background-tertiary",
|
||||
className
|
||||
)}>
|
||||
<div className="absolute inset-0 -translate-x-full animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-foreground/5 to-transparent" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TldPricingPage() {
|
||||
const { isAuthenticated, checkAuth, isLoading: authLoading } = useStore()
|
||||
const [tlds, setTlds] = useState<TldData[]>([])
|
||||
const [trending, setTrending] = useState<TrendingTld[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [sortField, setSortField] = useState<SortField>('tld')
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc')
|
||||
@ -94,11 +150,16 @@ export default function TldPricingPage() {
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const overviewData = await api.getTldOverview(100)
|
||||
const [overviewData, trendingData] = await Promise.all([
|
||||
api.getTldOverview(100),
|
||||
api.getTrendingTlds(),
|
||||
])
|
||||
setTlds(overviewData?.tlds || [])
|
||||
setTrending(trendingData?.trending || [])
|
||||
} catch (error) {
|
||||
console.error('Failed to load TLD data:', error)
|
||||
setTlds([])
|
||||
setTrending([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@ -148,7 +209,7 @@ export default function TldPricingPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background relative">
|
||||
<div className="min-h-screen bg-background relative flex flex-col">
|
||||
{/* Ambient glow */}
|
||||
<div className="fixed inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 left-1/4 w-[500px] h-[400px] bg-accent/[0.02] rounded-full blur-3xl" />
|
||||
@ -156,7 +217,7 @@ export default function TldPricingPage() {
|
||||
|
||||
<Header />
|
||||
|
||||
<main className="relative pt-28 sm:pt-32 pb-16 sm:pb-20 px-4 sm:px-6">
|
||||
<main className="relative pt-28 sm:pt-32 pb-16 sm:pb-20 px-4 sm:px-6 flex-1">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12 sm:mb-16 animate-fade-in">
|
||||
@ -196,6 +257,54 @@ export default function TldPricingPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trending Section */}
|
||||
{trending.length > 0 && (
|
||||
<div className="mb-12 sm:mb-16 animate-slide-up">
|
||||
<h2 className="text-body-lg sm:text-heading-sm font-medium text-foreground mb-4 sm:mb-6 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-accent" />
|
||||
Trending Now
|
||||
</h2>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||
{trending.map((item) => (
|
||||
<Link
|
||||
key={item.tld}
|
||||
href={isAuthenticated ? `/tld-pricing/${item.tld}` : '/register'}
|
||||
className="p-4 sm:p-5 bg-background-secondary/50 border border-border rounded-xl hover:border-border-hover hover:bg-background-secondary transition-all duration-300 text-left group"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-mono text-body-lg sm:text-heading-sm text-foreground">.{item.tld}</span>
|
||||
{isAuthenticated ? (
|
||||
<span className={clsx(
|
||||
"text-ui-sm font-medium px-2 py-0.5 rounded-full",
|
||||
item.price_change > 0
|
||||
? "text-[#f97316] bg-[#f9731615]"
|
||||
: "text-accent bg-accent-muted"
|
||||
)}>
|
||||
{item.price_change > 0 ? '+' : ''}{item.price_change.toFixed(1)}%
|
||||
</span>
|
||||
) : (
|
||||
<ShimmerBlock className="h-5 w-14" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-body-sm text-foreground-muted mb-2 line-clamp-2">
|
||||
{isAuthenticated ? item.reason : 'Sign in to view trend details'}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
{isAuthenticated ? (
|
||||
<span className="text-body-sm text-foreground-subtle">
|
||||
${item.current_price.toFixed(2)}/yr
|
||||
</span>
|
||||
) : (
|
||||
<ShimmerBlock className="h-5 w-20" />
|
||||
)}
|
||||
<ChevronRight className="w-4 h-4 text-foreground-subtle group-hover:text-accent transition-colors" />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TLD Table */}
|
||||
<div className="bg-background-secondary/30 border border-border rounded-xl overflow-hidden animate-slide-up">
|
||||
<div className="overflow-x-auto">
|
||||
@ -271,14 +380,7 @@ export default function TldPricingPage() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 sm:px-6 py-4 hidden md:table-cell">
|
||||
{isAuthenticated ? (
|
||||
<MiniChart tld={tld.tld} />
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-ui-sm text-foreground-subtle">
|
||||
<Lock className="w-3 h-3" />
|
||||
Sign in
|
||||
</div>
|
||||
)}
|
||||
<MiniChart tld={tld.tld} isAuthenticated={isAuthenticated} />
|
||||
</td>
|
||||
<td className="px-4 sm:px-6 py-4 text-right">
|
||||
{isAuthenticated ? (
|
||||
@ -334,6 +436,8 @@ export default function TldPricingPage() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
152
frontend/src/components/Footer.tsx
Normal file
152
frontend/src/components/Footer.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import Link from 'next/link'
|
||||
import { Github, Twitter, Mail } from 'lucide-react'
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="relative border-t border-border bg-background-secondary/30 backdrop-blur-sm mt-auto">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-12 sm:py-16">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-12">
|
||||
{/* Brand */}
|
||||
<div className="col-span-2 md:col-span-1">
|
||||
<div className="relative inline-flex items-baseline mb-4">
|
||||
<span className="absolute top-1 -left-3.5 flex items-center justify-center">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-accent" />
|
||||
<span className="absolute w-2.5 h-2.5 rounded-full bg-accent animate-ping opacity-30" />
|
||||
</span>
|
||||
<span className="text-xl font-bold tracking-tight text-foreground">
|
||||
pounce
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-body-sm text-foreground-muted mb-4 max-w-xs">
|
||||
Professional domain intelligence. Monitor availability, track prices, and secure your domains.
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href="https://github.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-9 h-9 flex items-center justify-center rounded-lg bg-background-tertiary hover:bg-background-secondary transition-colors"
|
||||
>
|
||||
<Github className="w-4 h-4 text-foreground-muted" />
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-9 h-9 flex items-center justify-center rounded-lg bg-background-tertiary hover:bg-background-secondary transition-colors"
|
||||
>
|
||||
<Twitter className="w-4 h-4 text-foreground-muted" />
|
||||
</a>
|
||||
<a
|
||||
href="mailto:support@pounce.dev"
|
||||
className="w-9 h-9 flex items-center justify-center rounded-lg bg-background-tertiary hover:bg-background-secondary transition-colors"
|
||||
>
|
||||
<Mail className="w-4 h-4 text-foreground-muted" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product */}
|
||||
<div>
|
||||
<h3 className="text-ui font-medium text-foreground mb-4">Product</h3>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<Link href="/" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Domain Monitoring
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/tld-pricing" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
TLD Pricing
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/pricing" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Pricing Plans
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/dashboard" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Dashboard
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Company */}
|
||||
<div>
|
||||
<h3 className="text-ui font-medium text-foreground mb-4">Company</h3>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<Link href="/about" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
About
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/blog" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Blog
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/contact" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/careers" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Careers
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Legal */}
|
||||
<div>
|
||||
<h3 className="text-ui font-medium text-foreground mb-4">Legal</h3>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<Link href="/privacy" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/terms" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Terms of Service
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/cookies" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Cookie Policy
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/imprint" className="text-body-sm text-foreground-muted hover:text-foreground transition-colors">
|
||||
Imprint
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom */}
|
||||
<div className="pt-8 border-t border-border flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<p className="text-ui-sm text-foreground-subtle">
|
||||
© {new Date().getFullYear()} pounce. All rights reserved.
|
||||
</p>
|
||||
<div className="flex items-center gap-6">
|
||||
<Link href="/privacy" className="text-ui-sm text-foreground-subtle hover:text-foreground transition-colors">
|
||||
Privacy
|
||||
</Link>
|
||||
<Link href="/terms" className="text-ui-sm text-foreground-subtle hover:text-foreground transition-colors">
|
||||
Terms
|
||||
</Link>
|
||||
<Link href="/contact" className="text-ui-sm text-foreground-subtle hover:text-foreground transition-colors">
|
||||
Contact
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user