yves.gugger b66c3b360d feat: Complete redesign of user and admin backend with consistent styling
USER BACKEND:
- Created PremiumTable component with elegant gradient styling
- All pages now use consistent max-w-7xl width via PageContainer
- Auctions page integrated into CommandCenterLayout with full functionality
- Intelligence page updated with PremiumTable and StatCards
- Added keyboard shortcuts system (press ? to show help):
  - G: Dashboard, W: Watchlist, P: Portfolio, A: Auctions
  - I: Intelligence, S: Settings, N: Add domain, Cmd+K: Search

ADMIN BACKEND:
- Created separate AdminLayout with dedicated sidebar (red theme)
- Admin sidebar with navigation tabs and shortcut hints
- Integrated keyboard shortcuts for admin:
  - O: Overview, U: Users, B: Blog, Y: System, D: Back to dashboard
- All tables use consistent PremiumTable component
- Professional stat cards and status badges

COMPONENTS:
- PremiumTable: Elegant table with sorting, selection, loading states
- Badge: Versatile status badge with variants and dot indicator
- StatCard: Consistent stat display with optional accent styling
- PageContainer: Enforces max-w-7xl for consistent page width
- TableActionButton: Consistent action buttons for tables
- PlatformBadge: Color-coded platform indicators for auctions
2025-12-10 10:23:40 +01:00

250 lines
9.1 KiB
TypeScript
Executable File

'use client'
import { useEffect, useState } from 'react'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { PremiumTable, Badge, StatCard, PageContainer } from '@/components/PremiumTable'
import {
Search,
TrendingUp,
TrendingDown,
Minus,
ChevronRight,
Globe,
ArrowUpDown,
DollarSign,
BarChart3,
RefreshCw,
Bell,
} from 'lucide-react'
import clsx from 'clsx'
import Link from 'next/link'
interface TLDData {
tld: string
min_price: number
avg_price: number
max_price: number
cheapest_registrar: string
cheapest_registrar_url?: string
price_change_7d?: number
popularity_rank?: number
}
export default function IntelligencePage() {
const { subscription } = useStore()
const [tldData, setTldData] = useState<TLDData[]>([])
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [sortBy, setSortBy] = useState<'popularity' | 'price_asc' | 'price_desc' | 'change'>('popularity')
const [page, setPage] = useState(0)
const [total, setTotal] = useState(0)
useEffect(() => {
loadTLDData()
}, [page, sortBy])
const loadTLDData = async () => {
setLoading(true)
try {
const response = await api.getTldPrices({
limit: 50,
offset: page * 50,
sort_by: sortBy,
})
setTldData(response.tlds || [])
setTotal(response.total || 0)
} catch (error) {
console.error('Failed to load TLD data:', error)
} finally {
setLoading(false)
}
}
const handleRefresh = async () => {
setRefreshing(true)
await loadTLDData()
setRefreshing(false)
}
// Filter by search
const filteredData = tldData.filter(tld =>
tld.tld.toLowerCase().includes(searchQuery.toLowerCase())
)
const getTrendIcon = (change: number | undefined) => {
if (!change) return <Minus className="w-4 h-4 text-foreground-muted" />
if (change > 0) return <TrendingUp className="w-4 h-4 text-orange-400" />
return <TrendingDown className="w-4 h-4 text-accent" />
}
// Calculate stats
const lowestPrice = tldData.reduce((min, tld) => Math.min(min, tld.min_price), Infinity)
const hottestTld = tldData.find(tld => (tld.price_change_7d || 0) > 0)?.tld || 'N/A'
return (
<CommandCenterLayout
title="TLD Intelligence"
subtitle={`Real-time pricing data for ${total}+ TLDs`}
actions={
<button
onClick={handleRefresh}
disabled={refreshing}
className="flex items-center gap-2 px-4 py-2 text-sm text-foreground-muted hover:text-foreground
hover:bg-foreground/5 rounded-lg transition-all disabled:opacity-50"
>
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
<span className="hidden sm:inline">Refresh</span>
</button>
}
>
<PageContainer>
{/* Stats Overview */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard title="TLDs Tracked" value={total} subtitle="updated daily" icon={Globe} accent />
<StatCard title="Lowest Price" value={`$${lowestPrice === Infinity ? '0.99' : lowestPrice.toFixed(2)}`} icon={DollarSign} />
<StatCard title="Hottest TLD" value={`.${hottestTld}`} subtitle="rising prices" icon={TrendingUp} />
<StatCard title="Update Freq" value="24h" subtitle="automatic" icon={BarChart3} />
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-muted" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search TLDs..."
className="w-full h-11 pl-11 pr-4 bg-background-secondary/50 border border-border/50 rounded-xl
text-sm text-foreground placeholder:text-foreground-subtle
focus:outline-none focus:border-accent/50 transition-all"
/>
</div>
<div className="relative">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="h-11 pl-4 pr-10 bg-background-secondary/50 border border-border/50 rounded-xl
text-sm text-foreground appearance-none cursor-pointer
focus:outline-none focus:border-accent/50"
>
<option value="popularity">By Popularity</option>
<option value="price_asc">Price: Low to High</option>
<option value="price_desc">Price: High to Low</option>
<option value="change">By Change %</option>
</select>
<ArrowUpDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-muted pointer-events-none" />
</div>
</div>
{/* TLD Table */}
<PremiumTable
data={filteredData}
keyExtractor={(tld) => tld.tld}
loading={loading}
onRowClick={(tld) => window.location.href = `/tld-pricing/${tld.tld}`}
emptyIcon={<Globe className="w-12 h-12 text-foreground-subtle" />}
emptyTitle="No TLDs found"
emptyDescription={searchQuery ? `No TLDs matching "${searchQuery}"` : "Check back later for TLD data"}
columns={[
{
key: 'tld',
header: 'TLD',
render: (tld) => (
<span className="font-mono text-xl font-semibold text-foreground group-hover:text-accent transition-colors">
.{tld.tld}
</span>
),
},
{
key: 'min_price',
header: 'Min Price',
render: (tld) => (
<span className="font-medium text-foreground">${tld.min_price.toFixed(2)}</span>
),
},
{
key: 'avg_price',
header: 'Avg Price',
hideOnMobile: true,
render: (tld) => (
<span className="text-foreground-muted">${tld.avg_price.toFixed(2)}</span>
),
},
{
key: 'change',
header: '7d Change',
render: (tld) => (
<div className="flex items-center gap-2">
{getTrendIcon(tld.price_change_7d)}
<span className={clsx(
"font-medium",
(tld.price_change_7d || 0) > 0 ? "text-orange-400" :
(tld.price_change_7d || 0) < 0 ? "text-accent" : "text-foreground-muted"
)}>
{(tld.price_change_7d || 0) > 0 ? '+' : ''}{(tld.price_change_7d || 0).toFixed(1)}%
</span>
</div>
),
},
{
key: 'registrar',
header: 'Cheapest At',
hideOnMobile: true,
render: (tld) => (
<span className="text-foreground-muted truncate max-w-[150px] block">{tld.cheapest_registrar}</span>
),
},
{
key: 'actions',
header: '',
align: 'right',
render: (tld) => (
<div className="flex items-center gap-2 justify-end">
<Link
href={`/tld-pricing/${tld.tld}`}
className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
onClick={(e) => e.stopPropagation()}
>
<Bell className="w-4 h-4" />
</Link>
<ChevronRight className="w-5 h-5 text-foreground-subtle group-hover:text-accent transition-colors" />
</div>
),
},
]}
/>
{/* Pagination */}
{total > 50 && (
<div className="flex items-center justify-center gap-4">
<button
onClick={() => setPage(Math.max(0, page - 1))}
disabled={page === 0}
className="px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Previous
</button>
<span className="text-sm text-foreground-muted">
Page {page + 1} of {Math.ceil(total / 50)}
</span>
<button
onClick={() => setPage(page + 1)}
disabled={(page + 1) * 50 >= total}
className="px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Next
</button>
</div>
)}
</PageContainer>
</CommandCenterLayout>
)
}