- Removed old folders: dashboard, pricing, auctions, marketplace, portfolio, alerts, seo - Removed CommandCenterLayout.tsx (replaced by TerminalLayout) - Fixed all internal links to use new terminal routes - Updated keyboard shortcuts for new module names - Fixed welcome page next steps - Fixed landing page feature links - Fixed radar page stat cards and links
404 lines
16 KiB
TypeScript
404 lines
16 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState, useMemo, useCallback } from 'react'
|
|
import { useSearchParams } from 'next/navigation'
|
|
import { useStore } from '@/lib/store'
|
|
import { api } from '@/lib/api'
|
|
import { TerminalLayout } from '@/components/TerminalLayout'
|
|
import { PremiumTable, StatCard, PageContainer, Badge, SectionHeader, SearchInput, ActionButton } from '@/components/PremiumTable'
|
|
import { Toast, useToast } from '@/components/Toast'
|
|
import {
|
|
Eye,
|
|
Briefcase,
|
|
TrendingUp,
|
|
Gavel,
|
|
Clock,
|
|
ExternalLink,
|
|
Sparkles,
|
|
ChevronRight,
|
|
Plus,
|
|
Zap,
|
|
Crown,
|
|
Activity,
|
|
Loader2,
|
|
Search,
|
|
Bell,
|
|
} from 'lucide-react'
|
|
import clsx from 'clsx'
|
|
import Link from 'next/link'
|
|
|
|
interface HotAuction {
|
|
domain: string
|
|
current_bid: number
|
|
time_remaining: string
|
|
platform: string
|
|
affiliate_url?: string
|
|
}
|
|
|
|
interface TrendingTld {
|
|
tld: string
|
|
current_price: number
|
|
price_change: number
|
|
reason: string
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const searchParams = useSearchParams()
|
|
const {
|
|
isAuthenticated,
|
|
isLoading,
|
|
user,
|
|
domains,
|
|
subscription
|
|
} = useStore()
|
|
|
|
const { toast, showToast, hideToast } = useToast()
|
|
const [hotAuctions, setHotAuctions] = useState<HotAuction[]>([])
|
|
const [trendingTlds, setTrendingTlds] = useState<TrendingTld[]>([])
|
|
const [loadingAuctions, setLoadingAuctions] = useState(true)
|
|
const [loadingTlds, setLoadingTlds] = useState(true)
|
|
const [quickDomain, setQuickDomain] = useState('')
|
|
const [addingDomain, setAddingDomain] = useState(false)
|
|
|
|
// Check for upgrade success
|
|
useEffect(() => {
|
|
if (searchParams.get('upgraded') === 'true') {
|
|
showToast('Welcome to your upgraded plan! 🎉', 'success')
|
|
window.history.replaceState({}, '', '/terminal/radar')
|
|
}
|
|
}, [searchParams])
|
|
|
|
const loadDashboardData = useCallback(async () => {
|
|
try {
|
|
const [auctions, trending] = await Promise.all([
|
|
api.getEndingSoonAuctions(5).catch(() => []),
|
|
api.getTrendingTlds().catch(() => ({ trending: [] }))
|
|
])
|
|
setHotAuctions(auctions.slice(0, 5))
|
|
setTrendingTlds(trending.trending?.slice(0, 4) || [])
|
|
} catch (error) {
|
|
console.error('Failed to load dashboard data:', error)
|
|
} finally {
|
|
setLoadingAuctions(false)
|
|
setLoadingTlds(false)
|
|
}
|
|
}, [])
|
|
|
|
// Load dashboard data
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
loadDashboardData()
|
|
}
|
|
}, [isAuthenticated, loadDashboardData])
|
|
|
|
const handleQuickAdd = useCallback(async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!quickDomain.trim()) return
|
|
|
|
setAddingDomain(true)
|
|
try {
|
|
const store = useStore.getState()
|
|
await store.addDomain(quickDomain.trim())
|
|
setQuickDomain('')
|
|
showToast(`Added ${quickDomain.trim()} to watchlist`, 'success')
|
|
} catch (err: any) {
|
|
showToast(err.message || 'Failed to add domain', 'error')
|
|
} finally {
|
|
setAddingDomain(false)
|
|
}
|
|
}, [quickDomain, showToast])
|
|
|
|
// Memoized computed values
|
|
const { availableDomains, totalDomains, tierName, TierIcon, greeting, subtitle } = useMemo(() => {
|
|
const availableDomains = domains?.filter(d => d.is_available) || []
|
|
const totalDomains = domains?.length || 0
|
|
const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
|
|
const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
|
|
|
|
const hour = new Date().getHours()
|
|
const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening'
|
|
|
|
let subtitle = ''
|
|
if (availableDomains.length > 0) {
|
|
subtitle = `${availableDomains.length} domain${availableDomains.length !== 1 ? 's' : ''} ready to pounce!`
|
|
} else if (totalDomains > 0) {
|
|
subtitle = `Monitoring ${totalDomains} domain${totalDomains !== 1 ? 's' : ''} for you`
|
|
} else {
|
|
subtitle = 'Start tracking domains to find opportunities'
|
|
}
|
|
|
|
return { availableDomains, totalDomains, tierName, TierIcon, greeting, subtitle }
|
|
}, [domains, subscription])
|
|
|
|
if (isLoading || !isAuthenticated) {
|
|
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>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<TerminalLayout
|
|
title={`${greeting}${user?.name ? `, ${user.name.split(' ')[0]}` : ''}`}
|
|
subtitle={subtitle}
|
|
>
|
|
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
|
|
|
|
<PageContainer>
|
|
{/* Quick Add */}
|
|
<div className="relative p-5 sm:p-6 bg-gradient-to-r from-accent/10 via-accent/5 to-transparent border border-accent/20 rounded-2xl overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
|
|
<div className="relative">
|
|
<h2 className="text-base font-semibold text-foreground mb-4 flex items-center gap-2">
|
|
<div className="w-8 h-8 bg-accent/20 rounded-lg flex items-center justify-center">
|
|
<Search className="w-4 h-4 text-accent" />
|
|
</div>
|
|
Quick Add to Watchlist
|
|
</h2>
|
|
<form onSubmit={handleQuickAdd} className="flex flex-col sm:flex-row gap-3">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-subtle" />
|
|
<input
|
|
type="text"
|
|
value={quickDomain}
|
|
onChange={(e) => setQuickDomain(e.target.value)}
|
|
placeholder="Enter domain to track (e.g., dream.com)"
|
|
className="w-full h-11 pl-11 pr-4 bg-background/80 backdrop-blur-sm border border-border/50 rounded-xl
|
|
text-sm text-foreground placeholder:text-foreground-subtle
|
|
focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={addingDomain || !quickDomain.trim()}
|
|
className="flex items-center justify-center gap-2 h-11 px-6 bg-gradient-to-r from-accent to-accent/80 text-background rounded-xl
|
|
font-medium hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] transition-all
|
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Add</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Overview */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<Link href="/terminal/watchlist" className="group">
|
|
<StatCard
|
|
title="Domains Watched"
|
|
value={totalDomains}
|
|
icon={Eye}
|
|
/>
|
|
</Link>
|
|
<Link href="/terminal/watchlist?filter=available" className="group">
|
|
<StatCard
|
|
title="Available Now"
|
|
value={availableDomains.length}
|
|
icon={Sparkles}
|
|
accent={availableDomains.length > 0}
|
|
/>
|
|
</Link>
|
|
<Link href="/terminal/watchlist" className="group">
|
|
<StatCard
|
|
title="Watchlist"
|
|
value={totalDomains}
|
|
icon={Briefcase}
|
|
/>
|
|
</Link>
|
|
<StatCard
|
|
title="Plan"
|
|
value={tierName}
|
|
subtitle={`${subscription?.domains_used || 0}/${subscription?.domain_limit || 5} slots`}
|
|
icon={TierIcon}
|
|
/>
|
|
</div>
|
|
|
|
{/* Activity Feed + Market Pulse */}
|
|
<div className="grid lg:grid-cols-2 gap-6">
|
|
{/* Activity Feed */}
|
|
<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="p-5 border-b border-border/30">
|
|
<SectionHeader
|
|
title="Activity Feed"
|
|
icon={Activity}
|
|
compact
|
|
action={
|
|
<Link href="/terminal/watchlist" className="text-sm text-accent hover:text-accent/80 transition-colors">
|
|
View all →
|
|
</Link>
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="p-5">
|
|
{availableDomains.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{availableDomains.slice(0, 4).map((domain) => (
|
|
<div
|
|
key={domain.id}
|
|
className="flex items-center gap-4 p-3 bg-accent/5 border border-accent/20 rounded-xl"
|
|
>
|
|
<div className="relative">
|
|
<span className="w-3 h-3 bg-accent rounded-full block" />
|
|
<span className="absolute inset-0 bg-accent rounded-full animate-ping opacity-50" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-foreground truncate">{domain.name}</p>
|
|
<p className="text-xs text-accent">Available for registration!</p>
|
|
</div>
|
|
<a
|
|
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs font-medium text-accent hover:underline flex items-center gap-1"
|
|
>
|
|
Register <ExternalLink className="w-3 h-3" />
|
|
</a>
|
|
</div>
|
|
))}
|
|
{availableDomains.length > 4 && (
|
|
<p className="text-center text-sm text-foreground-muted">
|
|
+{availableDomains.length - 4} more available
|
|
</p>
|
|
)}
|
|
</div>
|
|
) : totalDomains > 0 ? (
|
|
<div className="text-center py-8">
|
|
<Bell className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
|
|
<p className="text-foreground-muted">All domains are still registered</p>
|
|
<p className="text-sm text-foreground-subtle mt-1">
|
|
We're monitoring {totalDomains} domains for you
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<Plus className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
|
|
<p className="text-foreground-muted">No domains tracked yet</p>
|
|
<p className="text-sm text-foreground-subtle mt-1">
|
|
Add a domain above to start monitoring
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Market Pulse */}
|
|
<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="p-5 border-b border-border/30">
|
|
<SectionHeader
|
|
title="Market Pulse"
|
|
icon={Gavel}
|
|
compact
|
|
action={
|
|
<Link href="/terminal/market" className="text-sm text-accent hover:text-accent/80 transition-colors">
|
|
View all →
|
|
</Link>
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="p-5">
|
|
{loadingAuctions ? (
|
|
<div className="space-y-3">
|
|
{[...Array(4)].map((_, i) => (
|
|
<div key={i} className="h-14 bg-foreground/5 rounded-xl animate-pulse" />
|
|
))}
|
|
</div>
|
|
) : hotAuctions.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{hotAuctions.map((auction, idx) => (
|
|
<a
|
|
key={`${auction.domain}-${idx}`}
|
|
href={auction.affiliate_url || '#'}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-4 p-3 bg-foreground/5 rounded-xl
|
|
hover:bg-foreground/10 transition-colors group"
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-foreground truncate">{auction.domain}</p>
|
|
<p className="text-xs text-foreground-muted flex items-center gap-2">
|
|
<Clock className="w-3 h-3" />
|
|
{auction.time_remaining}
|
|
<span className="text-foreground-subtle">• {auction.platform}</span>
|
|
</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm font-semibold text-foreground">${auction.current_bid}</p>
|
|
<p className="text-xs text-foreground-subtle">current bid</p>
|
|
</div>
|
|
<ExternalLink className="w-4 h-4 text-foreground-subtle group-hover:text-foreground" />
|
|
</a>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<Gavel className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
|
|
<p className="text-foreground-muted">No auctions ending soon</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Trending TLDs */}
|
|
<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="p-5 border-b border-border/30">
|
|
<SectionHeader
|
|
title="Trending TLDs"
|
|
icon={TrendingUp}
|
|
compact
|
|
action={
|
|
<Link href="/terminal/intel" className="text-sm text-accent hover:text-accent/80 transition-colors">
|
|
View all →
|
|
</Link>
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="p-5">
|
|
{loadingTlds ? (
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{[...Array(4)].map((_, i) => (
|
|
<div key={i} className="h-24 bg-foreground/5 rounded-xl animate-pulse" />
|
|
))}
|
|
</div>
|
|
) : trendingTlds.length > 0 ? (
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{trendingTlds.map((tld) => (
|
|
<Link
|
|
key={tld.tld}
|
|
href={`/tld-pricing/${tld.tld}`}
|
|
className="group relative p-4 bg-foreground/5 border border-border/30 rounded-xl
|
|
hover:border-accent/30 transition-all duration-300 overflow-hidden"
|
|
>
|
|
<div className="absolute inset-0 bg-accent/5 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
<div className="relative">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="font-mono text-2xl font-semibold text-foreground group-hover:text-accent transition-colors">.{tld.tld}</span>
|
|
<span className={clsx(
|
|
"text-xs font-bold px-2.5 py-1 rounded-lg border",
|
|
(tld.price_change || 0) > 0
|
|
? "text-orange-400 bg-orange-400/10 border-orange-400/20"
|
|
: "text-accent bg-accent/10 border-accent/20"
|
|
)}>
|
|
{(tld.price_change || 0) > 0 ? '+' : ''}{(tld.price_change || 0).toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-foreground-muted truncate">{tld.reason}</p>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<TrendingUp className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
|
|
<p className="text-foreground-muted">No trending TLDs available</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</PageContainer>
|
|
</TerminalLayout>
|
|
)
|
|
}
|