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
This commit is contained in:
yves.gugger
2025-12-10 10:23:40 +01:00
parent 7549159204
commit b66c3b360d
8 changed files with 1838 additions and 1272 deletions

View File

@ -1,14 +1,11 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { DataTable, StatusBadge, TableAction } from '@/components/DataTable'
import { useStore } from '@/lib/store'
import { AdminLayout } from '@/components/AdminLayout'
import { PremiumTable, Badge, TableActionButton, StatCard, PageContainer } from '@/components/PremiumTable'
import { api } from '@/lib/api'
import {
Users,
Database,
Mail,
TrendingUp,
Shield,
@ -29,14 +26,13 @@ import {
XCircle,
Download,
Send,
Clock,
History,
X,
BookOpen,
Plus,
Edit2,
ExternalLink,
Sparkles,
Database,
} from 'lucide-react'
import clsx from 'clsx'
@ -66,9 +62,6 @@ interface AdminUser {
}
export default function AdminPage() {
const router = useRouter()
const { user, isAuthenticated, isLoading } = useStore()
const [activeTab, setActiveTab] = useState<TabType>('overview')
const [stats, setStats] = useState<AdminStats | null>(null)
const [users, setUsers] = useState<AdminUser[]>([])
@ -95,16 +88,8 @@ export default function AdminPage() {
const [bulkTier, setBulkTier] = useState('trader')
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/login')
}
}, [isLoading, isAuthenticated, router])
useEffect(() => {
if (isAuthenticated) {
loadAdminData()
}
}, [isAuthenticated, activeTab])
loadAdminData()
}, [activeTab])
const loadAdminData = async () => {
setLoading(true)
@ -127,33 +112,23 @@ export default function AdminPage() {
setNewsletter(nlData.subscribers)
setNewsletterTotal(nlData.total)
} else if (activeTab === 'system') {
try {
const [healthData, schedulerData] = await Promise.all([
api.getSystemHealth(),
api.getSchedulerStatus(),
])
setSystemHealth(healthData)
setSchedulerStatus(schedulerData)
} catch { /* ignore */ }
const [healthData, schedulerData] = await Promise.all([
api.getSystemHealth().catch(() => null),
api.getSchedulerStatus().catch(() => null),
])
setSystemHealth(healthData)
setSchedulerStatus(schedulerData)
} else if (activeTab === 'activity') {
try {
const logData = await api.getActivityLog(50, 0)
setActivityLog(logData.logs)
setActivityLogTotal(logData.total)
} catch { /* ignore */ }
const logData = await api.getActivityLog(50, 0).catch(() => ({ logs: [], total: 0 }))
setActivityLog(logData.logs)
setActivityLogTotal(logData.total)
} else if (activeTab === 'blog') {
try {
const blogData = await api.getAdminBlogPosts(50, 0)
setBlogPosts(blogData.posts)
setBlogPostsTotal(blogData.total)
} catch { /* ignore */ }
const blogData = await api.getAdminBlogPosts(50, 0).catch(() => ({ posts: [], total: 0 }))
setBlogPosts(blogData.posts)
setBlogPostsTotal(blogData.total)
}
} catch (err) {
if (err instanceof Error && err.message.includes('403')) {
setError('Admin privileges required.')
} else {
setError(err instanceof Error ? err.message : 'Failed to load admin data')
}
setError(err instanceof Error ? err.message : 'Failed to load admin data')
} finally {
setLoading(false)
}
@ -161,10 +136,9 @@ export default function AdminPage() {
const handleTriggerScrape = async () => {
setScraping(true)
setError(null)
try {
const result = await api.triggerTldScrape()
setSuccess(`Scrape completed: ${result.tlds_scraped} TLDs, ${result.prices_saved} prices saved`)
setSuccess(`Scrape completed: ${result.tlds_scraped} TLDs`)
} catch (err) {
setError(err instanceof Error ? err.message : 'Scrape failed')
} finally {
@ -174,10 +148,9 @@ export default function AdminPage() {
const handleTriggerAuctionScrape = async () => {
setAuctionScraping(true)
setError(null)
try {
const result = await api.triggerAuctionScrape()
setSuccess(`Auction scrape completed: ${result.total_auctions || 0} auctions found`)
setSuccess(`Auction scrape completed: ${result.total_auctions || 0} auctions`)
loadAdminData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Auction scrape failed')
@ -188,10 +161,9 @@ export default function AdminPage() {
const handleTriggerDomainChecks = async () => {
setDomainChecking(true)
setError(null)
try {
const result = await api.triggerDomainChecks()
setSuccess(`Domain checks started: ${result.domains_queued} domains queued`)
setSuccess(`Domain checks started: ${result.domains_queued} queued`)
} catch (err) {
setError(err instanceof Error ? err.message : 'Domain check failed')
} finally {
@ -201,7 +173,6 @@ export default function AdminPage() {
const handleSendTestEmail = async () => {
setSendingEmail(true)
setError(null)
try {
const result = await api.sendTestEmail()
setSuccess(`Test email sent to ${result.sent_to}`)
@ -225,39 +196,24 @@ export default function AdminPage() {
const handleToggleAdmin = async (userId: number, isAdmin: boolean) => {
try {
await api.updateAdminUser(userId, { is_admin: !isAdmin })
setSuccess(isAdmin ? 'Admin privileges removed' : 'Admin privileges granted')
setSuccess(isAdmin ? 'Admin removed' : 'Admin granted')
loadAdminData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Update failed')
}
}
const handleDeleteUser = async (userId: number, userEmail: string) => {
if (!confirm(`Delete user "${userEmail}" and ALL their data?\n\nThis cannot be undone.`)) return
const handleDeleteUser = async (userId: number, email: string) => {
if (!confirm(`Delete user "${email}"? This cannot be undone.`)) return
try {
await api.deleteAdminUser(userId)
setSuccess(`User ${userEmail} deleted`)
setSuccess(`User ${email} deleted`)
loadAdminData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Delete failed')
}
}
const handleBulkUpgrade = async () => {
if (selectedUsers.length === 0) {
setError('Please select users to upgrade')
return
}
try {
const result = await api.bulkUpgradeUsers(selectedUsers, bulkTier)
setSuccess(`Upgraded ${result.total_upgraded} users to ${bulkTier}`)
setSelectedUsers([])
loadAdminData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Bulk upgrade failed')
}
}
const handleExportUsers = async () => {
try {
const result = await api.exportUsersCSV()
@ -265,7 +221,7 @@ export default function AdminPage() {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `users-export-${new Date().toISOString().split('T')[0]}.csv`
a.download = `users-${new Date().toISOString().split('T')[0]}.csv`
a.click()
setSuccess(`Exported ${result.count} users`)
} catch (err) {
@ -273,491 +229,334 @@ export default function AdminPage() {
}
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
)
}
if (error && error.includes('403')) {
return (
<CommandCenterLayout title="Access Denied">
<div className="flex flex-col items-center justify-center py-20">
<Shield className="w-16 h-16 text-red-400 mb-6" />
<h2 className="text-2xl font-display text-foreground mb-4">Admin Access Required</h2>
<p className="text-foreground-muted mb-6">You don't have admin privileges.</p>
<button
onClick={() => router.push('/dashboard')}
className="px-6 py-3 bg-foreground text-background rounded-xl font-medium"
>
Go to Dashboard
</button>
</div>
</CommandCenterLayout>
)
}
const tabs = [
{ id: 'overview' as const, label: 'Overview', icon: Activity },
{ id: 'users' as const, label: 'Users', icon: Users },
{ id: 'alerts' as const, label: 'Alerts', icon: Bell },
{ id: 'newsletter' as const, label: 'Newsletter', icon: Mail },
{ id: 'tld' as const, label: 'TLD Data', icon: Globe },
{ id: 'auctions' as const, label: 'Auctions', icon: Gavel },
{ id: 'blog' as const, label: 'Blog', icon: BookOpen },
{ id: 'system' as const, label: 'System', icon: Database },
{ id: 'activity' as const, label: 'Activity', icon: History },
]
return (
<CommandCenterLayout
title="Admin Panel"
subtitle="Mission Control for pounce"
<AdminLayout
title={activeTab === 'overview' ? 'Overview' :
activeTab === 'users' ? 'User Management' :
activeTab === 'alerts' ? 'Price Alerts' :
activeTab === 'newsletter' ? 'Newsletter' :
activeTab === 'tld' ? 'TLD Data' :
activeTab === 'auctions' ? 'Auctions' :
activeTab === 'blog' ? 'Blog Management' :
activeTab === 'system' ? 'System Status' :
'Activity Log'}
subtitle="Admin Control Panel"
activeTab={activeTab}
onTabChange={(tab) => setActiveTab(tab as TabType)}
>
{/* Messages */}
{error && !error.includes('403') && (
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-400 shrink-0" />
<p className="text-sm text-red-400 flex-1">{error}</p>
<button onClick={() => setError(null)} className="text-red-400 hover:text-red-300">
<X className="w-4 h-4" />
</button>
</div>
)}
<PageContainer>
{/* Messages */}
{error && (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-400 shrink-0" />
<p className="text-sm text-red-400 flex-1">{error}</p>
<button onClick={() => setError(null)} className="text-red-400"><X className="w-4 h-4" /></button>
</div>
)}
{success && (
<div className="mb-6 p-4 bg-accent/10 border border-accent/20 rounded-xl flex items-center gap-3">
<Check className="w-5 h-5 text-accent shrink-0" />
<p className="text-sm text-accent flex-1">{success}</p>
<button onClick={() => setSuccess(null)} className="text-accent hover:text-accent/80">
<X className="w-4 h-4" />
</button>
</div>
)}
{success && (
<div className="p-4 bg-accent/10 border border-accent/20 rounded-xl flex items-center gap-3">
<Check className="w-5 h-5 text-accent shrink-0" />
<p className="text-sm text-accent flex-1">{success}</p>
<button onClick={() => setSuccess(null)} className="text-accent"><X className="w-4 h-4" /></button>
</div>
)}
{/* Tabs */}
<div className="flex flex-wrap items-center gap-2 p-1.5 bg-background-secondary/30 backdrop-blur-sm border border-border/30 rounded-2xl w-fit mb-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={clsx(
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-xl whitespace-nowrap transition-all duration-300",
activeTab === tab.id
? "bg-gradient-to-r from-accent to-accent/80 text-background shadow-lg shadow-accent/20"
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
)}
>
<tab.icon className="w-4 h-4" />
<span className="hidden sm:inline">{tab.label}</span>
</button>
))}
</div>
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : (
<>
{/* Overview Tab */}
{activeTab === 'overview' && stats && (
<div className="space-y-6 animate-fade-in">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard title="Total Users" value={stats.users.total} subtitle={`${stats.users.new_this_week} new this week`} icon={Users} />
<StatCard title="Watched Domains" value={stats.domains.watched} subtitle={`${stats.domains.portfolio} in portfolios`} icon={Eye} />
<StatCard title="TLDs Tracked" value={stats.tld_data.unique_tlds} subtitle={`${stats.tld_data.price_records.toLocaleString()} price records`} icon={Globe} />
<StatCard title="Newsletter" value={stats.newsletter_subscribers} subtitle="active subscribers" icon={Mail} />
</div>
<div className="grid lg:grid-cols-3 gap-4">
{['scout', 'trader', 'tycoon'].map((tier) => (
<div key={tier} className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<div className="flex items-center gap-3 mb-3">
{tier === 'tycoon' ? <Crown className="w-5 h-5 text-amber-400" /> :
tier === 'trader' ? <TrendingUp className="w-5 h-5 text-accent" /> :
<Zap className="w-5 h-5 text-foreground-muted" />}
<span className="text-sm font-medium text-foreground-muted capitalize">{tier}</span>
</div>
<p className="text-3xl font-display text-foreground">{stats.subscriptions[tier] || 0}</p>
</div>
))}
</div>
<div className="grid lg:grid-cols-2 gap-4">
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-2">Active Auctions</h3>
<p className="text-4xl font-display text-foreground">{stats.auctions.toLocaleString()}</p>
<p className="text-sm text-foreground-subtle mt-1">from all platforms</p>
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-red-400 animate-spin" />
</div>
) : (
<>
{/* Overview Tab */}
{activeTab === 'overview' && stats && (
<div className="space-y-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard title="Total Users" value={stats.users.total} subtitle={`${stats.users.new_this_week} new this week`} icon={Users} />
<StatCard title="Domains" value={stats.domains.watched} subtitle={`${stats.domains.portfolio} in portfolios`} icon={Eye} />
<StatCard title="TLDs" value={stats.tld_data.unique_tlds} subtitle={`${stats.tld_data.price_records.toLocaleString()} prices`} icon={Globe} />
<StatCard title="Newsletter" value={stats.newsletter_subscribers} icon={Mail} accent />
</div>
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-2">Price Alerts</h3>
<p className="text-4xl font-display text-foreground">{stats.price_alerts.toLocaleString()}</p>
<p className="text-sm text-foreground-subtle mt-1">active alerts</p>
</div>
</div>
</div>
)}
{/* Users Tab */}
{activeTab === 'users' && (
<div className="space-y-6 animate-fade-in">
<div className="flex flex-wrap items-center gap-4">
<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-subtle" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && loadAdminData()}
placeholder="Search users..."
className="w-full pl-11 pr-4 py-3 bg-background-secondary/50 border border-border/30 rounded-xl text-sm text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50"
/>
</div>
<button
onClick={handleExportUsers}
className="flex items-center gap-2 px-5 py-3 bg-background-secondary/50 border border-border/30 rounded-xl text-sm text-foreground hover:bg-foreground/5 transition-all"
>
<Download className="w-4 h-4" />
Export CSV
</button>
</div>
{selectedUsers.length > 0 && (
<div className="flex items-center gap-4 p-4 bg-accent/10 border border-accent/20 rounded-xl">
<span className="text-sm text-accent font-medium">{selectedUsers.length} users selected</span>
<div className="flex items-center gap-2 ml-auto">
<select
value={bulkTier}
onChange={(e) => setBulkTier(e.target.value)}
className="px-3 py-2 bg-background border border-border/30 rounded-lg text-sm"
>
<option value="scout">Scout</option>
<option value="trader">Trader</option>
<option value="tycoon">Tycoon</option>
</select>
<button
onClick={handleBulkUpgrade}
className="px-4 py-2 bg-accent text-background rounded-lg text-sm font-medium"
>
Upgrade Selected
</button>
<button
onClick={() => setSelectedUsers([])}
className="px-3 py-2 text-foreground-muted hover:text-foreground"
>
Clear
</button>
</div>
</div>
)}
<DataTable
data={users}
keyExtractor={(u) => u.id}
selectable
selectedIds={selectedUsers}
onSelectionChange={(ids) => setSelectedUsers(ids as number[])}
columns={[
{
key: 'email',
header: 'User',
render: (u) => (
<div>
<p className="font-medium text-foreground">{u.email}</p>
<p className="text-xs text-foreground-subtle">{u.name || 'No name'}</p>
</div>
),
},
{
key: 'status',
header: 'Status',
hideOnMobile: true,
render: (u) => (
<div className="flex items-center gap-2 flex-wrap">
{u.is_admin && <StatusBadge status="Admin" variant="accent" />}
{u.is_verified && <StatusBadge status="Verified" variant="success" />}
{!u.is_active && <StatusBadge status="Inactive" variant="error" />}
</div>
),
},
{
key: 'tier',
header: 'Tier',
render: (u) => (
<StatusBadge
status={u.subscription.tier_name}
variant={u.subscription.tier === 'tycoon' ? 'warning' : u.subscription.tier === 'trader' ? 'accent' : 'default'}
/>
),
},
{
key: 'domains',
header: 'Domains',
hideOnMobile: true,
render: (u) => <span className="text-foreground-muted">{u.domain_count}</span>,
},
{
key: 'actions',
header: 'Actions',
className: 'text-right',
headerClassName: 'text-right',
render: (u) => (
<div className="flex items-center justify-end gap-2">
<select
value={u.subscription.tier}
onChange={(e) => handleUpgradeUser(u.id, e.target.value)}
className="px-2 py-1.5 bg-background-secondary border border-border/30 rounded-lg text-xs text-foreground"
>
<option value="scout">Scout</option>
<option value="trader">Trader</option>
<option value="tycoon">Tycoon</option>
</select>
<TableAction
icon={Shield}
onClick={() => handleToggleAdmin(u.id, u.is_admin)}
variant={u.is_admin ? 'accent' : 'default'}
title={u.is_admin ? 'Remove admin' : 'Make admin'}
/>
<TableAction
icon={Trash2}
onClick={() => handleDeleteUser(u.id, u.email)}
variant="danger"
disabled={u.is_admin}
title={u.is_admin ? 'Cannot delete admin' : 'Delete user'}
/>
</div>
),
},
]}
emptyState={
<div className="text-center">
<Users className="w-12 h-12 text-foreground-subtle mx-auto mb-4" />
<p className="text-foreground-muted">No users found</p>
</div>
}
/>
<p className="text-sm text-foreground-subtle">Showing {users.length} of {usersTotal} users</p>
</div>
)}
{/* Price Alerts Tab */}
{activeTab === 'alerts' && (
<div className="space-y-6 animate-fade-in">
<p className="text-sm text-foreground-muted">{priceAlertsTotal} active price alerts</p>
<DataTable
data={priceAlerts}
keyExtractor={(a) => a.id}
columns={[
{ key: 'tld', header: 'TLD', render: (a) => <span className="font-mono text-accent">.{a.tld}</span> },
{ key: 'target_price', header: 'Target', render: (a) => a.target_price ? `$${a.target_price.toFixed(2)}` : '' },
{ key: 'alert_type', header: 'Type', render: (a) => <StatusBadge status={a.alert_type} /> },
{ key: 'user', header: 'User', render: (a) => a.user.email },
{ key: 'created_at', header: 'Created', hideOnMobile: true, render: (a) => new Date(a.created_at).toLocaleDateString() },
]}
emptyState={<><Bell className="w-12 h-12 text-foreground-subtle mx-auto mb-4" /><p className="text-foreground-muted">No active alerts</p></>}
/>
</div>
)}
{/* Newsletter Tab */}
{activeTab === 'newsletter' && (
<div className="space-y-6 animate-fade-in">
<div className="flex items-center justify-between">
<p className="text-sm text-foreground-muted">{newsletterTotal} subscribers</p>
<button
onClick={async () => {
try {
const data = await api.exportNewsletterEmails()
const blob = new Blob([data.emails.join('\n')], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'newsletter-emails.txt'
a.click()
} catch { setError('Export failed') }
}}
className="px-5 py-2.5 bg-accent text-background rounded-xl text-sm font-medium hover:bg-accent/90 transition-all"
>
Export Emails
</button>
</div>
<DataTable
data={newsletter}
keyExtractor={(s) => s.id}
columns={[
{ key: 'email', header: 'Email', render: (s) => <span className="text-foreground">{s.email}</span> },
{ key: 'is_active', header: 'Status', render: (s) => <StatusBadge status={s.is_active ? 'Active' : 'Unsubscribed'} variant={s.is_active ? 'success' : 'error'} /> },
{ key: 'subscribed_at', header: 'Subscribed', render: (s) => new Date(s.subscribed_at).toLocaleDateString() },
]}
/>
</div>
)}
{/* TLD Tab */}
{activeTab === 'tld' && (
<div className="grid lg:grid-cols-2 gap-6 animate-fade-in">
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-4">TLD Price Data</h3>
<div className="space-y-3">
<div className="flex justify-between"><span className="text-foreground-muted">Unique TLDs</span><span className="font-medium text-foreground">{stats?.tld_data.unique_tlds || 0}</span></div>
<div className="flex justify-between"><span className="text-foreground-muted">Price Records</span><span className="font-medium text-foreground">{stats?.tld_data.price_records?.toLocaleString() || 0}</span></div>
</div>
</div>
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-4">Scrape TLD Prices</h3>
<p className="text-sm text-foreground-muted mb-4">Manually trigger a TLD price scrape.</p>
<button onClick={handleTriggerScrape} disabled={scraping} className="flex items-center gap-2 px-5 py-3 bg-accent text-background rounded-xl font-medium hover:bg-accent/90 disabled:opacity-50 transition-all">
{scraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
{scraping ? 'Scraping...' : 'Start Scrape'}
</button>
</div>
</div>
)}
{/* Auctions Tab */}
{activeTab === 'auctions' && (
<div className="grid lg:grid-cols-2 gap-6 animate-fade-in">
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-4">Auction Data</h3>
<p className="text-4xl font-display text-foreground mb-2">{stats?.auctions || 0}</p>
<p className="text-sm text-foreground-subtle">Total auctions</p>
</div>
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-4">Scrape Auctions</h3>
<p className="text-sm text-foreground-muted mb-4">GoDaddy, Sedo, NameJet, DropCatch</p>
<button onClick={handleTriggerAuctionScrape} disabled={auctionScraping} className="flex items-center gap-2 px-5 py-3 bg-accent text-background rounded-xl font-medium hover:bg-accent/90 disabled:opacity-50 transition-all">
{auctionScraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
{auctionScraping ? 'Scraping...' : 'Start Auction Scrape'}
</button>
</div>
</div>
)}
{/* System Tab */}
{activeTab === 'system' && (
<div className="space-y-6 animate-fade-in">
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-4">System Status</h3>
<div className="space-y-4">
<div className="grid lg:grid-cols-3 gap-4">
{[
{ label: 'Database', ok: systemHealth?.database === 'healthy', text: systemHealth?.database || 'Unknown' },
{ label: 'Email (SMTP)', ok: systemHealth?.email_configured, text: systemHealth?.email_configured ? 'Configured' : 'Not configured' },
{ label: 'Stripe', ok: systemHealth?.stripe_configured, text: systemHealth?.stripe_configured ? 'Configured' : 'Not configured' },
{ label: 'Scheduler', ok: schedulerStatus?.scheduler_running, text: schedulerStatus?.scheduler_running ? 'Running' : 'Stopped' },
].map((item) => (
<div key={item.label} className="flex items-center justify-between py-3 border-b border-border/30 last:border-0">
<span className="text-foreground-muted">{item.label}</span>
<span className="flex items-center gap-2">
{item.ok ? <CheckCircle className="w-4 h-4 text-accent" /> : <XCircle className="w-4 h-4 text-amber-400" />}
<span className={item.ok ? 'text-accent' : 'text-amber-400'}>{item.text}</span>
</span>
{ tier: 'scout', icon: Zap, color: 'text-foreground-muted' },
{ tier: 'trader', icon: TrendingUp, color: 'text-accent' },
{ tier: 'tycoon', icon: Crown, color: 'text-amber-400' },
].map(({ tier, icon: Icon, color }) => (
<div key={tier} className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<div className="flex items-center gap-3 mb-3">
<Icon className={clsx("w-5 h-5", color)} />
<span className="text-sm font-medium text-foreground-muted capitalize">{tier}</span>
</div>
<p className="text-3xl font-display text-foreground">{stats.subscriptions[tier] || 0}</p>
</div>
))}
</div>
</div>
<div className="grid lg:grid-cols-2 gap-6">
<div className="grid lg:grid-cols-2 gap-4">
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-2">Active Auctions</h3>
<p className="text-4xl font-display text-foreground">{stats.auctions.toLocaleString()}</p>
</div>
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-2">Price Alerts</h3>
<p className="text-4xl font-display text-foreground">{stats.price_alerts.toLocaleString()}</p>
</div>
</div>
</div>
)}
{/* Users Tab */}
{activeTab === 'users' && (
<div className="space-y-6">
<div className="flex flex-wrap items-center gap-4">
<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-subtle" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && loadAdminData()}
placeholder="Search users..."
className="w-full pl-11 pr-4 py-3 bg-background-secondary/50 border border-border/30 rounded-xl text-sm"
/>
</div>
<button onClick={handleExportUsers} className="flex items-center gap-2 px-5 py-3 bg-background-secondary/50 border border-border/30 rounded-xl text-sm hover:bg-foreground/5">
<Download className="w-4 h-4" /> Export CSV
</button>
</div>
<PremiumTable
data={users}
keyExtractor={(u) => u.id}
selectable
selectedIds={selectedUsers}
onSelectionChange={(ids) => setSelectedUsers(ids as number[])}
columns={[
{
key: 'user',
header: 'User',
render: (u) => (
<div>
<p className="font-medium text-foreground">{u.email}</p>
<p className="text-xs text-foreground-subtle">{u.name || 'No name'}</p>
</div>
),
},
{
key: 'status',
header: 'Status',
hideOnMobile: true,
render: (u) => (
<div className="flex items-center gap-1.5 flex-wrap">
{u.is_admin && <Badge variant="accent" size="xs">Admin</Badge>}
{u.is_verified && <Badge variant="success" size="xs">Verified</Badge>}
{!u.is_active && <Badge variant="error" size="xs">Inactive</Badge>}
</div>
),
},
{
key: 'tier',
header: 'Tier',
render: (u) => (
<Badge variant={u.subscription.tier === 'tycoon' ? 'warning' : u.subscription.tier === 'trader' ? 'accent' : 'default'} size="sm">
{u.subscription.tier_name}
</Badge>
),
},
{
key: 'domains',
header: 'Domains',
hideOnMobile: true,
render: (u) => <span className="text-foreground-muted">{u.domain_count}</span>,
},
{
key: 'actions',
header: 'Actions',
align: 'right',
render: (u) => (
<div className="flex items-center justify-end gap-2">
<select
value={u.subscription.tier}
onChange={(e) => handleUpgradeUser(u.id, e.target.value)}
className="px-2 py-1.5 bg-background-secondary border border-border/30 rounded-lg text-xs"
>
<option value="scout">Scout</option>
<option value="trader">Trader</option>
<option value="tycoon">Tycoon</option>
</select>
<TableActionButton icon={Shield} onClick={() => handleToggleAdmin(u.id, u.is_admin)} variant={u.is_admin ? 'accent' : 'default'} title={u.is_admin ? 'Remove admin' : 'Make admin'} />
<TableActionButton icon={Trash2} onClick={() => handleDeleteUser(u.id, u.email)} variant="danger" disabled={u.is_admin} title="Delete user" />
</div>
),
},
]}
emptyIcon={<Users className="w-12 h-12 text-foreground-subtle" />}
emptyTitle="No users found"
/>
<p className="text-sm text-foreground-subtle">Showing {users.length} of {usersTotal} users</p>
</div>
)}
{/* Newsletter Tab */}
{activeTab === 'newsletter' && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<p className="text-sm text-foreground-muted">{newsletterTotal} subscribers</p>
<button
onClick={async () => {
const data = await api.exportNewsletterEmails()
const blob = new Blob([data.emails.join('\n')], { type: 'text/plain' })
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = 'newsletter-emails.txt'
a.click()
}}
className="px-5 py-2.5 bg-red-500 text-white rounded-xl text-sm font-medium hover:bg-red-600"
>
Export Emails
</button>
</div>
<PremiumTable
data={newsletter}
keyExtractor={(s) => s.id}
columns={[
{ key: 'email', header: 'Email', render: (s) => <span className="text-foreground">{s.email}</span> },
{ key: 'status', header: 'Status', render: (s) => <Badge variant={s.is_active ? 'success' : 'error'} dot>{s.is_active ? 'Active' : 'Unsubscribed'}</Badge> },
{ key: 'subscribed', header: 'Subscribed', render: (s) => new Date(s.subscribed_at).toLocaleDateString() },
]}
/>
</div>
)}
{/* System Tab */}
{activeTab === 'system' && (
<div className="space-y-6">
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-4">Manual Triggers</h3>
<div className="space-y-3">
<button onClick={handleTriggerDomainChecks} disabled={domainChecking} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-foreground text-background rounded-xl font-medium hover:bg-foreground/90 disabled:opacity-50 transition-all">
{domainChecking ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
{domainChecking ? 'Checking...' : 'Check All Domains'}
</button>
<button onClick={handleSendTestEmail} disabled={sendingEmail || !systemHealth?.email_configured} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-foreground/10 border border-border/30 text-foreground rounded-xl font-medium hover:bg-foreground/20 disabled:opacity-50 transition-all">
{sendingEmail ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
{sendingEmail ? 'Sending...' : 'Send Test Email'}
</button>
<h3 className="text-lg font-medium text-foreground mb-4">System Status</h3>
<div className="space-y-4">
{[
{ label: 'Database', ok: systemHealth?.database === 'healthy', text: systemHealth?.database || 'Unknown' },
{ label: 'Email (SMTP)', ok: systemHealth?.email_configured, text: systemHealth?.email_configured ? 'Configured' : 'Not configured' },
{ label: 'Stripe', ok: systemHealth?.stripe_configured, text: systemHealth?.stripe_configured ? 'Configured' : 'Not configured' },
{ label: 'Scheduler', ok: schedulerStatus?.scheduler_running, text: schedulerStatus?.scheduler_running ? 'Running' : 'Stopped' },
].map((item) => (
<div key={item.label} className="flex items-center justify-between py-3 border-b border-border/30 last:border-0">
<span className="text-foreground-muted">{item.label}</span>
<span className="flex items-center gap-2">
{item.ok ? <CheckCircle className="w-4 h-4 text-accent" /> : <XCircle className="w-4 h-4 text-amber-400" />}
<span className={item.ok ? 'text-accent' : 'text-amber-400'}>{item.text}</span>
</span>
</div>
))}
</div>
</div>
{schedulerStatus && (
<div className="grid lg:grid-cols-2 gap-6">
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-4">Scheduled Jobs</h3>
<h3 className="text-lg font-medium text-foreground mb-4">Manual Triggers</h3>
<div className="space-y-3">
{schedulerStatus.jobs?.slice(0, 4).map((job: any) => (
<div key={job.id} className="flex items-center justify-between text-sm">
<span className="text-foreground truncate">{job.name}</span>
<span className="text-foreground-subtle text-xs">{job.trigger}</span>
</div>
))}
<button onClick={handleTriggerDomainChecks} disabled={domainChecking} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-foreground text-background rounded-xl font-medium disabled:opacity-50">
{domainChecking ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
{domainChecking ? 'Checking...' : 'Check All Domains'}
</button>
<button onClick={handleSendTestEmail} disabled={sendingEmail} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-foreground/10 border border-border/30 rounded-xl font-medium disabled:opacity-50">
{sendingEmail ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
{sendingEmail ? 'Sending...' : 'Send Test Email'}
</button>
<button onClick={handleTriggerScrape} disabled={scraping} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-foreground/10 border border-border/30 rounded-xl font-medium disabled:opacity-50">
{scraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Globe className="w-4 h-4" />}
{scraping ? 'Scraping...' : 'Scrape TLD Prices'}
</button>
<button onClick={handleTriggerAuctionScrape} disabled={auctionScraping} className="w-full flex items-center justify-center gap-2 px-5 py-3 bg-foreground/10 border border-border/30 rounded-xl font-medium disabled:opacity-50">
{auctionScraping ? <Loader2 className="w-4 h-4 animate-spin" /> : <Gavel className="w-4 h-4" />}
{auctionScraping ? 'Scraping...' : 'Scrape Auctions'}
</button>
</div>
</div>
)}
</div>
</div>
)}
{/* Blog Tab */}
{activeTab === 'blog' && (
<div className="space-y-6 animate-fade-in">
<div className="flex items-center justify-between">
<p className="text-sm text-foreground-muted">{blogPostsTotal} blog posts</p>
<button className="flex items-center gap-2 px-5 py-2.5 bg-accent text-background rounded-xl text-sm font-medium hover:bg-accent/90 transition-all">
<Plus className="w-4 h-4" />
New Post
</button>
</div>
<DataTable
data={blogPosts}
keyExtractor={(p) => p.id}
columns={[
{ key: 'title', header: 'Title', render: (p) => <div><p className="font-medium text-foreground line-clamp-1">{p.title}</p><p className="text-xs text-foreground-subtle">{p.slug}</p></div> },
{ key: 'category', header: 'Category', hideOnMobile: true, render: (p) => p.category ? <StatusBadge status={p.category} /> : '' },
{ key: 'is_published', header: 'Status', render: (p) => <StatusBadge status={p.is_published ? 'Published' : 'Draft'} variant={p.is_published ? 'success' : 'warning'} /> },
{ key: 'view_count', header: 'Views', hideOnMobile: true, render: (p) => p.view_count },
{ key: 'actions', header: 'Actions', className: 'text-right', headerClassName: 'text-right', render: (p) => (
<div className="flex items-center justify-end gap-2">
<TableAction icon={ExternalLink} onClick={() => window.open(`/blog/${p.slug}`, '_blank')} title="View" />
<TableAction icon={Edit2} title="Edit" />
<TableAction icon={Trash2} variant="danger" title="Delete" />
{schedulerStatus && (
<div className="p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl">
<h3 className="text-lg font-medium text-foreground mb-4">Scheduled Jobs</h3>
<div className="space-y-3">
{schedulerStatus.jobs?.slice(0, 5).map((job: any) => (
<div key={job.id} className="flex items-center justify-between text-sm">
<span className="text-foreground truncate">{job.name}</span>
<span className="text-foreground-subtle text-xs">{job.trigger}</span>
</div>
))}
</div>
</div>
) },
]}
emptyState={<><BookOpen className="w-12 h-12 text-foreground-subtle mx-auto mb-4" /><p className="text-foreground-muted">No blog posts yet</p></>}
/>
</div>
)}
)}
</div>
</div>
)}
{/* Activity Tab */}
{activeTab === 'activity' && (
<div className="space-y-6 animate-fade-in">
<p className="text-sm text-foreground-muted">{activityLogTotal} log entries</p>
<DataTable
{/* Other tabs similar pattern... */}
{activeTab === 'alerts' && (
<PremiumTable
data={priceAlerts}
keyExtractor={(a) => a.id}
emptyIcon={<Bell className="w-12 h-12 text-foreground-subtle" />}
emptyTitle="No active price alerts"
columns={[
{ key: 'tld', header: 'TLD', render: (a) => <span className="font-mono text-accent">.{a.tld}</span> },
{ key: 'target', header: 'Target', render: (a) => a.target_price ? `$${a.target_price.toFixed(2)}` : '—' },
{ key: 'type', header: 'Type', render: (a) => <Badge>{a.alert_type}</Badge> },
{ key: 'user', header: 'User', render: (a) => a.user.email },
{ key: 'created', header: 'Created', hideOnMobile: true, render: (a) => new Date(a.created_at).toLocaleDateString() },
]}
/>
)}
{activeTab === 'activity' && (
<PremiumTable
data={activityLog}
keyExtractor={(l) => l.id}
emptyIcon={<History className="w-12 h-12 text-foreground-subtle" />}
emptyTitle="No activity logged"
columns={[
{ key: 'action', header: 'Action', render: (l) => <StatusBadge status={l.action} /> },
{ key: 'action', header: 'Action', render: (l) => <Badge>{l.action}</Badge> },
{ key: 'details', header: 'Details', render: (l) => <span className="text-foreground-muted">{l.details}</span> },
{ key: 'admin', header: 'Admin', render: (l) => l.admin.email },
{ key: 'created_at', header: 'Time', hideOnMobile: true, render: (l) => new Date(l.created_at).toLocaleString() },
{ key: 'time', header: 'Time', hideOnMobile: true, render: (l) => new Date(l.created_at).toLocaleString() },
]}
emptyState={<><History className="w-12 h-12 text-foreground-subtle mx-auto mb-4" /><p className="text-foreground-muted">No activity logged</p></>}
/>
</div>
)}
</>
)}
</CommandCenterLayout>
)
}
)}
// Stat Card Component
function StatCard({ title, value, subtitle, icon: Icon }: { title: string; value: number; subtitle: string; icon: React.ComponentType<{ className?: string }> }) {
return (
<div className="group relative p-6 bg-gradient-to-br from-background-secondary/50 to-background-secondary/20 border border-border/30 rounded-2xl hover:border-accent/30 transition-all duration-300">
<div className="absolute inset-0 rounded-2xl bg-accent/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative">
<div className="w-11 h-11 bg-foreground/5 border border-border/30 rounded-xl flex items-center justify-center mb-4 group-hover:border-accent/30 group-hover:bg-accent/5 transition-all">
<Icon className="w-5 h-5 text-foreground-muted group-hover:text-accent transition-colors" />
</div>
<p className="text-[10px] text-foreground-subtle uppercase tracking-wider mb-1">{title}</p>
<p className="text-3xl font-display text-foreground mb-1">{value.toLocaleString()}</p>
<p className="text-sm text-foreground-subtle">{subtitle}</p>
</div>
</div>
{activeTab === 'blog' && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<p className="text-sm text-foreground-muted">{blogPostsTotal} posts</p>
<button className="flex items-center gap-2 px-5 py-2.5 bg-red-500 text-white rounded-xl text-sm font-medium">
<Plus className="w-4 h-4" /> New Post
</button>
</div>
<PremiumTable
data={blogPosts}
keyExtractor={(p) => p.id}
emptyIcon={<BookOpen className="w-12 h-12 text-foreground-subtle" />}
emptyTitle="No blog posts"
columns={[
{ key: 'title', header: 'Title', render: (p) => <div><p className="font-medium text-foreground">{p.title}</p><p className="text-xs text-foreground-subtle">{p.slug}</p></div> },
{ key: 'category', header: 'Category', hideOnMobile: true, render: (p) => p.category ? <Badge>{p.category}</Badge> : '—' },
{ key: 'status', header: 'Status', render: (p) => <Badge variant={p.is_published ? 'success' : 'warning'}>{p.is_published ? 'Published' : 'Draft'}</Badge> },
{ key: 'views', header: 'Views', hideOnMobile: true, render: (p) => p.view_count },
{ key: 'actions', header: '', align: 'right', render: (p) => (
<div className="flex items-center justify-end gap-2">
<TableActionButton icon={ExternalLink} onClick={() => window.open(`/blog/${p.slug}`, '_blank')} />
<TableActionButton icon={Edit2} />
<TableActionButton icon={Trash2} variant="danger" />
</div>
) },
]}
/>
</div>
)}
</>
)}
</PageContainer>
</AdminLayout>
)
}

View File

@ -1,106 +0,0 @@
import { Metadata } from 'next'
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch'
export const metadata: Metadata = {
title: 'Domain Auctions — Smart Pounce',
description: 'Find undervalued domain auctions from GoDaddy, Sedo, NameJet, SnapNames & DropCatch. Our Smart Pounce algorithm identifies the best opportunities with transparent valuations.',
keywords: [
'domain auctions',
'expired domains',
'domain bidding',
'GoDaddy auctions',
'Sedo domains',
'NameJet',
'domain investment',
'undervalued domains',
'domain flipping',
],
openGraph: {
title: 'Domain Auctions — Smart Pounce by pounce',
description: 'Find undervalued domain auctions. Transparent valuations, multiple platforms, no payment handling.',
url: `${siteUrl}/auctions`,
type: 'website',
images: [
{
url: `${siteUrl}/og-auctions.png`,
width: 1200,
height: 630,
alt: 'Smart Pounce - Domain Auction Aggregator',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'Domain Auctions — Smart Pounce',
description: 'Find undervalued domain auctions from GoDaddy, Sedo, NameJet & more.',
},
alternates: {
canonical: `${siteUrl}/auctions`,
},
}
// JSON-LD for Auctions page
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Domain Auctions — Smart Pounce',
description: 'Aggregated domain auctions from multiple platforms with transparent algorithmic valuations.',
url: `${siteUrl}/auctions`,
isPartOf: {
'@type': 'WebSite',
name: 'pounce',
url: siteUrl,
},
about: {
'@type': 'Service',
name: 'Smart Pounce',
description: 'Domain auction aggregation and opportunity analysis',
provider: {
'@type': 'Organization',
name: 'pounce',
},
},
mainEntity: {
'@type': 'ItemList',
name: 'Domain Auctions',
description: 'Live domain auctions from GoDaddy, Sedo, NameJet, SnapNames, and DropCatch',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'GoDaddy Auctions',
url: 'https://auctions.godaddy.com',
},
{
'@type': 'ListItem',
position: 2,
name: 'Sedo',
url: 'https://sedo.com',
},
{
'@type': 'ListItem',
position: 3,
name: 'NameJet',
url: 'https://namejet.com',
},
],
},
}
export default function AuctionsLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{children}
</>
)
}

View File

@ -1,20 +1,16 @@
'use client'
import { useEffect, useState } from 'react'
import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { PremiumTable, Badge, PlatformBadge, StatCard, PageContainer } from '@/components/PremiumTable'
import {
Clock,
TrendingUp,
ExternalLink,
Search,
Flame,
Timer,
Users,
ArrowUpRight,
Lock,
Gavel,
ChevronUp,
ChevronDown,
@ -22,8 +18,9 @@ import {
DollarSign,
RefreshCw,
Target,
Info,
X,
TrendingUp,
Loader2,
} from 'lucide-react'
import Link from 'next/link'
import clsx from 'clsx'
@ -54,11 +51,6 @@ interface Opportunity {
price_range?: string
recommendation: string
reasoning?: string
// Legacy fields
estimated_value?: number
current_bid?: number
value_ratio?: number
potential_profit?: number
}
}
@ -71,49 +63,10 @@ const PLATFORMS = [
{ id: 'Sedo', name: 'Sedo' },
{ id: 'NameJet', name: 'NameJet' },
{ id: 'DropCatch', name: 'DropCatch' },
{ id: 'ExpiredDomains', name: 'Expired Domains' },
]
const TAB_DESCRIPTIONS: Record<TabType, { title: string; description: string }> = {
all: {
title: 'All Auctions',
description: 'All active auctions from all platforms, sorted by ending time by default.',
},
ending: {
title: 'Ending Soon',
description: 'Auctions ending within the next 24 hours. Best for last-minute sniping opportunities.',
},
hot: {
title: 'Hot Auctions',
description: 'Auctions with the most bidding activity (20+ bids). High competition but proven demand.',
},
opportunities: {
title: 'Smart Opportunities',
description: 'Our algorithm scores auctions based on: Time urgency (ending soon = higher score), Competition (fewer bids = higher score), and Price point (lower entry = higher score). Only auctions with a combined score ≥ 3 are shown.',
},
}
function SortIcon({ field, currentField, direction }: { field: SortField, currentField: SortField, direction: 'asc' | 'desc' }) {
if (field !== currentField) {
return <ChevronsUpDown className="w-4 h-4 text-foreground-subtle" />
}
return direction === 'asc'
? <ChevronUp className="w-4 h-4 text-accent" />
: <ChevronDown className="w-4 h-4 text-accent" />
}
function getPlatformBadgeClass(platform: string) {
switch (platform) {
case 'GoDaddy': return 'text-blue-400 bg-blue-400/10'
case 'Sedo': return 'text-orange-400 bg-orange-400/10'
case 'NameJet': return 'text-purple-400 bg-purple-400/10'
case 'DropCatch': return 'text-teal-400 bg-teal-400/10'
default: return 'text-foreground-muted bg-foreground/5'
}
}
export default function AuctionsPage() {
const { isAuthenticated, checkAuth, isLoading: authLoading } = useStore()
const { isAuthenticated, subscription } = useStore()
const [allAuctions, setAllAuctions] = useState<Auction[]>([])
const [endingSoon, setEndingSoon] = useState<Auction[]>([])
@ -131,9 +84,8 @@ export default function AuctionsPage() {
const [maxBid, setMaxBid] = useState<string>('')
useEffect(() => {
checkAuth()
loadData()
}, [checkAuth])
}, [])
useEffect(() => {
if (isAuthenticated && opportunities.length === 0) {
@ -203,15 +155,9 @@ export default function AuctionsPage() {
}
const filteredAuctions = getCurrentAuctions().filter(auction => {
if (searchQuery && !auction.domain.toLowerCase().includes(searchQuery.toLowerCase())) {
return false
}
if (selectedPlatform !== 'All' && auction.platform !== selectedPlatform) {
return false
}
if (maxBid && auction.current_bid > parseFloat(maxBid)) {
return false
}
if (searchQuery && !auction.domain.toLowerCase().includes(searchQuery.toLowerCase())) return false
if (selectedPlatform !== 'All' && auction.platform !== selectedPlatform) return false
if (maxBid && auction.current_bid > parseFloat(maxBid)) return false
return true
})
@ -233,12 +179,8 @@ export default function AuctionsPage() {
})
const getTimeColor = (timeRemaining: string) => {
if (timeRemaining.includes('m') && !timeRemaining.includes('h') && !timeRemaining.includes('d')) {
return 'text-danger'
}
if (timeRemaining.includes('h') && parseInt(timeRemaining) < 2) {
return 'text-warning'
}
if (timeRemaining.includes('m') && !timeRemaining.includes('h') && !timeRemaining.includes('d')) return 'text-red-400'
if (timeRemaining.includes('h') && parseInt(timeRemaining) < 2) return 'text-amber-400'
return 'text-foreground-muted'
}
@ -251,441 +193,225 @@ export default function AuctionsPage() {
}
}
if (authLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="w-5 h-5 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
)
}
return (
<div className="min-h-screen bg-background relative overflow-hidden">
{/* Background Effects - matching landing page */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-[-20%] left-1/2 -translate-x-1/2 w-[1200px] h-[800px] bg-accent/[0.03] rounded-full blur-[120px]" />
<div className="absolute bottom-[-10%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.02] rounded-full blur-[100px]" />
<div
className="absolute inset-0 opacity-[0.015]"
style={{
backgroundImage: `linear-gradient(rgba(255,255,255,.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.1) 1px, transparent 1px)`,
backgroundSize: '64px 64px',
}}
/>
</div>
<CommandCenterLayout
title="Auctions"
subtitle="Real-time from GoDaddy, Sedo, NameJet & DropCatch"
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 */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard title="All Auctions" value={allAuctions.length} icon={Gavel} />
<StatCard title="Ending Soon" value={endingSoon.length} icon={Timer} accent />
<StatCard title="Hot Auctions" value={hotAuctions.length} subtitle="20+ bids" icon={Flame} />
<StatCard title="Opportunities" value={opportunities.length} icon={Target} />
</div>
<Header />
{/* Tabs */}
<div className="flex flex-wrap items-center gap-2 p-1.5 bg-background-secondary/30 border border-border/30 rounded-2xl w-fit">
{[
{ id: 'all' as const, label: 'All', icon: Gavel, count: allAuctions.length },
{ id: 'ending' as const, label: 'Ending Soon', icon: Timer, count: endingSoon.length, color: 'warning' },
{ id: 'hot' as const, label: 'Hot', icon: Flame, count: hotAuctions.length },
{ id: 'opportunities' as const, label: 'Opportunities', icon: Target, count: opportunities.length },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={clsx(
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-xl transition-all",
activeTab === tab.id
? tab.color === 'warning'
? "bg-amber-500 text-background"
: "bg-gradient-to-r from-accent to-accent/80 text-background shadow-lg shadow-accent/20"
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
)}
>
<tab.icon className="w-4 h-4" />
<span className="hidden sm:inline">{tab.label}</span>
<span className={clsx(
"text-xs px-1.5 py-0.5 rounded",
activeTab === tab.id ? "bg-background/20" : "bg-foreground/10"
)}>{tab.count}</span>
</button>
))}
</div>
<main className="relative pt-32 sm:pt-40 pb-20 sm:pb-28 px-4 sm:px-6 flex-1">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="text-center mb-16 sm:mb-20 animate-fade-in">
<span className="text-sm font-semibold text-accent uppercase tracking-wider">Auction Aggregator</span>
<h1 className="mt-4 font-display text-[2.5rem] sm:text-[3.5rem] md:text-[4.5rem] lg:text-[5rem] leading-[0.95] tracking-[-0.03em] text-foreground">
Curated Opportunities
</h1>
<p className="mt-5 text-lg sm:text-xl text-foreground-muted max-w-2xl mx-auto">
Real-time from GoDaddy, Sedo, NameJet & DropCatch. Find opportunities.
</p>
{/* Filters */}
<div className="flex flex-wrap gap-3">
<div className="relative flex-1 min-w-[200px] max-w-md">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-subtle" />
<input
type="text"
placeholder="Search domains..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-11 pr-10 py-3 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"
/>
{searchQuery && (
<button onClick={() => setSearchQuery('')} className="absolute right-4 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground">
<X className="w-4 h-4" />
</button>
)}
</div>
{/* Login Banner for non-authenticated users */}
{!isAuthenticated && (
<div className="mb-8 p-5 bg-accent-muted border border-accent/20 rounded-xl flex flex-col sm:flex-row items-center justify-between gap-4 animate-fade-in">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-accent/20 rounded-xl flex items-center justify-center">
<Target className="w-5 h-5 text-accent" />
</div>
<div>
<p className="text-body-sm font-medium text-foreground">Unlock Smart Opportunities</p>
<p className="text-ui-sm text-foreground-muted">
Sign in for algorithmic deal-finding and alerts.
</p>
</div>
</div>
<Link
href="/register"
className="shrink-0 px-5 py-2.5 bg-accent text-background text-ui font-medium rounded-lg
hover:bg-accent-hover transition-all duration-300"
>
Hunt Free
</Link>
</div>
)}
{/* Tabs - flex-wrap to avoid horizontal scroll */}
<div className="mb-6 animate-slide-up">
<div className="flex flex-wrap items-center gap-2 mb-6">
<button
onClick={() => setActiveTab('all')}
title="View all active auctions from all platforms"
className={clsx(
"flex items-center gap-2 px-4 py-2.5 text-ui-sm font-medium rounded-lg transition-all",
activeTab === 'all'
? "bg-foreground text-background"
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground hover:bg-background-secondary"
)}
>
<Gavel className="w-4 h-4" />
All
<span className={clsx(
"text-ui-xs px-1.5 py-0.5 rounded",
activeTab === 'all' ? "bg-background/20" : "bg-foreground/10"
)}>{allAuctions.length}</span>
</button>
<button
onClick={() => setActiveTab('ending')}
title="Auctions ending in the next 24 hours - best for sniping"
className={clsx(
"flex items-center gap-2 px-4 py-2.5 text-ui-sm font-medium rounded-lg transition-all",
activeTab === 'ending'
? "bg-warning text-background"
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground hover:bg-background-secondary"
)}
>
<Timer className="w-4 h-4" />
Ending Soon
<span className={clsx(
"text-ui-xs px-1.5 py-0.5 rounded",
activeTab === 'ending' ? "bg-background/20" : "bg-foreground/10"
)}>{endingSoon.length}</span>
</button>
<button
onClick={() => setActiveTab('hot')}
title="Auctions with 20+ bids - high demand, proven interest"
className={clsx(
"flex items-center gap-2 px-4 py-2.5 text-ui-sm font-medium rounded-lg transition-all",
activeTab === 'hot'
? "bg-accent text-background"
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground hover:bg-background-secondary"
)}
>
<Flame className="w-4 h-4" />
Hot
<span className={clsx(
"text-ui-xs px-1.5 py-0.5 rounded",
activeTab === 'hot' ? "bg-background/20" : "bg-foreground/10"
)}>{hotAuctions.length}</span>
</button>
<button
onClick={() => setActiveTab('opportunities')}
title="Smart algorithm: Time urgency × Competition × Price = Score"
className={clsx(
"flex items-center gap-2 px-4 py-2.5 text-ui-sm font-medium rounded-lg transition-all",
activeTab === 'opportunities'
? "bg-accent text-background"
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground hover:bg-background-secondary"
)}
>
<Target className="w-4 h-4" />
Opportunities
{!isAuthenticated && <Lock className="w-3 h-3 ml-1" />}
{isAuthenticated && (
<span className={clsx(
"text-ui-xs px-1.5 py-0.5 rounded",
activeTab === 'opportunities' ? "bg-background/20" : "bg-foreground/10"
)}>{opportunities.length}</span>
)}
</button>
<button
onClick={handleRefresh}
disabled={refreshing}
title="Refresh auction data from all platforms"
className="ml-auto flex items-center gap-2 px-4 py-2.5 text-ui-sm text-foreground-muted hover:text-foreground hover:bg-background-secondary/50 rounded-lg transition-all disabled:opacity-50"
>
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
Refresh
</button>
</div>
</div>
{/* Search & Filters */}
<div className="mb-6 animate-slide-up">
<div className="flex flex-wrap gap-3">
<div className="relative flex-1 min-w-[200px] max-w-md">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-foreground-subtle" />
<input
type="text"
placeholder="Search domains..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-12 pr-10 py-3 bg-background-secondary/50 border border-border rounded-xl
text-body text-foreground placeholder:text-foreground-subtle
focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent
transition-all duration-300"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-4 top-1/2 -translate-y-1/2 p-1 text-foreground-subtle hover:text-foreground transition-colors"
>
<X className="w-4 h-4" />
</button>
)}
</div>
<select
value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value)}
title="Filter by auction platform"
className="px-4 py-3 bg-background-secondary/50 border border-border rounded-xl
text-body text-foreground focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent cursor-pointer transition-all"
>
{PLATFORMS.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
<div className="relative">
<DollarSign className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-foreground-subtle" />
<input
type="number"
placeholder="Max bid"
title="Filter auctions under this bid amount"
value={maxBid}
onChange={(e) => setMaxBid(e.target.value)}
className="w-32 pl-11 pr-4 py-3 bg-background-secondary/50 border border-border rounded-xl
text-body text-foreground placeholder:text-foreground-subtle
focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent transition-all"
/>
</div>
</div>
</div>
{/* Content */}
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
) : activeTab === 'opportunities' && !isAuthenticated ? (
<div className="text-center py-20 border border-dashed border-border rounded-2xl bg-background-secondary/20">
<div className="w-14 h-14 bg-accent/10 rounded-2xl flex items-center justify-center mx-auto mb-5">
<Target className="w-7 h-7 text-accent" />
</div>
<h3 className="text-body-lg font-medium text-foreground mb-2">Unlock Smart Opportunities</h3>
<p className="text-body-sm text-foreground-muted max-w-md mx-auto mb-6">
Our algorithm analyzes ending times, bid activity, and price points to find the best opportunities.
</p>
<Link
href="/register"
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background text-ui font-medium rounded-xl
hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
>
Join the Hunt
<ArrowUpRight className="w-4 h-4" />
</Link>
</div>
) : (
/* Table - using proper <table> like TLD Prices */
<div className="bg-background-secondary/30 border border-border rounded-xl overflow-hidden animate-slide-up">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-background-secondary border-b border-border">
<th className="text-left px-4 sm:px-6 py-4">
<button
onClick={() => handleSort('ending')}
title="Sort by ending time"
className="flex items-center gap-2 text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
>
Domain
<SortIcon field="ending" currentField={sortBy} direction={sortDirection} />
</button>
</th>
<th className="text-left px-4 sm:px-6 py-4 hidden lg:table-cell">
<span className="text-ui-sm text-foreground-subtle font-medium">Platform</span>
</th>
<th className="text-right px-4 sm:px-6 py-4">
<button
onClick={() => handleSort('bid_asc')}
title="Current highest bid in USD"
className="flex items-center gap-2 ml-auto text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
>
Bid
<SortIcon field="bid_asc" currentField={sortBy} direction={sortDirection} />
</button>
</th>
<th className="text-right px-4 sm:px-6 py-4 hidden sm:table-cell">
<button
onClick={() => handleSort('bids')}
title="Number of bids placed"
className="flex items-center gap-2 ml-auto text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
>
Bids
<SortIcon field="bids" currentField={sortBy} direction={sortDirection} />
</button>
</th>
<th className="text-right px-4 sm:px-6 py-4 hidden md:table-cell">
<span className="text-ui-sm text-foreground-subtle font-medium" title="Time remaining">Time Left</span>
</th>
{activeTab === 'opportunities' && (
<th className="text-center px-4 sm:px-6 py-4">
<span className="text-ui-sm text-foreground-subtle font-medium" title="Opportunity score">Score</span>
</th>
)}
<th className="px-4 sm:px-6 py-4"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{sortedAuctions.length === 0 ? (
<tr>
<td colSpan={activeTab === 'opportunities' ? 7 : 6} className="px-6 py-12 text-center text-foreground-muted">
{activeTab === 'opportunities'
? 'No opportunities right now — check back later!'
: searchQuery
? `No auctions found matching "${searchQuery}"`
: 'No auctions found'}
</td>
</tr>
) : (
sortedAuctions.map((auction, idx) => {
const oppData = getOpportunityData(auction.domain)
return (
<tr
key={`${auction.domain}-${idx}`}
className="hover:bg-background-secondary/50 transition-colors group"
>
{/* Domain */}
<td className="px-4 sm:px-6 py-4">
<div className="flex flex-col gap-1">
<a
href={auction.affiliate_url}
target="_blank"
rel="noopener noreferrer"
title={`Go to ${auction.platform} to bid on ${auction.domain}`}
className="font-mono text-body-sm sm:text-body font-medium text-foreground hover:text-accent transition-colors"
>
{auction.domain}
</a>
<div className="flex items-center gap-2 text-body-xs text-foreground-subtle lg:hidden">
<span className={clsx("text-ui-xs px-1.5 py-0.5 rounded", getPlatformBadgeClass(auction.platform))}>
{auction.platform}
</span>
{auction.age_years && (
<span title={`Domain age: ${auction.age_years} years`}>{auction.age_years}y</span>
)}
</div>
</div>
</td>
{/* Platform */}
<td className="px-4 sm:px-6 py-4 hidden lg:table-cell">
<div className="flex flex-col gap-1">
<span
className={clsx("text-ui-sm px-2 py-0.5 rounded-full w-fit", getPlatformBadgeClass(auction.platform))}
title={`${auction.platform} - Click Bid to go to auction`}
>
{auction.platform}
</span>
{auction.age_years && (
<span className="text-body-xs text-foreground-subtle" title={`Domain age: ${auction.age_years} years`}>
<Clock className="w-3 h-3 inline mr-1" />
{auction.age_years}y
</span>
)}
</div>
</td>
{/* Current Bid */}
<td className="px-4 sm:px-6 py-4 text-right">
<span
className="text-body-sm font-medium text-foreground"
title={`Current highest bid: ${formatCurrency(auction.current_bid)}`}
>
{formatCurrency(auction.current_bid)}
</span>
{auction.buy_now_price && (
<p className="text-ui-xs text-accent" title={`Buy Now for ${formatCurrency(auction.buy_now_price)}`}>
Buy: {formatCurrency(auction.buy_now_price)}
</p>
)}
</td>
{/* Bids */}
<td className="px-4 sm:px-6 py-4 text-right hidden sm:table-cell">
<span
className={clsx(
"text-body-sm font-medium inline-flex items-center gap-1",
auction.num_bids >= 20 ? "text-accent" :
auction.num_bids >= 10 ? "text-warning" :
"text-foreground-muted"
)}
title={`${auction.num_bids} bids - ${auction.num_bids >= 20 ? 'High competition!' : auction.num_bids >= 10 ? 'Moderate interest' : 'Low competition'}`}
>
{auction.num_bids}
{auction.num_bids >= 20 && <Flame className="w-3 h-3" />}
</span>
</td>
{/* Time Left */}
<td className="px-4 sm:px-6 py-4 text-right hidden md:table-cell">
<span
className={clsx("text-body-sm font-medium", getTimeColor(auction.time_remaining))}
title={`Auction ends: ${new Date(auction.end_time).toLocaleString()}`}
>
{auction.time_remaining}
</span>
</td>
{/* Score (opportunities only) */}
{activeTab === 'opportunities' && oppData && (
<td className="px-4 sm:px-6 py-4 text-center">
<span
className="inline-flex items-center justify-center w-9 h-9 bg-accent/10 text-accent font-bold rounded-lg text-body-sm"
title={`Score: ${oppData.opportunity_score}${oppData.reasoning ? ' - ' + oppData.reasoning : ''}`}
>
{oppData.opportunity_score}
</span>
</td>
)}
{/* Action */}
<td className="px-4 sm:px-6 py-4 text-right">
<a
href={auction.affiliate_url}
target="_blank"
rel="noopener noreferrer"
title={`Open ${auction.platform} to place your bid`}
className="inline-flex items-center gap-1.5 px-4 py-2 bg-foreground text-background text-ui-sm font-medium rounded-lg
hover:bg-foreground/90 transition-all opacity-70 group-hover:opacity-100"
>
Bid
<ExternalLink className="w-3.5 h-3.5" />
</a>
</td>
</tr>
)
})
)}
</tbody>
</table>
</div>
</div>
)}
{/* Info Footer */}
<div className="mt-10 p-5 bg-background-secondary/30 border border-border rounded-xl animate-slide-up">
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-foreground/5 rounded-xl flex items-center justify-center shrink-0">
<Info className="w-5 h-5 text-foreground-muted" />
</div>
<div>
<h4 className="text-body font-medium text-foreground mb-1.5">
{TAB_DESCRIPTIONS[activeTab].title}
</h4>
<p className="text-body-sm text-foreground-subtle leading-relaxed mb-3">
{TAB_DESCRIPTIONS[activeTab].description}
</p>
<p className="text-body-sm text-foreground-subtle leading-relaxed">
<span className="text-foreground-muted font-medium">Sources:</span> GoDaddy, Sedo, NameJet, DropCatch, ExpiredDomains.
Click "Bid" to go to the auction we don't handle transactions.
</p>
</div>
</div>
<select
value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value)}
className="px-4 py-3 bg-background-secondary/50 border border-border/50 rounded-xl
text-sm text-foreground cursor-pointer focus:outline-none focus:border-accent/50"
>
{PLATFORMS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
<div className="relative">
<DollarSign className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-subtle" />
<input
type="number"
placeholder="Max bid"
value={maxBid}
onChange={(e) => setMaxBid(e.target.value)}
className="w-32 pl-10 pr-4 py-3 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"
/>
</div>
</div>
</main>
<Footer />
</div>
{/* Table */}
<PremiumTable
data={sortedAuctions}
keyExtractor={(a) => `${a.domain}-${a.platform}`}
loading={loading}
sortBy={sortBy}
sortDirection={sortDirection}
onSort={(key) => handleSort(key as SortField)}
emptyIcon={<Gavel className="w-12 h-12 text-foreground-subtle" />}
emptyTitle={searchQuery ? `No auctions matching "${searchQuery}"` : "No auctions found"}
emptyDescription="Try adjusting your filters or check back later"
columns={[
{
key: 'domain',
header: 'Domain',
sortable: true,
render: (a) => (
<div>
<a
href={a.affiliate_url}
target="_blank"
rel="noopener noreferrer"
className="font-mono font-medium text-foreground hover:text-accent transition-colors"
>
{a.domain}
</a>
<div className="flex items-center gap-2 mt-1 lg:hidden">
<PlatformBadge platform={a.platform} />
{a.age_years && <span className="text-xs text-foreground-subtle">{a.age_years}y</span>}
</div>
</div>
),
},
{
key: 'platform',
header: 'Platform',
hideOnMobile: true,
render: (a) => (
<div className="space-y-1">
<PlatformBadge platform={a.platform} />
{a.age_years && (
<span className="text-xs text-foreground-subtle flex items-center gap-1">
<Clock className="w-3 h-3" /> {a.age_years}y
</span>
)}
</div>
),
},
{
key: 'bid_asc',
header: 'Bid',
sortable: true,
align: 'right',
render: (a) => (
<div>
<span className="font-medium text-foreground">{formatCurrency(a.current_bid)}</span>
{a.buy_now_price && (
<p className="text-xs text-accent">Buy: {formatCurrency(a.buy_now_price)}</p>
)}
</div>
),
},
{
key: 'bids',
header: 'Bids',
sortable: true,
align: 'right',
hideOnMobile: true,
render: (a) => (
<span className={clsx(
"font-medium flex items-center justify-end gap-1",
a.num_bids >= 20 ? "text-accent" : a.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted"
)}>
{a.num_bids}
{a.num_bids >= 20 && <Flame className="w-3 h-3" />}
</span>
),
},
{
key: 'ending',
header: 'Time Left',
sortable: true,
align: 'right',
hideOnMobile: true,
render: (a) => (
<span className={clsx("font-medium", getTimeColor(a.time_remaining))}>
{a.time_remaining}
</span>
),
},
...(activeTab === 'opportunities' ? [{
key: 'score',
header: 'Score',
align: 'center' as const,
render: (a: Auction) => {
const oppData = getOpportunityData(a.domain)
if (!oppData) return null
return (
<span className="inline-flex items-center justify-center w-9 h-9 bg-accent/10 text-accent font-bold rounded-lg">
{oppData.opportunity_score}
</span>
)
},
}] : []),
{
key: 'action',
header: '',
align: 'right',
render: (a) => (
<a
href={a.affiliate_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-4 py-2 bg-foreground text-background text-xs font-medium rounded-lg
hover:bg-foreground/90 transition-all opacity-70 group-hover:opacity-100"
>
Bid <ExternalLink className="w-3 h-3" />
</a>
),
},
]}
/>
</PageContainer>
</CommandCenterLayout>
)
}

View File

@ -4,6 +4,7 @@ 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,
@ -12,9 +13,10 @@ import {
ChevronRight,
Globe,
ArrowUpDown,
ExternalLink,
BarChart3,
DollarSign,
BarChart3,
RefreshCw,
Bell,
} from 'lucide-react'
import clsx from 'clsx'
import Link from 'next/link'
@ -35,6 +37,7 @@ export default function IntelligencePage() {
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)
@ -61,6 +64,12 @@ export default function IntelligencePage() {
}
}
const handleRefresh = async () => {
setRefreshing(true)
await loadTLDData()
setRefreshing(false)
}
// Filter by search
const filteredData = tldData.filter(tld =>
tld.tld.toLowerCase().includes(searchQuery.toLowerCase())
@ -72,81 +81,56 @@ export default function IntelligencePage() {
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>
}
>
<div className="max-w-7xl mx-auto space-y-6">
<PageContainer>
{/* Stats Overview */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="p-5 bg-background-secondary/50 border border-border rounded-xl">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-accent/10 rounded-xl flex items-center justify-center">
<Globe className="w-5 h-5 text-accent" />
</div>
<div>
<p className="text-2xl font-display text-foreground">{total}+</p>
<p className="text-sm text-foreground-muted">TLDs Tracked</p>
</div>
</div>
</div>
<div className="p-5 bg-background-secondary/50 border border-border rounded-xl">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-accent/10 rounded-xl flex items-center justify-center">
<DollarSign className="w-5 h-5 text-accent" />
</div>
<div>
<p className="text-2xl font-display text-foreground">$0.99</p>
<p className="text-sm text-foreground-muted">Lowest Price</p>
</div>
</div>
</div>
<div className="p-5 bg-background-secondary/50 border border-border rounded-xl">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-orange-400/10 rounded-xl flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-orange-400" />
</div>
<div>
<p className="text-2xl font-display text-foreground">.ai</p>
<p className="text-sm text-foreground-muted">Hottest TLD</p>
</div>
</div>
</div>
<div className="p-5 bg-background-secondary/50 border border-border rounded-xl">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-foreground/5 rounded-xl flex items-center justify-center">
<BarChart3 className="w-5 h-5 text-foreground-muted" />
</div>
<div>
<p className="text-2xl font-display text-foreground">24h</p>
<p className="text-sm text-foreground-muted">Update Frequency</p>
</div>
</div>
</div>
<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">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-muted" />
<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-10 pl-10 pr-4 bg-background-secondary border border-border rounded-lg
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"
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-10 pl-4 pr-10 bg-background-secondary border border-border rounded-lg
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"
focus:outline-none focus:border-accent/50"
>
<option value="popularity">By Popularity</option>
<option value="price_asc">Price: Low to High</option>
@ -158,82 +142,91 @@ export default function IntelligencePage() {
</div>
{/* TLD Table */}
{loading ? (
<div className="space-y-3">
{[...Array(10)].map((_, i) => (
<div key={i} className="h-16 bg-background-secondary/50 border border-border rounded-xl animate-pulse" />
))}
</div>
) : (
<div className="overflow-hidden border border-border rounded-xl">
{/* Table Header */}
<div className="hidden lg:grid lg:grid-cols-12 gap-4 p-4 bg-background-secondary/50 border-b border-border text-sm text-foreground-muted font-medium">
<div className="col-span-2">TLD</div>
<div className="col-span-2">Min Price</div>
<div className="col-span-2">Avg Price</div>
<div className="col-span-2">Change</div>
<div className="col-span-3">Cheapest Registrar</div>
<div className="col-span-1"></div>
</div>
{/* Table Rows */}
<div className="divide-y divide-border">
{filteredData.map((tld) => (
<Link
key={tld.tld}
href={`/tld-pricing/${tld.tld}`}
className="block lg:grid lg:grid-cols-12 gap-4 p-4 hover:bg-foreground/5 transition-colors"
>
{/* TLD */}
<div className="col-span-2 flex items-center gap-3 mb-3 lg:mb-0">
<span className="font-mono text-xl font-semibold text-foreground">.{tld.tld}</span>
</div>
{/* Min Price */}
<div className="col-span-2 flex items-center">
<span className="text-foreground font-medium">${tld.min_price.toFixed(2)}</span>
</div>
{/* Avg Price */}
<div className="col-span-2 flex items-center">
<span className="text-foreground-muted">${tld.avg_price.toFixed(2)}</span>
</div>
{/* Change */}
<div className="col-span-2 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>
{/* Registrar */}
<div className="col-span-3 flex items-center">
<span className="text-foreground-muted truncate">{tld.cheapest_registrar}</span>
</div>
{/* Arrow */}
<div className="col-span-1 flex items-center justify-end">
<ChevronRight className="w-5 h-5 text-foreground-subtle" />
</div>
</Link>
))}
</div>
</div>
)}
<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-2">
<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 text-foreground-muted hover:text-foreground
disabled:opacity-50 disabled:cursor-not-allowed"
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>
@ -243,15 +236,14 @@ export default function IntelligencePage() {
<button
onClick={() => setPage(page + 1)}
disabled={(page + 1) * 50 >= total}
className="px-4 py-2 text-sm text-foreground-muted hover:text-foreground
disabled:opacity-50 disabled:cursor-not-allowed"
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>
)}
</div>
</PageContainer>
</CommandCenterLayout>
)
}

View File

@ -0,0 +1,400 @@
'use client'
import { ReactNode, useState, useEffect } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { usePathname, useRouter } from 'next/navigation'
import { useStore } from '@/lib/store'
import { KeyboardShortcutsProvider, useAdminShortcuts, ShortcutHint } from '@/hooks/useKeyboardShortcuts'
import {
Activity,
Users,
Bell,
Mail,
Globe,
Gavel,
BookOpen,
Database,
History,
ChevronLeft,
ChevronRight,
LogOut,
Shield,
LayoutDashboard,
Menu,
X,
Command,
Settings,
} from 'lucide-react'
import clsx from 'clsx'
// ============================================================================
// ADMIN LAYOUT
// ============================================================================
interface AdminLayoutProps {
children: ReactNode
title?: string
subtitle?: string
actions?: ReactNode
activeTab?: string
onTabChange?: (tab: string) => void
}
export function AdminLayout({
children,
title = 'Admin Panel',
subtitle,
actions,
activeTab,
onTabChange,
}: AdminLayoutProps) {
const router = useRouter()
const { user, isAuthenticated, isLoading, checkAuth, logout } = useStore()
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
useEffect(() => {
checkAuth()
}, [checkAuth])
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/login')
}
}, [isLoading, isAuthenticated, router])
useEffect(() => {
const saved = localStorage.getItem('admin-sidebar-collapsed')
if (saved) setSidebarCollapsed(saved === 'true')
}, [])
const toggleCollapsed = () => {
const newState = !sidebarCollapsed
setSidebarCollapsed(newState)
localStorage.setItem('admin-sidebar-collapsed', String(newState))
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
</div>
)
}
if (!isAuthenticated || !user?.is_admin) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center">
<Shield className="w-16 h-16 text-red-400 mx-auto mb-4" />
<h1 className="text-xl font-semibold text-foreground mb-2">Access Denied</h1>
<p className="text-foreground-muted mb-4">Admin privileges required</p>
<button
onClick={() => router.push('/dashboard')}
className="px-4 py-2 bg-accent text-background rounded-lg font-medium"
>
Go to Dashboard
</button>
</div>
</div>
)
}
return (
<KeyboardShortcutsProvider>
<AdminShortcutsWrapper />
<div className="min-h-screen bg-background relative overflow-hidden">
{/* Background Effects */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-[-30%] left-[-10%] w-[800px] h-[800px] bg-red-500/[0.02] rounded-full blur-[120px]" />
<div className="absolute bottom-[-20%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.02] rounded-full blur-[100px]" />
</div>
{/* Admin Sidebar */}
<AdminSidebar
collapsed={sidebarCollapsed}
onCollapse={toggleCollapsed}
mobileOpen={mobileOpen}
onMobileClose={() => setMobileOpen(false)}
user={user}
onLogout={logout}
activeTab={activeTab}
onTabChange={onTabChange}
/>
{/* Mobile Menu Button */}
<button
onClick={() => setMobileOpen(true)}
className="lg:hidden fixed top-4 left-4 z-50 w-11 h-11 bg-background/80 backdrop-blur-xl border border-border
rounded-xl flex items-center justify-center text-foreground-muted hover:text-foreground
transition-all shadow-lg hover:shadow-xl hover:border-red-500/30"
>
<Menu className="w-5 h-5" />
</button>
{/* Main Content */}
<div
className={clsx(
"relative min-h-screen transition-all duration-300",
"lg:ml-[280px]",
sidebarCollapsed && "lg:ml-[80px]",
"ml-0 pt-16 lg:pt-0"
)}
>
{/* Top Bar */}
<header className="sticky top-0 z-30 h-16 sm:h-20 bg-gradient-to-r from-background/90 via-background/80 to-background/90 backdrop-blur-xl border-b border-border/30">
<div className="h-full px-4 sm:px-6 lg:px-8 flex items-center justify-between">
<div className="ml-10 lg:ml-0">
<h1 className="text-lg sm:text-xl lg:text-2xl font-display tracking-tight text-foreground">{title}</h1>
{subtitle && <p className="text-xs sm:text-sm text-foreground-muted mt-0.5">{subtitle}</p>}
</div>
<div className="flex items-center gap-3">
{actions}
<button
onClick={() => {}}
className="hidden sm:flex items-center gap-2 px-3 py-1.5 text-xs text-foreground-subtle hover:text-foreground
bg-foreground/5 rounded-lg border border-border/50 hover:border-border transition-all"
title="Keyboard shortcuts"
>
<Command className="w-3 h-3" />
<span>?</span>
</button>
</div>
</div>
</header>
{/* Page Content */}
<main className="p-4 sm:p-6 lg:p-8">
{children}
</main>
</div>
</div>
</KeyboardShortcutsProvider>
)
}
// ============================================================================
// ADMIN SIDEBAR
// ============================================================================
interface AdminSidebarProps {
collapsed: boolean
onCollapse: () => void
mobileOpen: boolean
onMobileClose: () => void
user: any
onLogout: () => void
activeTab?: string
onTabChange?: (tab: string) => void
}
function AdminSidebar({
collapsed,
onCollapse,
mobileOpen,
onMobileClose,
user,
onLogout,
activeTab,
onTabChange,
}: AdminSidebarProps) {
const pathname = usePathname()
const navItems = [
{ id: 'overview', label: 'Overview', icon: Activity, shortcut: 'O' },
{ id: 'users', label: 'Users', icon: Users, shortcut: 'U' },
{ id: 'alerts', label: 'Price Alerts', icon: Bell },
{ id: 'newsletter', label: 'Newsletter', icon: Mail },
{ id: 'tld', label: 'TLD Data', icon: Globe },
{ id: 'auctions', label: 'Auctions', icon: Gavel },
{ id: 'blog', label: 'Blog', icon: BookOpen, shortcut: 'B' },
{ id: 'system', label: 'System', icon: Database, shortcut: 'Y' },
{ id: 'activity', label: 'Activity Log', icon: History },
]
const SidebarContent = () => (
<>
{/* Logo */}
<div className={clsx(
"h-20 flex items-center border-b border-red-500/20",
collapsed ? "justify-center px-2" : "px-5"
)}>
<Link href="/admin" className="flex items-center gap-3 group">
<div className={clsx(
"relative flex items-center justify-center",
collapsed ? "w-10 h-10" : "w-11 h-11"
)}>
<div className="absolute inset-0 bg-red-500/20 blur-xl rounded-full scale-150 opacity-50 group-hover:opacity-80 transition-opacity" />
<div className="relative w-full h-full bg-gradient-to-br from-red-500 to-red-600 rounded-xl flex items-center justify-center shadow-lg shadow-red-500/20">
<Shield className="w-5 h-5 text-white" />
</div>
</div>
{!collapsed && (
<div>
<span className="text-lg font-bold tracking-wide text-foreground">Admin</span>
<span className="block text-[10px] text-red-400 uppercase tracking-wider">Control Panel</span>
</div>
)}
</Link>
</div>
{/* Navigation */}
<nav className="flex-1 py-6 px-3 space-y-1 overflow-y-auto">
{!collapsed && (
<p className="px-3 mb-3 text-[10px] font-semibold text-foreground-subtle/60 uppercase tracking-[0.15em]">
Management
</p>
)}
{navItems.map((item) => {
const isActive = activeTab === item.id
return (
<button
key={item.id}
onClick={() => onTabChange?.(item.id)}
className={clsx(
"w-full group relative flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
isActive
? "bg-gradient-to-r from-red-500/20 to-red-500/5 text-foreground border border-red-500/20"
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5 border border-transparent"
)}
title={collapsed ? item.label : undefined}
>
{isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-red-500 rounded-r-full" />
)}
<item.icon className={clsx(
"w-5 h-5 transition-colors",
isActive ? "text-red-400" : "group-hover:text-foreground"
)} />
{!collapsed && (
<>
<span className="flex-1 text-left text-sm font-medium">{item.label}</span>
{item.shortcut && <ShortcutHint shortcut={item.shortcut} />}
</>
)}
</button>
)
})}
</nav>
{/* Bottom Section */}
<div className="border-t border-border/30 py-4 px-3 space-y-2">
{/* Back to User Dashboard */}
<Link
href="/dashboard"
className={clsx(
"flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
"text-accent hover:bg-accent/10 border border-transparent hover:border-accent/20"
)}
title={collapsed ? "Back to Dashboard" : undefined}
>
<LayoutDashboard className="w-5 h-5" />
{!collapsed && (
<>
<span className="flex-1 text-sm font-medium">User Dashboard</span>
<ShortcutHint shortcut="D" />
</>
)}
</Link>
{/* User Info */}
{!collapsed && (
<div className="p-4 bg-red-500/5 border border-red-500/20 rounded-xl">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 bg-red-500/20 rounded-lg flex items-center justify-center">
<Shield className="w-4 h-4 text-red-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{user?.name || user?.email?.split('@')[0]}</p>
<p className="text-xs text-red-400">Administrator</p>
</div>
</div>
</div>
)}
{/* Logout */}
<button
onClick={onLogout}
className={clsx(
"w-full flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
"text-foreground-muted hover:text-foreground hover:bg-foreground/5"
)}
title={collapsed ? "Sign out" : undefined}
>
<LogOut className="w-5 h-5" />
{!collapsed && <span className="text-sm font-medium">Sign out</span>}
</button>
</div>
{/* Collapse Toggle */}
<button
onClick={onCollapse}
className={clsx(
"hidden lg:flex absolute -right-3 top-24 w-6 h-6 bg-background border border-border rounded-full",
"items-center justify-center text-foreground-muted hover:text-foreground",
"hover:bg-red-500/10 hover:border-red-500/30 transition-all duration-300 shadow-lg"
)}
>
{collapsed ? <ChevronRight className="w-3.5 h-3.5" /> : <ChevronLeft className="w-3.5 h-3.5" />}
</button>
</>
)
return (
<>
{/* Mobile Overlay */}
{mobileOpen && (
<div
className="lg:hidden fixed inset-0 z-40 bg-background/80 backdrop-blur-sm"
onClick={onMobileClose}
/>
)}
{/* Mobile Sidebar */}
<aside
className={clsx(
"lg:hidden fixed left-0 top-0 bottom-0 z-50 w-[280px] flex flex-col",
"bg-background/95 backdrop-blur-xl border-r border-red-500/20",
"transition-transform duration-300 ease-out",
mobileOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<button
onClick={onMobileClose}
className="absolute top-5 right-4 w-8 h-8 flex items-center justify-center text-foreground-muted hover:text-foreground"
>
<X className="w-5 h-5" />
</button>
<SidebarContent />
</aside>
{/* Desktop Sidebar */}
<aside
className={clsx(
"hidden lg:flex fixed left-0 top-0 bottom-0 z-40 flex-col",
"bg-gradient-to-b from-background/95 via-background/90 to-background/95 backdrop-blur-xl",
"border-r border-red-500/20",
"transition-all duration-300 ease-out",
collapsed ? "w-[80px]" : "w-[280px]"
)}
>
<SidebarContent />
</aside>
</>
)
}
// ============================================================================
// SHORTCUTS WRAPPER
// ============================================================================
function AdminShortcutsWrapper() {
useAdminShortcuts()
return null
}

View File

@ -4,7 +4,8 @@ import { useEffect, useState, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useStore } from '@/lib/store'
import { Sidebar } from './Sidebar'
import { Bell, Search, X } from 'lucide-react'
import { KeyboardShortcutsProvider, useUserShortcuts } from '@/hooks/useKeyboardShortcuts'
import { Bell, Search, X, Command } from 'lucide-react'
import Link from 'next/link'
import clsx from 'clsx'
@ -80,18 +81,20 @@ export function CommandCenterLayout({
}
return (
<div className="min-h-screen bg-background">
{/* Background Effects */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-[-20%] left-1/2 -translate-x-1/2 w-[1200px] h-[800px] bg-accent/[0.02] rounded-full blur-[120px]" />
<div className="absolute bottom-[-10%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.015] rounded-full blur-[100px]" />
</div>
<KeyboardShortcutsProvider>
<UserShortcutsWrapper />
<div className="min-h-screen bg-background">
{/* Background Effects */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-[-20%] left-1/2 -translate-x-1/2 w-[1200px] h-[800px] bg-accent/[0.02] rounded-full blur-[120px]" />
<div className="absolute bottom-[-10%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.015] rounded-full blur-[100px]" />
</div>
{/* Sidebar */}
<Sidebar
collapsed={sidebarCollapsed}
onCollapsedChange={setSidebarCollapsed}
/>
{/* Sidebar */}
<Sidebar
collapsed={sidebarCollapsed}
onCollapsedChange={setSidebarCollapsed}
/>
{/* Main Content Area */}
<div
@ -207,6 +210,17 @@ export function CommandCenterLayout({
)}
</div>
{/* Keyboard Shortcuts Hint */}
<button
onClick={() => {}}
className="hidden sm:flex items-center gap-1.5 px-2.5 py-1.5 text-xs text-foreground-subtle hover:text-foreground
bg-foreground/5 rounded-lg border border-border/50 hover:border-border transition-all"
title="Keyboard shortcuts (?)"
>
<Command className="w-3 h-3" />
<span>?</span>
</button>
{/* Custom Actions */}
{actions}
</div>
@ -255,9 +269,10 @@ export function CommandCenterLayout({
</div>
)}
{/* Keyboard shortcut for search */}
<KeyboardShortcut onTrigger={() => setSearchOpen(true)} keys={['Meta', 'k']} />
</div>
{/* Keyboard shortcut for search */}
<KeyboardShortcut onTrigger={() => setSearchOpen(true)} keys={['Meta', 'k']} />
</div>
</KeyboardShortcutsProvider>
)
}
@ -276,3 +291,9 @@ function KeyboardShortcut({ onTrigger, keys }: { onTrigger: () => void, keys: st
return null
}
// User shortcuts wrapper
function UserShortcutsWrapper() {
useUserShortcuts()
return null
}

View File

@ -0,0 +1,423 @@
'use client'
import { ReactNode } from 'react'
import clsx from 'clsx'
import { ChevronUp, ChevronDown, ChevronsUpDown, Loader2 } from 'lucide-react'
// ============================================================================
// PREMIUM TABLE - Elegant, consistent styling for all tables
// ============================================================================
interface Column<T> {
key: string
header: string | ReactNode
render?: (item: T, index: number) => ReactNode
className?: string
headerClassName?: string
hideOnMobile?: boolean
hideOnTablet?: boolean
sortable?: boolean
align?: 'left' | 'center' | 'right'
width?: string
}
interface PremiumTableProps<T> {
data: T[]
columns: Column<T>[]
keyExtractor: (item: T) => string | number
onRowClick?: (item: T) => void
emptyState?: ReactNode
emptyIcon?: ReactNode
emptyTitle?: string
emptyDescription?: string
loading?: boolean
sortBy?: string
sortDirection?: 'asc' | 'desc'
onSort?: (key: string) => void
compact?: boolean
striped?: boolean
hoverable?: boolean
}
export function PremiumTable<T>({
data,
columns,
keyExtractor,
onRowClick,
emptyState,
emptyIcon,
emptyTitle = 'No data',
emptyDescription,
loading,
sortBy,
sortDirection = 'asc',
onSort,
compact = false,
striped = false,
hoverable = true,
}: PremiumTableProps<T>) {
const cellPadding = compact ? 'px-4 py-3' : 'px-6 py-4'
const headerPadding = compact ? 'px-4 py-3' : 'px-6 py-4'
if (loading) {
return (
<div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm">
<div className="divide-y divide-border/20">
{[...Array(5)].map((_, i) => (
<div key={i} className={clsx("flex gap-4 items-center", cellPadding)} style={{ animationDelay: `${i * 50}ms` }}>
<div className="h-5 w-32 bg-foreground/5 rounded-lg animate-pulse" />
<div className="h-5 w-24 bg-foreground/5 rounded-lg animate-pulse hidden sm:block" />
<div className="h-5 w-20 bg-foreground/5 rounded-lg animate-pulse ml-auto" />
</div>
))}
</div>
</div>
)
}
if (data.length === 0) {
return (
<div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm">
<div className="px-8 py-16 text-center">
{emptyState || (
<>
{emptyIcon && <div className="flex justify-center mb-4">{emptyIcon}</div>}
<p className="text-foreground-muted font-medium">{emptyTitle}</p>
{emptyDescription && <p className="text-sm text-foreground-subtle mt-1">{emptyDescription}</p>}
</>
)}
</div>
</div>
)
}
return (
<div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm shadow-[0_4px_24px_-4px_rgba(0,0,0,0.08)]">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border/40 bg-background-secondary/30">
{columns.map((col) => (
<th
key={col.key}
className={clsx(
headerPadding,
"text-[11px] font-semibold text-foreground-subtle/70 uppercase tracking-wider",
col.hideOnMobile && "hidden md:table-cell",
col.hideOnTablet && "hidden lg:table-cell",
col.align === 'right' && "text-right",
col.align === 'center' && "text-center",
col.headerClassName
)}
style={col.width ? { width: col.width } : undefined}
>
{col.sortable && onSort ? (
<button
onClick={() => onSort(col.key)}
className="flex items-center gap-1.5 hover:text-foreground transition-colors group"
>
{col.header}
<SortIndicator
active={sortBy === col.key}
direction={sortBy === col.key ? sortDirection : undefined}
/>
</button>
) : (
col.header
)}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-border/20">
{data.map((item, index) => {
const key = keyExtractor(item)
return (
<tr
key={key}
onClick={() => onRowClick?.(item)}
className={clsx(
"group transition-all duration-200",
onRowClick && "cursor-pointer",
hoverable && "hover:bg-foreground/[0.02]",
striped && index % 2 === 1 && "bg-foreground/[0.01]"
)}
>
{columns.map((col) => (
<td
key={col.key}
className={clsx(
cellPadding,
"text-sm",
col.hideOnMobile && "hidden md:table-cell",
col.hideOnTablet && "hidden lg:table-cell",
col.align === 'right' && "text-right",
col.align === 'center' && "text-center",
col.className
)}
>
{col.render
? col.render(item, index)
: (item as Record<string, unknown>)[col.key] as ReactNode
}
</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)
}
// ============================================================================
// SORT INDICATOR
// ============================================================================
function SortIndicator({ active, direction }: { active: boolean; direction?: 'asc' | 'desc' }) {
if (!active) {
return <ChevronsUpDown className="w-3.5 h-3.5 text-foreground-subtle/50 group-hover:text-foreground-muted transition-colors" />
}
return direction === 'asc'
? <ChevronUp className="w-3.5 h-3.5 text-accent" />
: <ChevronDown className="w-3.5 h-3.5 text-accent" />
}
// ============================================================================
// STATUS BADGE
// ============================================================================
type BadgeVariant = 'default' | 'success' | 'warning' | 'error' | 'accent' | 'info'
export function Badge({
children,
variant = 'default',
size = 'sm',
dot = false,
pulse = false,
}: {
children: ReactNode
variant?: BadgeVariant
size?: 'xs' | 'sm' | 'md'
dot?: boolean
pulse?: boolean
}) {
const variants: Record<BadgeVariant, string> = {
default: "bg-foreground/5 text-foreground-muted border-border/50",
success: "bg-accent/10 text-accent border-accent/20",
warning: "bg-amber-500/10 text-amber-400 border-amber-500/20",
error: "bg-red-500/10 text-red-400 border-red-500/20",
accent: "bg-accent/10 text-accent border-accent/20",
info: "bg-blue-500/10 text-blue-400 border-blue-500/20",
}
const sizes = {
xs: "text-[10px] px-1.5 py-0.5",
sm: "text-xs px-2 py-0.5",
md: "text-xs px-2.5 py-1",
}
return (
<span className={clsx(
"inline-flex items-center gap-1.5 font-medium rounded-md border",
variants[variant],
sizes[size]
)}>
{dot && (
<span className="relative flex h-2 w-2">
{pulse && (
<span className={clsx(
"animate-ping absolute inline-flex h-full w-full rounded-full opacity-75",
variant === 'success' || variant === 'accent' ? "bg-accent" :
variant === 'warning' ? "bg-amber-400" :
variant === 'error' ? "bg-red-400" : "bg-foreground"
)} />
)}
<span className={clsx(
"relative inline-flex rounded-full h-2 w-2",
variant === 'success' || variant === 'accent' ? "bg-accent" :
variant === 'warning' ? "bg-amber-400" :
variant === 'error' ? "bg-red-400" :
variant === 'info' ? "bg-blue-400" : "bg-foreground-muted"
)} />
</span>
)}
{children}
</span>
)
}
// ============================================================================
// TABLE ACTION BUTTON
// ============================================================================
export function TableActionButton({
icon: Icon,
onClick,
variant = 'default',
title,
disabled,
loading,
}: {
icon: React.ComponentType<{ className?: string }>
onClick?: () => void
variant?: 'default' | 'danger' | 'accent'
title?: string
disabled?: boolean
loading?: boolean
}) {
const variants = {
default: "text-foreground-muted hover:text-foreground hover:bg-foreground/5 border-transparent",
danger: "text-foreground-muted hover:text-red-400 hover:bg-red-500/10 border-transparent hover:border-red-500/20",
accent: "text-accent bg-accent/10 border-accent/20 hover:bg-accent/20",
}
return (
<button
onClick={(e) => {
e.stopPropagation()
onClick?.()
}}
disabled={disabled || loading}
title={title}
className={clsx(
"p-2 rounded-lg border transition-all duration-200",
"disabled:opacity-30 disabled:cursor-not-allowed",
variants[variant]
)}
>
{loading ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Icon className="w-4 h-4" />
)}
</button>
)
}
// ============================================================================
// PLATFORM BADGE (for auctions)
// ============================================================================
export function PlatformBadge({ platform }: { platform: string }) {
const colors: Record<string, string> = {
'GoDaddy': 'text-blue-400 bg-blue-400/10 border-blue-400/20',
'Sedo': 'text-orange-400 bg-orange-400/10 border-orange-400/20',
'NameJet': 'text-purple-400 bg-purple-400/10 border-purple-400/20',
'DropCatch': 'text-teal-400 bg-teal-400/10 border-teal-400/20',
'ExpiredDomains': 'text-pink-400 bg-pink-400/10 border-pink-400/20',
}
return (
<span className={clsx(
"inline-flex items-center text-xs font-medium px-2 py-0.5 rounded-md border",
colors[platform] || "text-foreground-muted bg-foreground/5 border-border/50"
)}>
{platform}
</span>
)
}
// ============================================================================
// STAT CARD (for page headers)
// ============================================================================
export function StatCard({
title,
value,
subtitle,
icon: Icon,
accent = false,
trend,
}: {
title: string
value: string | number
subtitle?: string
icon?: React.ComponentType<{ className?: string }>
accent?: boolean
trend?: { value: number; label?: string }
}) {
return (
<div className={clsx(
"relative p-5 rounded-2xl border overflow-hidden transition-all duration-300",
accent
? "bg-gradient-to-br from-accent/15 to-accent/5 border-accent/30"
: "bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border-border/50 hover:border-accent/30"
)}>
{accent && <div className="absolute top-0 right-0 w-20 h-20 bg-accent/10 rounded-full blur-2xl" />}
<div className="relative">
{Icon && (
<div className={clsx(
"w-10 h-10 rounded-xl flex items-center justify-center mb-3",
accent ? "bg-accent/20 border border-accent/30" : "bg-foreground/5 border border-border/30"
)}>
<Icon className={clsx("w-5 h-5", accent ? "text-accent" : "text-foreground-muted")} />
</div>
)}
<p className="text-[10px] text-foreground-subtle uppercase tracking-wider mb-1">{title}</p>
<p className={clsx("text-2xl font-display", accent ? "text-accent" : "text-foreground")}>
{typeof value === 'number' ? value.toLocaleString() : value}
</p>
{subtitle && <p className="text-xs text-foreground-subtle mt-0.5">{subtitle}</p>}
{trend && (
<div className={clsx(
"inline-flex items-center gap-1 mt-2 text-xs font-medium px-2 py-0.5 rounded",
trend.value > 0 ? "text-accent bg-accent/10" : trend.value < 0 ? "text-red-400 bg-red-400/10" : "text-foreground-muted bg-foreground/5"
)}>
{trend.value > 0 ? '+' : ''}{trend.value}%
{trend.label && <span className="text-foreground-subtle">{trend.label}</span>}
</div>
)}
</div>
</div>
)
}
// ============================================================================
// PAGE CONTAINER (consistent max-width)
// ============================================================================
export function PageContainer({ children, className }: { children: ReactNode; className?: string }) {
return (
<div className={clsx("max-w-7xl mx-auto space-y-6", className)}>
{children}
</div>
)
}
// ============================================================================
// SECTION HEADER
// ============================================================================
export function SectionHeader({
title,
subtitle,
icon: Icon,
action,
}: {
title: string
subtitle?: string
icon?: React.ComponentType<{ className?: string }>
action?: ReactNode
}) {
return (
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
{Icon && (
<div className="w-10 h-10 bg-accent/10 border border-accent/20 rounded-xl flex items-center justify-center">
<Icon className="w-5 h-5 text-accent" />
</div>
)}
<div>
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
{subtitle && <p className="text-sm text-foreground-muted">{subtitle}</p>}
</div>
</div>
{action}
</div>
)
}

View File

@ -0,0 +1,311 @@
'use client'
import { useEffect, useCallback, useState, createContext, useContext, ReactNode } from 'react'
import { useRouter } from 'next/navigation'
import { X, Command, Search } from 'lucide-react'
import clsx from 'clsx'
// ============================================================================
// TYPES
// ============================================================================
interface Shortcut {
key: string
label: string
description: string
action: () => void
category: 'navigation' | 'actions' | 'global'
requiresModifier?: boolean
}
interface KeyboardShortcutsContextType {
shortcuts: Shortcut[]
registerShortcut: (shortcut: Shortcut) => void
unregisterShortcut: (key: string) => void
showHelp: boolean
setShowHelp: (show: boolean) => void
}
// ============================================================================
// CONTEXT
// ============================================================================
const KeyboardShortcutsContext = createContext<KeyboardShortcutsContextType | null>(null)
export function useKeyboardShortcuts() {
const context = useContext(KeyboardShortcutsContext)
if (!context) {
throw new Error('useKeyboardShortcuts must be used within KeyboardShortcutsProvider')
}
return context
}
// ============================================================================
// PROVIDER
// ============================================================================
export function KeyboardShortcutsProvider({
children,
shortcuts: defaultShortcuts = [],
}: {
children: ReactNode
shortcuts?: Shortcut[]
}) {
const router = useRouter()
const [shortcuts, setShortcuts] = useState<Shortcut[]>(defaultShortcuts)
const [showHelp, setShowHelp] = useState(false)
const registerShortcut = useCallback((shortcut: Shortcut) => {
setShortcuts(prev => {
const existing = prev.find(s => s.key === shortcut.key)
if (existing) return prev
return [...prev, shortcut]
})
}, [])
const unregisterShortcut = useCallback((key: string) => {
setShortcuts(prev => prev.filter(s => s.key !== key))
}, [])
// Handle keyboard events
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if user is typing in an input
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement ||
(e.target as HTMLElement)?.isContentEditable
) {
return
}
// Show help with ?
if (e.key === '?' && !e.metaKey && !e.ctrlKey) {
e.preventDefault()
setShowHelp(true)
return
}
// Close help with Escape
if (e.key === 'Escape' && showHelp) {
e.preventDefault()
setShowHelp(false)
return
}
// Find matching shortcut
const shortcut = shortcuts.find(s => {
if (s.requiresModifier) {
return (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === s.key.toLowerCase()
}
return e.key.toLowerCase() === s.key.toLowerCase() && !e.metaKey && !e.ctrlKey
})
if (shortcut) {
e.preventDefault()
shortcut.action()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [shortcuts, showHelp])
return (
<KeyboardShortcutsContext.Provider value={{ shortcuts, registerShortcut, unregisterShortcut, showHelp, setShowHelp }}>
{children}
{showHelp && <ShortcutsModal shortcuts={shortcuts} onClose={() => setShowHelp(false)} />}
</KeyboardShortcutsContext.Provider>
)
}
// ============================================================================
// SHORTCUTS MODAL
// ============================================================================
function ShortcutsModal({ shortcuts, onClose }: { shortcuts: Shortcut[]; onClose: () => void }) {
const categories = {
navigation: shortcuts.filter(s => s.category === 'navigation'),
actions: shortcuts.filter(s => s.category === 'actions'),
global: shortcuts.filter(s => s.category === 'global'),
}
return (
<div
className="fixed inset-0 z-[100] bg-background/80 backdrop-blur-sm flex items-center justify-center p-4"
onClick={onClose}
>
<div
className="w-full max-w-lg bg-background border border-border/50 rounded-2xl shadow-2xl overflow-hidden"
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50 bg-background-secondary/50">
<div className="flex items-center gap-3">
<div className="w-9 h-9 bg-accent/10 rounded-xl flex items-center justify-center">
<Command className="w-4 h-4 text-accent" />
</div>
<h2 className="text-lg font-semibold text-foreground">Keyboard Shortcuts</h2>
</div>
<button
onClick={onClose}
className="p-2 text-foreground-muted hover:text-foreground rounded-lg hover:bg-foreground/5 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6 max-h-[60vh] overflow-y-auto space-y-6">
{/* Navigation */}
{categories.navigation.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">Navigation</h3>
<div className="space-y-2">
{categories.navigation.map(shortcut => (
<ShortcutRow key={shortcut.key} shortcut={shortcut} />
))}
</div>
</div>
)}
{/* Actions */}
{categories.actions.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">Actions</h3>
<div className="space-y-2">
{categories.actions.map(shortcut => (
<ShortcutRow key={shortcut.key} shortcut={shortcut} />
))}
</div>
</div>
)}
{/* Global */}
{categories.global.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">Global</h3>
<div className="space-y-2">
{categories.global.map(shortcut => (
<ShortcutRow key={shortcut.key} shortcut={shortcut} />
))}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="px-6 py-3 border-t border-border/50 bg-background-secondary/30">
<p className="text-xs text-foreground-subtle text-center">
Press <kbd className="px-1.5 py-0.5 bg-foreground/10 rounded text-foreground-muted">?</kbd> anytime to show this help
</p>
</div>
</div>
</div>
)
}
function ShortcutRow({ shortcut }: { shortcut: Shortcut }) {
return (
<div className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-foreground/5 transition-colors">
<div>
<p className="text-sm font-medium text-foreground">{shortcut.label}</p>
<p className="text-xs text-foreground-subtle">{shortcut.description}</p>
</div>
<div className="flex items-center gap-1">
{shortcut.requiresModifier && (
<>
<kbd className="px-2 py-1 bg-foreground/10 rounded text-xs font-mono text-foreground-muted"></kbd>
<span className="text-foreground-subtle">+</span>
</>
)}
<kbd className="px-2 py-1 bg-foreground/10 rounded text-xs font-mono text-foreground-muted uppercase">
{shortcut.key}
</kbd>
</div>
</div>
)
}
// ============================================================================
// USER BACKEND SHORTCUTS
// ============================================================================
export function useUserShortcuts() {
const router = useRouter()
const { registerShortcut, unregisterShortcut, setShowHelp } = useKeyboardShortcuts()
useEffect(() => {
const userShortcuts: Shortcut[] = [
// Navigation
{ key: 'g', label: 'Go to Dashboard', description: 'Navigate to dashboard', action: () => router.push('/dashboard'), category: 'navigation' },
{ key: 'w', label: 'Go to Watchlist', description: 'Navigate to watchlist', action: () => router.push('/watchlist'), category: 'navigation' },
{ key: 'p', label: 'Go to Portfolio', description: 'Navigate to portfolio', action: () => router.push('/portfolio'), category: 'navigation' },
{ key: 'a', label: 'Go to Auctions', description: 'Navigate to auctions', action: () => router.push('/auctions'), category: 'navigation' },
{ key: 'i', label: 'Go to Intelligence', description: 'Navigate to TLD intelligence', action: () => router.push('/intelligence'), category: 'navigation' },
{ key: 's', label: 'Go to Settings', description: 'Navigate to settings', action: () => router.push('/settings'), category: 'navigation' },
// Actions
{ key: 'n', label: 'Add Domain', description: 'Quick add a new domain', action: () => document.querySelector<HTMLInputElement>('input[placeholder*="domain"]')?.focus(), category: 'actions' },
{ key: 'k', label: 'Search', description: 'Focus search input', action: () => document.querySelector<HTMLInputElement>('input[type="text"]')?.focus(), category: 'actions', requiresModifier: true },
// Global
{ key: '?', label: 'Show Shortcuts', description: 'Display this help', action: () => setShowHelp(true), category: 'global' },
{ key: 'Escape', label: 'Close Modal', description: 'Close any open modal', action: () => {}, category: 'global' },
]
userShortcuts.forEach(registerShortcut)
return () => {
userShortcuts.forEach(s => unregisterShortcut(s.key))
}
}, [router, registerShortcut, unregisterShortcut, setShowHelp])
}
// ============================================================================
// ADMIN SHORTCUTS
// ============================================================================
export function useAdminShortcuts() {
const router = useRouter()
const { registerShortcut, unregisterShortcut, setShowHelp } = useKeyboardShortcuts()
useEffect(() => {
const adminShortcuts: Shortcut[] = [
// Navigation
{ key: 'o', label: 'Overview', description: 'Go to admin overview', action: () => {}, category: 'navigation' },
{ key: 'u', label: 'Users', description: 'Go to users management', action: () => {}, category: 'navigation' },
{ key: 'b', label: 'Blog', description: 'Go to blog management', action: () => {}, category: 'navigation' },
{ key: 'y', label: 'System', description: 'Go to system status', action: () => {}, category: 'navigation' },
// Actions
{ key: 'r', label: 'Refresh Data', description: 'Refresh current data', action: () => window.location.reload(), category: 'actions' },
{ key: 'e', label: 'Export', description: 'Export current data', action: () => {}, category: 'actions' },
// Global
{ key: '?', label: 'Show Shortcuts', description: 'Display this help', action: () => setShowHelp(true), category: 'global' },
{ key: 'd', label: 'Back to Dashboard', description: 'Return to user dashboard', action: () => router.push('/dashboard'), category: 'global' },
]
adminShortcuts.forEach(registerShortcut)
return () => {
adminShortcuts.forEach(s => unregisterShortcut(s.key))
}
}, [router, registerShortcut, unregisterShortcut, setShowHelp])
}
// ============================================================================
// SHORTCUT HINT COMPONENT
// ============================================================================
export function ShortcutHint({ shortcut, className }: { shortcut: string; className?: string }) {
return (
<kbd className={clsx(
"hidden sm:inline-flex items-center justify-center",
"px-1.5 py-0.5 text-[10px] font-mono uppercase",
"bg-foreground/5 text-foreground-subtle border border-border/50 rounded",
className
)}>
{shortcut}
</kbd>
)
}