yves.gugger ae1416bd34
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
Major navigation overhaul: Add Command Center with Sidebar
- 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
2025-12-10 08:37:29 +01:00

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>
)
}