fix: Make Public TLD Pricing table 1:1 identical to Command Center

CHANGES:
1. Sparkline: Now uses exact same SVG polyline implementation
   - Upward trend: orange polyline
   - Downward trend: accent polyline
   - Neutral: horizontal line

2. Risk Badge: Now shows dot + text label (like Command Center)
   - 'Stable', 'Renewal trap', etc. visible on desktop
   - Width changed from 80px → 130px

3. Actions column: Width changed from 40px → 80px

4. Pagination: Simplified to match Command Center styling
   - Removed icons from buttons
   - Uses tabular-nums for page counter

Both tables now render identically with same:
- Column widths
- Sparkline SVGs
- Risk badges with text
- Button styling
This commit is contained in:
yves.gugger
2025-12-10 15:47:12 +01:00
parent 7ffaa8265c
commit eda676265d

View File

@ -8,8 +8,6 @@ import { useStore } from '@/lib/store'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { import {
TrendingUp, TrendingUp,
TrendingDown,
Minus,
ChevronRight, ChevronRight,
ChevronLeft, ChevronLeft,
Search, Search,
@ -55,31 +53,37 @@ interface PaginationData {
has_more: boolean has_more: boolean
} }
// Sparkline component // Sparkline component - matching Command Center exactly
function Sparkline({ trend }: { trend: number }) { function Sparkline({ trend }: { trend: number }) {
const isPositive = trend > 0 const isPositive = trend > 0
const isNegative = trend < 0 const isNeutral = trend === 0
// Generate simple sparkline points
const points = Array.from({ length: 8 }, (_, i) => {
const baseY = 50
const variance = isPositive ? -trend * 3 : isNegative ? -trend * 3 : 5
return `${i * 14},${baseY + (Math.random() * variance - variance / 2) * (i / 7)}`
}).join(' ')
return ( return (
<div className="w-16 h-8 flex items-center"> <div className="flex items-center gap-1">
<svg viewBox="0 0 100 100" className="w-full h-full" preserveAspectRatio="none"> <svg width="40" height="16" viewBox="0 0 40 16" className="overflow-visible">
<polyline {isNeutral ? (
points={points} <line x1="0" y1="8" x2="40" y2="8" stroke="currentColor" className="text-foreground-muted" strokeWidth="1.5" />
fill="none" ) : isPositive ? (
strokeWidth="3" <polyline
strokeLinecap="round" points="0,14 10,12 20,10 30,6 40,2"
strokeLinejoin="round" fill="none"
className={clsx( stroke="currentColor"
isPositive ? "stroke-orange-400" : isNegative ? "stroke-accent" : "stroke-foreground-subtle" className="text-orange-400"
)} strokeWidth="1.5"
/> strokeLinecap="round"
strokeLinejoin="round"
/>
) : (
<polyline
points="0,2 10,6 20,10 30,12 40,14"
fill="none"
stroke="currentColor"
className="text-accent"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
)}
</svg> </svg>
</div> </div>
) )
@ -90,7 +94,7 @@ export default function TldPricingPage() {
const [tlds, setTlds] = useState<TldData[]>([]) const [tlds, setTlds] = useState<TldData[]>([])
const [trending, setTrending] = useState<TrendingTld[]>([]) const [trending, setTrending] = useState<TrendingTld[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [pagination, setPagination] = useState<PaginationData>({ total: 0, limit: 25, offset: 0, has_more: false }) const [pagination, setPagination] = useState<PaginationData>({ total: 0, limit: 50, offset: 0, has_more: false })
// Search & Sort state // Search & Sort state
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
@ -150,6 +154,28 @@ export default function TldPricingPage() {
} }
} }
// Risk badge - matching Command Center exactly
const getRiskBadge = (tld: TldData) => {
const level = tld.risk_level || 'low'
const reason = tld.risk_reason || 'Stable'
return (
<span className={clsx(
"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium",
level === 'high' && "bg-red-500/10 text-red-400",
level === 'medium' && "bg-amber-500/10 text-amber-400",
level === 'low' && "bg-accent/10 text-accent"
)}>
<span className={clsx(
"w-2.5 h-2.5 rounded-full",
level === 'high' && "bg-red-400",
level === 'medium' && "bg-amber-400",
level === 'low' && "bg-accent"
)} />
<span className="hidden sm:inline ml-1">{reason}</span>
</span>
)
}
// Get renewal trap indicator // Get renewal trap indicator
const getRenewalTrap = (tld: TldData) => { const getRenewalTrap = (tld: TldData) => {
if (!tld.min_renewal_price || !tld.min_registration_price) return null if (!tld.min_renewal_price || !tld.min_registration_price) return null
@ -348,7 +374,7 @@ export default function TldPricingPage() {
</div> </div>
</div> </div>
{/* TLD Table using PremiumTable */} {/* TLD Table using PremiumTable - matching Command Center exactly */}
<PremiumTable <PremiumTable
data={tlds} data={tlds}
keyExtractor={(tld) => tld.tld} keyExtractor={(tld) => tld.tld}
@ -387,7 +413,7 @@ export default function TldPricingPage() {
render: (tld, idx) => { render: (tld, idx) => {
const showData = isAuthenticated || (page === 0 && idx === 0) const showData = isAuthenticated || (page === 0 && idx === 0)
if (!showData) { if (!showData) {
return <div className="w-16 h-8 bg-foreground/5 rounded blur-[3px]" /> return <div className="w-10 h-4 bg-foreground/5 rounded blur-[3px]" />
} }
return <Sparkline trend={tld.price_change_1y || 0} /> return <Sparkline trend={tld.price_change_1y || 0} />
}, },
@ -473,30 +499,25 @@ export default function TldPricingPage() {
key: 'risk', key: 'risk',
header: 'Risk', header: 'Risk',
align: 'center', align: 'center',
width: '80px', width: '130px',
hideOnMobile: true,
render: (tld, idx) => { render: (tld, idx) => {
const showData = isAuthenticated || (page === 0 && idx === 0) const showData = isAuthenticated || (page === 0 && idx === 0)
if (!showData) { if (!showData) {
return <span className="w-3 h-3 rounded-full bg-foreground-subtle blur-[3px]" /> return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-foreground/5 blur-[3px]">
<span className="w-2.5 h-2.5 rounded-full bg-foreground-subtle" />
<span className="hidden sm:inline ml-1">Hidden</span>
</span>
)
} }
return ( return getRiskBadge(tld)
<div className="flex justify-center" title={tld.risk_reason}>
<span className={clsx(
"w-3 h-3 rounded-full",
tld.risk_level === 'high' && "bg-red-400",
tld.risk_level === 'medium' && "bg-amber-400",
tld.risk_level === 'low' && "bg-accent"
)} />
</div>
)
}, },
}, },
{ {
key: 'action', key: 'actions',
header: '', header: '',
align: 'right', align: 'right',
width: '40px', width: '80px',
render: () => ( render: () => (
<ChevronRight className="w-5 h-5 text-foreground-subtle group-hover:text-accent transition-colors" /> <ChevronRight className="w-5 h-5 text-foreground-subtle group-hover:text-accent transition-colors" />
), ),
@ -506,29 +527,27 @@ export default function TldPricingPage() {
{/* Pagination */} {/* Pagination */}
{!loading && pagination.total > pagination.limit && ( {!loading && pagination.total > pagination.limit && (
<div className="flex items-center justify-center gap-4 pt-6"> <div className="flex items-center justify-center gap-4 pt-2">
<button <button
onClick={() => setPage(Math.max(0, page - 1))} onClick={() => setPage(Math.max(0, page - 1))}
disabled={page === 0} disabled={page === 0}
className="flex items-center gap-1 px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground className="px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground
bg-foreground/5 hover:bg-foreground/10 rounded-lg bg-foreground/5 hover:bg-foreground/10 rounded-lg
disabled:opacity-50 disabled:cursor-not-allowed transition-colors" disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
<ChevronLeft className="w-4 h-4" />
Previous Previous
</button> </button>
<span className="text-sm text-foreground-muted"> <span className="text-sm text-foreground-muted tabular-nums">
Page {currentPage} of {totalPages} Page {currentPage} of {totalPages}
</span> </span>
<button <button
onClick={() => setPage(page + 1)} onClick={() => setPage(page + 1)}
disabled={!pagination.has_more} disabled={!pagination.has_more}
className="flex items-center gap-1 px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground className="px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground
bg-foreground/5 hover:bg-foreground/10 rounded-lg bg-foreground/5 hover:bg-foreground/10 rounded-lg
disabled:opacity-50 disabled:cursor-not-allowed transition-colors" disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
Next Next
<ChevronRight className="w-4 h-4" />
</button> </button>
</div> </div>
)} )}