Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
- New Sidebar component with collapsible navigation - New CommandCenterLayout for logged-in users - Separate routes: /watchlist, /portfolio, /market, /intelligence - Dashboard with Activity Feed and Market Pulse - Traffic light status indicators for domain status - Updated Header for public/logged-in state separation - Settings page uses new Command Center layout
420 lines
16 KiB
TypeScript
420 lines
16 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useRouter, useSearchParams } from 'next/navigation'
|
|
import { useStore } from '@/lib/store'
|
|
import { api } from '@/lib/api'
|
|
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
|
|
import { Toast, useToast } from '@/components/Toast'
|
|
import {
|
|
Eye,
|
|
Briefcase,
|
|
TrendingUp,
|
|
Gavel,
|
|
Clock,
|
|
Bell,
|
|
ArrowRight,
|
|
ExternalLink,
|
|
Sparkles,
|
|
ChevronRight,
|
|
Search,
|
|
Plus,
|
|
Zap,
|
|
Crown,
|
|
Activity,
|
|
} 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 router = useRouter()
|
|
const searchParams = useSearchParams()
|
|
const {
|
|
isAuthenticated,
|
|
isLoading,
|
|
checkAuth,
|
|
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)
|
|
|
|
useEffect(() => {
|
|
checkAuth()
|
|
}, [checkAuth])
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !isAuthenticated) {
|
|
router.push('/login')
|
|
}
|
|
}, [isLoading, isAuthenticated, router])
|
|
|
|
// Check for upgrade success
|
|
useEffect(() => {
|
|
if (searchParams.get('upgraded') === 'true') {
|
|
showToast('Welcome to your upgraded plan! 🎉', 'success')
|
|
window.history.replaceState({}, '', '/dashboard')
|
|
}
|
|
}, [searchParams])
|
|
|
|
// Load dashboard data
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
loadDashboardData()
|
|
}
|
|
}, [isAuthenticated])
|
|
|
|
const loadDashboardData = 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)
|
|
}
|
|
}
|
|
|
|
const handleQuickAdd = 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)
|
|
}
|
|
}
|
|
|
|
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>
|
|
)
|
|
}
|
|
|
|
// Calculate stats
|
|
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
|
|
|
|
return (
|
|
<CommandCenterLayout
|
|
title={`Welcome back${user?.name ? `, ${user.name.split(' ')[0]}` : ''}`}
|
|
subtitle="Your domain command center"
|
|
>
|
|
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
|
|
|
|
<div className="max-w-7xl mx-auto space-y-8">
|
|
{/* Quick Add */}
|
|
<div className="p-6 bg-gradient-to-r from-accent/10 to-transparent border border-accent/20 rounded-2xl">
|
|
<h2 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
|
<Search className="w-5 h-5 text-accent" />
|
|
Quick Add to Watchlist
|
|
</h2>
|
|
<form onSubmit={handleQuickAdd} className="flex gap-3">
|
|
<input
|
|
type="text"
|
|
value={quickDomain}
|
|
onChange={(e) => setQuickDomain(e.target.value)}
|
|
placeholder="Enter domain to track (e.g., dream.com)"
|
|
className="flex-1 h-12 px-4 bg-background border border-border rounded-xl
|
|
text-foreground placeholder:text-foreground-subtle
|
|
focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={addingDomain || !quickDomain.trim()}
|
|
className="flex items-center gap-2 h-12 px-6 bg-accent text-background rounded-xl
|
|
font-medium hover:bg-accent-hover transition-colors
|
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Stats Overview */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<Link
|
|
href="/watchlist"
|
|
className="group p-5 bg-background-secondary/50 border border-border rounded-xl
|
|
hover:border-foreground/20 transition-all"
|
|
>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="w-10 h-10 bg-foreground/5 rounded-xl flex items-center justify-center">
|
|
<Eye className="w-5 h-5 text-foreground-muted" />
|
|
</div>
|
|
<ChevronRight className="w-5 h-5 text-foreground-subtle group-hover:text-foreground
|
|
group-hover:translate-x-0.5 transition-all" />
|
|
</div>
|
|
<p className="text-2xl font-display text-foreground">{totalDomains}</p>
|
|
<p className="text-sm text-foreground-muted">Domains Watched</p>
|
|
</Link>
|
|
|
|
<Link
|
|
href="/watchlist?filter=available"
|
|
className={clsx(
|
|
"group p-5 border rounded-xl transition-all",
|
|
availableDomains.length > 0
|
|
? "bg-accent/10 border-accent/20 hover:border-accent/40"
|
|
: "bg-background-secondary/50 border-border hover:border-foreground/20"
|
|
)}
|
|
>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className={clsx(
|
|
"w-10 h-10 rounded-xl flex items-center justify-center",
|
|
availableDomains.length > 0 ? "bg-accent/20" : "bg-foreground/5"
|
|
)}>
|
|
<Sparkles className={clsx(
|
|
"w-5 h-5",
|
|
availableDomains.length > 0 ? "text-accent" : "text-foreground-muted"
|
|
)} />
|
|
</div>
|
|
{availableDomains.length > 0 && (
|
|
<span className="px-2 py-0.5 bg-accent text-background text-xs font-semibold rounded-full animate-pulse">
|
|
Action!
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className={clsx(
|
|
"text-2xl font-display",
|
|
availableDomains.length > 0 ? "text-accent" : "text-foreground"
|
|
)}>
|
|
{availableDomains.length}
|
|
</p>
|
|
<p className="text-sm text-foreground-muted">Available Now</p>
|
|
</Link>
|
|
|
|
<Link
|
|
href="/portfolio"
|
|
className="group p-5 bg-background-secondary/50 border border-border rounded-xl
|
|
hover:border-foreground/20 transition-all"
|
|
>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="w-10 h-10 bg-foreground/5 rounded-xl flex items-center justify-center">
|
|
<Briefcase className="w-5 h-5 text-foreground-muted" />
|
|
</div>
|
|
<ChevronRight className="w-5 h-5 text-foreground-subtle group-hover:text-foreground
|
|
group-hover:translate-x-0.5 transition-all" />
|
|
</div>
|
|
<p className="text-2xl font-display text-foreground">0</p>
|
|
<p className="text-sm text-foreground-muted">Portfolio Domains</p>
|
|
</Link>
|
|
|
|
<div className="p-5 bg-background-secondary/50 border border-border rounded-xl">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="w-10 h-10 bg-accent/10 rounded-xl flex items-center justify-center">
|
|
<TierIcon className="w-5 h-5 text-accent" />
|
|
</div>
|
|
</div>
|
|
<p className="text-2xl font-display text-foreground">{tierName}</p>
|
|
<p className="text-sm text-foreground-muted">
|
|
{subscription?.domains_used || 0}/{subscription?.domain_limit || 5} slots used
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Activity Feed + Market Pulse */}
|
|
<div className="grid lg:grid-cols-2 gap-6">
|
|
{/* Activity Feed */}
|
|
<div className="p-6 bg-background-secondary/50 border border-border rounded-2xl">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
|
<Activity className="w-5 h-5 text-accent" />
|
|
Activity Feed
|
|
</h2>
|
|
<Link href="/watchlist" className="text-sm text-accent hover:underline">
|
|
View all
|
|
</Link>
|
|
</div>
|
|
|
|
{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>
|
|
|
|
{/* Market Pulse */}
|
|
<div className="p-6 bg-background-secondary/50 border border-border rounded-2xl">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
|
<Gavel className="w-5 h-5 text-accent" />
|
|
Market Pulse
|
|
</h2>
|
|
<Link href="/market" className="text-sm text-accent hover:underline">
|
|
View all
|
|
</Link>
|
|
</div>
|
|
|
|
{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>
|
|
|
|
{/* Trending TLDs */}
|
|
<div className="p-6 bg-background-secondary/50 border border-border rounded-2xl">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
|
<TrendingUp className="w-5 h-5 text-accent" />
|
|
Trending TLDs
|
|
</h2>
|
|
<Link href="/intelligence" className="text-sm text-accent hover:underline">
|
|
View all
|
|
</Link>
|
|
</div>
|
|
|
|
{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>
|
|
) : (
|
|
<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 p-4 bg-background border border-border rounded-xl
|
|
hover:border-foreground/20 transition-all"
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-mono text-xl font-semibold text-foreground">.{tld.tld}</span>
|
|
<span className={clsx(
|
|
"text-xs font-semibold px-2 py-0.5 rounded-full",
|
|
(tld.price_change || 0) > 0
|
|
? "text-orange-400 bg-orange-400/10"
|
|
: "text-accent bg-accent/10"
|
|
)}>
|
|
{(tld.price_change || 0) > 0 ? '+' : ''}{(tld.price_change || 0).toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-foreground-muted truncate">{tld.reason}</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CommandCenterLayout>
|
|
)
|
|
}
|