feat: merge hunt/market pages, integrate cfo into portfolio
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

This commit is contained in:
2025-12-15 21:16:09 +01:00
parent b4954bf695
commit fccd88da46
29 changed files with 1572 additions and 1279 deletions

View File

@ -88,7 +88,8 @@ fi
# Step 2: Sync files (only changed)
echo -e "\n${YELLOW}[2/4] Syncing changed files...${NC}"
RSYNC_OPTS="-avz --delete"
# Using compression (-z), checksum-based detection, and bandwidth throttling for stability
RSYNC_OPTS="-avz --delete --compress-level=9 --checksum"
if ! $BACKEND_ONLY; then
echo " Frontend:"
@ -160,16 +161,24 @@ if ! $BACKEND_ONLY; then
set -e
cd ~/pounce/frontend
echo " Installing dependencies..."
if [ -f "package-lock.json" ]; then
npm ci
# Check if package.json changed (skip npm ci if not)
LOCKFILE_HASH=""
if [ -f ".lockfile_hash" ]; then
LOCKFILE_HASH=$(cat .lockfile_hash)
fi
CURRENT_HASH=$(md5sum package-lock.json 2>/dev/null | cut -d' ' -f1 || echo "none")
if [ "$LOCKFILE_HASH" != "$CURRENT_HASH" ]; then
echo " Installing dependencies (package-lock.json changed)..."
npm ci --prefer-offline --no-audit --no-fund
echo "$CURRENT_HASH" > .lockfile_hash
else
npm install
echo " ✓ Dependencies unchanged, skipping npm ci"
fi
# Build new version
# Build new version (with reduced memory for stability)
echo " Building..."
npm run build
NODE_OPTIONS="--max-old-space-size=2048" npm run build
BUILD_EXIT=$?
if [ $BUILD_EXIT -eq 0 ]; then

View File

@ -56,7 +56,7 @@ function LoginForm() {
const [verified, setVerified] = useState(false)
const sanitizeRedirect = (value: string | null | undefined): string => {
const fallback = '/terminal/radar'
const fallback = '/terminal/hunt'
if (!value) return fallback
const v = value.trim()
if (!v.startsWith('/')) return fallback
@ -131,7 +131,7 @@ function LoginForm() {
}
// Generate register link with redirect preserved
const registerLink = redirectTo !== '/terminal/radar'
const registerLink = redirectTo !== '/terminal/hunt'
? `/register?redirect=${encodeURIComponent(redirectTo)}`
: '/register'

View File

@ -12,7 +12,7 @@ function OAuthCallbackContent() {
useEffect(() => {
const sanitizeRedirect = (value: string | null): string => {
const fallback = '/terminal/radar'
const fallback = '/terminal/hunt'
if (!value) return fallback
const v = value.trim()
if (!v.startsWith('/')) return fallback

View File

@ -630,7 +630,7 @@ export default function HomePage() {
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 sm:gap-8">
<Link
href={isAuthenticated ? "/terminal/radar" : "/register"}
href={isAuthenticated ? "/terminal/hunt" : "/register"}
className="w-full sm:w-auto group relative px-8 sm:px-10 py-4 sm:py-5 bg-accent text-black text-xs sm:text-sm font-bold uppercase tracking-[0.15em] sm:tracking-[0.2em] hover:bg-white transition-colors duration-500"
>
<span className="relative z-10 flex items-center justify-center gap-3">

View File

@ -144,7 +144,7 @@ export default function PricingPage() {
}
if (!isPaid) {
router.push('/terminal/radar')
router.push('/terminal/hunt')
return
}
@ -405,7 +405,7 @@ export default function PricingPage() {
Start with Scout. It&apos;s free forever. Upgrade when you need more firepower.
</p>
<Link
href={isAuthenticated ? "/terminal/radar" : "/register"}
href={isAuthenticated ? "/terminal/hunt" : "/register"}
className="inline-flex items-center gap-3 px-8 py-4 border border-white/20 text-white text-xs font-bold uppercase tracking-[0.2em] hover:bg-white hover:text-black transition-all"
>
{isAuthenticated ? "Command Center" : "Join the Hunt"}

View File

@ -62,7 +62,7 @@ function RegisterForm() {
const [registered, setRegistered] = useState(false)
// Get redirect URL from query params
const redirectTo = searchParams.get('redirect') || '/terminal/radar'
const redirectTo = searchParams.get('redirect') || '/terminal/hunt'
const refFromUrl = searchParams.get('ref')
const getCookie = (name: string): string | null => {
@ -85,7 +85,7 @@ function RegisterForm() {
const ref = refFromUrl || getCookie('pounce_ref') || undefined
await register(email, password, undefined, ref || undefined)
if (redirectTo !== '/terminal/radar') {
if (redirectTo !== '/terminal/hunt') {
localStorage.setItem('pounce_redirect_after_login', redirectTo)
}
@ -98,7 +98,7 @@ function RegisterForm() {
}
// Generate login link with redirect preserved
const loginLink = redirectTo !== '/terminal/radar'
const loginLink = redirectTo !== '/terminal/hunt'
? `/login?redirect=${encodeURIComponent(redirectTo)}`
: '/login'

View File

@ -1,176 +0,0 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { Sidebar } from '@/components/Sidebar'
import { Toast, useToast } from '@/components/Toast'
import { api } from '@/lib/api'
import { useStore } from '@/lib/store'
import { BurnRateTimeline } from '@/components/cfo/BurnRateTimeline'
import { KillList } from '@/components/cfo/KillList'
import { Loader2, RefreshCw } from 'lucide-react'
import clsx from 'clsx'
export default function CfoPage() {
const { checkAuth } = useStore()
const { toast, hideToast } = useToast()
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [data, setData] = useState<{
computed_at: string
upcoming_30d_total_usd: number
upcoming_30d_rows: Array<{
domain_id: number
domain: string
renewal_date: string | null
renewal_cost_usd: number | null
cost_source: string
is_sold: boolean
}>
monthly: Array<{ month: string; total_cost_usd: number; domains: number }>
kill_list: Array<{
domain_id: number
domain: string
renewal_date: string | null
renewal_cost_usd: number | null
cost_source: string
auto_renew: boolean
is_dns_verified: boolean
yield_net_60d: number
yield_clicks_60d: number
reason: string
}>
} | null>(null)
const load = useCallback(async () => {
setError(null)
const res = await api.getCfoSummary()
setData(res)
}, [])
useEffect(() => {
checkAuth()
}, [checkAuth])
useEffect(() => {
let cancelled = false
const run = async () => {
setLoading(true)
try {
await load()
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : String(e))
} finally {
if (!cancelled) setLoading(false)
}
}
run()
return () => {
cancelled = true
}
}, [load])
const refresh = useCallback(async () => {
setLoading(true)
try {
await load()
} finally {
setLoading(false)
}
}, [load])
return (
<div className="min-h-screen bg-[#020202]">
<Sidebar />
<main className="lg:ml-72">
<section className="px-4 lg:px-10 pt-8 pb-5 border-b border-white/[0.08]">
<div className="flex items-end justify-between gap-6 flex-wrap">
<div>
<p className="text-[10px] font-mono text-white/40 uppercase tracking-[0.25em]">PHASE 4</p>
<h1 className="text-3xl font-bold text-white tracking-tight">CFO</h1>
<p className="text-white/40 text-sm font-mono mt-2 max-w-2xl">
Renewal runway, burn rate, and drop advice. No fluff just numbers.
</p>
</div>
<button
onClick={refresh}
disabled={loading}
className={clsx(
'p-2 border border-white/10 text-white/40 hover:text-white hover:bg-white/5 transition-colors',
loading && 'opacity-50'
)}
title="Refresh"
>
<RefreshCw className={clsx('w-4 h-4', loading && 'animate-spin')} />
</button>
</div>
</section>
<section className="px-4 lg:px-10 py-6 pb-24 space-y-3">
{loading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : error ? (
<div className="border border-white/[0.08] bg-[#020202] p-4 text-[12px] font-mono text-red-300">{error}</div>
) : !data ? (
<div className="border border-white/[0.08] bg-[#020202] p-6 text-white/40 font-mono text-sm">No data.</div>
) : (
<>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div className="border border-white/[0.08] bg-[#020202]">
<div className="px-4 py-3 border-b border-white/[0.08]">
<div className="text-[10px] font-mono uppercase tracking-widest text-white/40">Upcoming costs</div>
<div className="text-sm font-bold text-white">Next 30 days</div>
</div>
<div className="p-4">
<div className="text-2xl font-bold text-white font-mono">${Math.round(data.upcoming_30d_total_usd)}</div>
<div className="text-[10px] font-mono text-white/30 mt-1">{data.upcoming_30d_rows.length} renewals due</div>
<div className="mt-3 space-y-1">
{data.upcoming_30d_rows.slice(0, 8).map((r) => (
<div key={r.domain_id} className="flex items-center justify-between text-[11px] font-mono text-white/60">
<span className="truncate">{r.domain}</span>
<span className="text-white/40">
{r.renewal_date ? r.renewal_date.slice(0, 10) : '—'} · ${Math.round(r.renewal_cost_usd || 0)}
</span>
</div>
))}
{data.upcoming_30d_rows.length === 0 ? (
<div className="text-[12px] font-mono text-white/30">No renewals in the next 30 days.</div>
) : null}
</div>
</div>
</div>
<BurnRateTimeline monthly={data.monthly} />
</div>
<KillList rows={data.kill_list} onChanged={refresh} />
<div className="border border-white/[0.08] bg-[#020202] p-4">
<div className="text-[10px] font-mono uppercase tracking-widest text-white/40">What to do next</div>
<div className="mt-2 text-[12px] font-mono text-white/50 space-y-1">
<div>
- If a renewal cost is missing, fill it in on the domain in <span className="text-white/70">Portfolio Edit</span>.
</div>
<div>
- Set to Drop is a local flag you still need to disable auto-renew at your registrar.
</div>
<div>
- Want to cover costs? Activate Yield only for <span className="text-white/70">DNSverified</span> domains.
</div>
</div>
</div>
</>
)}
</section>
</main>
{toast && (
<Toast message={toast.message} type={toast.type} onClose={hideToast} />
)}
</div>
)
}

View File

@ -1,11 +1,12 @@
'use client'
import { useEffect, useState, useCallback } from 'react'
import { useEffect, useState } from 'react'
import { useStore } from '@/lib/store'
import { Sidebar } from '@/components/Sidebar'
import { Toast, useToast } from '@/components/Toast'
import { HuntStrategyChips, type HuntTab } from '@/components/hunt/HuntStrategyChips'
import { SniperTab } from '@/components/hunt/SniperTab'
import { AuctionsTab } from '@/components/hunt/AuctionsTab'
import { DropsTab } from '@/components/hunt/DropsTab'
import { SearchTab } from '@/components/hunt/SearchTab'
import { TrendSurferTab } from '@/components/hunt/TrendSurferTab'
import { BrandableForgeTab } from '@/components/hunt/BrandableForgeTab'
import {
@ -25,19 +26,41 @@ import {
Crown,
X,
Briefcase,
Search,
Download,
Flame,
Wand2,
} from 'lucide-react'
import clsx from 'clsx'
import Link from 'next/link'
import Image from 'next/image'
// ============================================================================
// TYPES
// ============================================================================
type HuntTab = 'auctions' | 'drops' | 'search' | 'trends' | 'forge'
// ============================================================================
// TAB CONFIG
// ============================================================================
const TABS: Array<{ key: HuntTab; label: string; shortLabel: string; icon: any; color: string }> = [
{ key: 'auctions', label: 'Auctions', shortLabel: 'Auctions', icon: Gavel, color: 'accent' },
{ key: 'drops', label: 'Drops', shortLabel: 'Drops', icon: Download, color: 'blue' },
{ key: 'search', label: 'Search', shortLabel: 'Search', icon: Search, color: 'white' },
{ key: 'trends', label: 'Trends', shortLabel: 'Trends', icon: Flame, color: 'orange' },
{ key: 'forge', label: 'Forge', shortLabel: 'Forge', icon: Wand2, color: 'purple' },
]
// ============================================================================
// MAIN PAGE
// ============================================================================
export default function HuntPage() {
const { isAuthenticated, isLoading: authLoading, user, subscription, logout, checkAuth, domains } = useStore()
const { user, subscription, logout, checkAuth, domains } = useStore()
const { toast, showToast, hideToast } = useToast()
const [tab, setTab] = useState<HuntTab>('sniper')
const [tab, setTab] = useState<HuntTab>('auctions')
// Mobile Menu State
const [menuOpen, setMenuOpen] = useState(false)
@ -48,15 +71,15 @@ export default function HuntPage() {
}, [checkAuth])
// Computed
const availableDomains = domains?.filter(d => d.is_available) || []
const availableDomains = domains?.filter((d) => d.is_available) || []
const totalDomains = domains?.length || 0
// Nav Items for Mobile Bottom Bar
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/hunt', label: 'Hunt', icon: Crosshair, active: true },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/portfolio', label: 'Portfolio', icon: Briefcase, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
]
// Full Navigation for Drawer
@ -67,36 +90,28 @@ export default function HuntPage() {
{
title: 'Discover',
items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/hunt', label: 'Hunt', icon: Crosshair },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]
],
},
{
title: 'Manage',
items: [
{ href: '/terminal/watchlist', label: 'Watchlist', icon: Eye },
{ href: '/terminal/portfolio', label: 'Portfolio', icon: Briefcase },
{ href: '/terminal/cfo', label: 'CFO', icon: Shield },
{ href: '/terminal/sniper', label: 'Sniper', icon: Target },
]
],
},
{
title: 'Monetize',
items: [
{ href: '/terminal/yield', label: 'Yield', icon: Coins, isNew: true },
{ href: '/terminal/listing', label: 'For Sale', icon: Tag },
]
}
],
},
]
// Tab labels for header
const tabLabels: Record<HuntTab, string> = {
sniper: 'Closeout Sniper',
trends: 'Trend Surfer',
forge: 'Brandable Forge',
}
const activeTab = TABS.find((t) => t.key === tab)!
return (
<div className="min-h-screen bg-[#020202]">
@ -107,106 +122,121 @@ export default function HuntPage() {
{/* Main Content */}
<main className="lg:pl-[240px]">
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* MOBILE HEADER - Techy Angular */}
{/* MOBILE HEADER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<header
className="lg:hidden sticky top-0 z-40 bg-[#020202]/95 backdrop-blur-md border-b border-white/[0.08]"
style={{ paddingTop: 'env(safe-area-inset-top)' }}
>
<div className="px-4 py-3">
{/* Top Row: Brand + Stats */}
{/* Top Row */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<Crosshair className="w-4 h-4 text-accent" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Domain Hunt</span>
</div>
<div className="flex items-center gap-2 text-[10px] font-mono text-white/40">
<span className="text-accent">{tabLabels[tab]}</span>
<div className="text-[10px] font-mono text-white/40">
{totalDomains} tracked · {availableDomains.length} available
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-3 gap-2">
<div className="bg-white/[0.02] border border-white/[0.08] p-2">
<div className="text-lg font-bold text-white tabular-nums">{totalDomains}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Tracked</div>
{/* Tab Bar - Scrollable */}
<div className="-mx-4 px-4 overflow-x-auto">
<div className="flex gap-1 min-w-max pb-1">
{TABS.map((t) => {
const isActive = tab === t.key
return (
<button
key={t.key}
onClick={() => setTab(t.key)}
className={clsx(
'flex items-center gap-1.5 px-3 py-2 border transition-all shrink-0',
isActive
? t.color === 'accent'
? 'border-accent/40 bg-accent/10 text-accent'
: t.color === 'blue'
? 'border-blue-500/40 bg-blue-500/10 text-blue-400'
: t.color === 'orange'
? 'border-orange-500/40 bg-orange-500/10 text-orange-400'
: t.color === 'purple'
? 'border-purple-500/40 bg-purple-500/10 text-purple-400'
: 'border-white/40 bg-white/10 text-white'
: 'border-transparent text-white/40 active:bg-white/5'
)}
>
<t.icon className="w-3.5 h-3.5" />
<span className="text-[10px] font-bold uppercase tracking-wider font-mono">{t.shortLabel}</span>
</button>
)
})}
</div>
<div className="bg-accent/[0.05] border border-accent/20 p-2">
<div className="text-lg font-bold text-accent tabular-nums">{availableDomains.length}</div>
<div className="text-[9px] font-mono text-accent/60 uppercase tracking-wider">Available</div>
</div>
<button
onClick={() => {
const next: HuntTab = tab === 'sniper' ? 'trends' : tab === 'trends' ? 'forge' : 'sniper'
setTab(next)
}}
className="bg-white/[0.02] border border-white/[0.08] p-2 text-left hover:bg-white/[0.04] active:bg-white/[0.06] transition-colors"
>
<div className="text-[10px] font-bold text-white/70 truncate">{tabLabels[tab]}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Strategy</div>
</button>
</div>
</div>
</header>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* DESKTOP HERO + STRATEGY CHIPS */}
{/* DESKTOP HEADER + TAB BAR */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="px-4 lg:px-10 pt-4 lg:pt-10 pb-4">
{/* Desktop Hero Text */}
<div className="hidden lg:block mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Phase 1 · Discovery</span>
<section className="hidden lg:block px-10 pt-10 pb-6 border-b border-white/[0.08]">
<div className="flex items-end justify-between gap-6 mb-6">
<div>
<div className="flex items-center gap-3 mb-3">
<Crosshair className="w-5 h-5 text-accent" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Discovery Hub</span>
</div>
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em] text-white">Domain Hunt</h1>
<p className="text-white/40 text-sm font-mono mt-2 max-w-lg">
Search domains, browse auctions, discover drops, ride trends, or generate brandables.
</p>
</div>
<div className="flex gap-6">
<div className="text-right">
<div className="text-2xl font-bold text-accent font-mono">{totalDomains}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Tracked</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-white font-mono">{availableDomains.length}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Available</div>
</div>
</div>
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em] text-white mb-2">
Domain Hunt
</h1>
<p className="text-white/40 text-sm font-mono max-w-lg">
Find Analyze Decide. Strategy-first discovery for domainers.
</p>
</div>
{/* Strategy Chips - Desktop */}
<div className="hidden lg:block">
<HuntStrategyChips tab={tab} onChange={setTab} />
</div>
{/* Desktop Tab Bar */}
<div className="flex gap-2">
{TABS.map((t) => {
const isActive = tab === t.key
const colorClasses: Record<string, { active: string; inactive: string }> = {
accent: { active: 'border-accent bg-accent/10 text-accent', inactive: 'border-white/[0.08] text-white/50 hover:text-white hover:bg-white/[0.02]' },
blue: { active: 'border-blue-500 bg-blue-500/10 text-blue-400', inactive: 'border-white/[0.08] text-white/50 hover:text-white hover:bg-white/[0.02]' },
white: { active: 'border-white/40 bg-white/10 text-white', inactive: 'border-white/[0.08] text-white/50 hover:text-white hover:bg-white/[0.02]' },
orange: { active: 'border-orange-500 bg-orange-500/10 text-orange-400', inactive: 'border-white/[0.08] text-white/50 hover:text-white hover:bg-white/[0.02]' },
purple: { active: 'border-purple-500 bg-purple-500/10 text-purple-400', inactive: 'border-white/[0.08] text-white/50 hover:text-white hover:bg-white/[0.02]' },
}
const classes = colorClasses[t.color] || colorClasses.white
{/* Strategy Chips - Mobile (horizontal scroll) */}
<div className="lg:hidden -mx-4 px-4 overflow-x-auto pb-2">
<div className="flex gap-2 min-w-max">
{(['sniper', 'trends', 'forge'] as HuntTab[]).map((t) => {
const active = tab === t
const labels: Record<HuntTab, { label: string; hint: string }> = {
sniper: { label: 'SNIPER', hint: '< $10 · 5y+' },
trends: { label: 'TRENDS', hint: 'Keywords + Typos' },
forge: { label: 'FORGE', hint: 'Brandables' },
}
return (
<button
key={t}
onClick={() => setTab(t)}
className={clsx(
'px-3 py-2 border text-left transition-colors shrink-0',
active ? 'border-accent/30 bg-accent/10 text-accent' : 'border-white/10 text-white/50 active:bg-white/5'
)}
>
<div className="text-[10px] font-bold uppercase tracking-wider font-mono">{labels[t].label}</div>
<div className="text-[9px] font-mono mt-0.5 text-white/30">{labels[t].hint}</div>
</button>
)
})}
</div>
return (
<button
key={t.key}
onClick={() => setTab(t.key)}
className={clsx('flex items-center gap-2 px-4 py-2.5 border transition-all', isActive ? classes.active : classes.inactive)}
>
<t.icon className="w-4 h-4" />
<span className="text-xs font-bold uppercase tracking-wider">{t.label}</span>
</button>
)
})}
</div>
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* TAB CONTENT */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="px-4 lg:px-10 py-4 pb-28 lg:pb-10">
{tab === 'sniper' && <SniperTab showToast={showToast} />}
<section className="px-4 lg:px-10 py-4 lg:py-6 pb-28 lg:pb-10">
{tab === 'auctions' && <AuctionsTab showToast={showToast} />}
{tab === 'drops' && <DropsTab showToast={showToast} />}
{tab === 'search' && <SearchTab showToast={showToast} />}
{tab === 'trends' && <TrendSurferTab showToast={showToast} />}
{tab === 'forge' && <BrandableForgeTab showToast={showToast} />}
</section>
@ -224,19 +254,16 @@ export default function HuntPage() {
key={item.href}
href={item.href}
className={clsx(
"flex-1 flex flex-col items-center justify-center gap-0.5 relative transition-colors",
item.active ? "text-accent" : "text-white/40 active:text-white/80"
'flex-1 flex flex-col items-center justify-center gap-0.5 relative transition-colors',
item.active ? 'text-accent' : 'text-white/40 active:text-white/80'
)}
>
{item.active && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-0.5 bg-accent" />
)}
{item.active && <div className="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-0.5 bg-accent" />}
<item.icon className="w-5 h-5" />
<span className="text-[9px] font-mono uppercase tracking-wider">{item.label}</span>
</Link>
))}
{/* Menu Button */}
<button
onClick={() => setMenuOpen(true)}
className="flex-1 flex flex-col items-center justify-center gap-0.5 text-white/40 active:text-white/80 transition-all"
@ -252,16 +279,12 @@ export default function HuntPage() {
{/* ═══════════════════════════════════════════════════════════════════════ */}
{menuOpen && (
<div className="lg:hidden fixed inset-0 z-[100]">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/80 animate-in fade-in duration-200" onClick={() => setMenuOpen(false)} />
<div
className="absolute inset-0 bg-black/80 animate-in fade-in duration-200"
onClick={() => setMenuOpen(false)}
/>
{/* Drawer Panel */}
<div className="absolute top-0 right-0 bottom-0 w-[80%] max-w-[300px] bg-[#0A0A0A] border-l border-white/[0.08] animate-in slide-in-from-right duration-300 flex flex-col" style={{ paddingTop: 'env(safe-area-inset-top)', paddingBottom: 'env(safe-area-inset-bottom)' }}>
{/* Drawer Header */}
className="absolute top-0 right-0 bottom-0 w-[80%] max-w-[300px] bg-[#0A0A0A] border-l border-white/[0.08] animate-in slide-in-from-right duration-300 flex flex-col"
style={{ paddingTop: 'env(safe-area-inset-top)', paddingBottom: 'env(safe-area-inset-bottom)' }}
>
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
<div className="flex items-center gap-3">
<Image src="/pounce-puma.png" alt="Pounce" width={28} height={28} className="object-contain" />
@ -278,7 +301,6 @@ export default function HuntPage() {
</button>
</div>
{/* Navigation Sections */}
<div className="flex-1 overflow-y-auto py-4">
{drawerNavSections.map((section) => (
<div key={section.title} className="mb-4">
@ -296,16 +318,13 @@ export default function HuntPage() {
>
<item.icon className="w-4 h-4 text-white/30" />
<span className="text-sm font-medium flex-1">{item.label}</span>
{item.isNew && (
<span className="px-1.5 py-0.5 text-[8px] font-bold bg-accent text-black">NEW</span>
)}
{item.isNew && <span className="px-1.5 py-0.5 text-[8px] font-bold bg-accent text-black">NEW</span>}
</Link>
))}
</div>
</div>
))}
{/* Settings */}
<div className="pt-3 border-t border-white/[0.08] mx-4">
<Link
href="/terminal/settings"
@ -329,16 +348,13 @@ export default function HuntPage() {
</div>
</div>
{/* User Card */}
<div className="p-4 bg-white/[0.02] border-t border-white/[0.08]">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 bg-accent/10 border border-accent/20 flex items-center justify-center">
<TierIcon className="w-4 h-4 text-accent" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white truncate">
{user?.name || user?.email?.split('@')[0] || 'User'}
</p>
<p className="text-sm font-bold text-white truncate">{user?.name || user?.email?.split('@')[0] || 'User'}</p>
<p className="text-[9px] font-mono text-white/40 uppercase tracking-wider">{tierName}</p>
</div>
</div>
@ -355,7 +371,10 @@ export default function HuntPage() {
)}
<button
onClick={() => { logout(); setMenuOpen(false) }}
onClick={() => {
logout()
setMenuOpen(false)
}}
className="flex items-center justify-center gap-2 w-full py-2 border border-white/10 text-white/40 text-[10px] font-mono uppercase tracking-wider active:bg-white/5 transition-all"
>
<LogOut className="w-3 h-3" />

View File

@ -68,7 +68,7 @@ export default function InboxPage() {
const drawerNavSections = [
{ title: 'Discover', items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/hunt', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]},

View File

@ -368,7 +368,7 @@ export default function TldDetailPage() {
// Mobile Nav
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/hunt', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: true },
@ -381,7 +381,7 @@ export default function TldDetailPage() {
{
title: 'Discover',
items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/hunt', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]

View File

@ -234,7 +234,7 @@ export default function IntelPage() {
// Mobile Nav
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/hunt', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: true },
@ -247,7 +247,7 @@ export default function IntelPage() {
{
title: 'Discover',
items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/hunt', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]

View File

@ -126,7 +126,7 @@ export default function MyListingsPage() {
}
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/hunt', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
@ -137,7 +137,7 @@ export default function MyListingsPage() {
const drawerNavSections = [
{ title: 'Discover', items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/hunt', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]},

View File

@ -304,7 +304,7 @@ export default function MarketPage() {
// Mobile Nav
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/hunt', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: true },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
@ -317,7 +317,7 @@ export default function MarketPage() {
{
title: 'Discover',
items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/hunt', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]

View File

@ -3,11 +3,11 @@
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
export default function CommandPage() {
export default function TerminalPage() {
const router = useRouter()
useEffect(() => {
router.replace('/terminal/radar')
router.replace('/terminal/hunt')
}, [router])
return (

View File

@ -6,6 +6,8 @@ import { api, PortfolioDomain, PortfolioSummary, DomainHealthReport, HealthStatu
import { useAnalyzePanelStore } from '@/lib/analyze-store'
import { Sidebar } from '@/components/Sidebar'
import { Toast, useToast } from '@/components/Toast'
import { BurnRateTimeline } from '@/components/cfo/BurnRateTimeline'
import { KillList } from '@/components/cfo/KillList'
import {
Plus,
Trash2,
@ -14,10 +16,7 @@ import {
Briefcase,
X,
Target,
ExternalLink,
Gavel,
TrendingUp,
Menu,
Settings,
ShieldCheck,
Shield,
@ -28,24 +27,20 @@ import {
Eye,
ChevronUp,
ChevronDown,
DollarSign,
Calendar,
Edit3,
CheckCircle,
AlertCircle,
Copy,
Check,
ArrowUpRight,
Navigation,
Coins,
Activity,
Save,
FileText,
Clock,
Building2,
CreditCard,
MoreVertical,
RotateCcw
Crosshair,
Wallet,
} from 'lucide-react'
import clsx from 'clsx'
import Link from 'next/link'
@ -55,6 +50,34 @@ import Image from 'next/image'
// TYPES
// ============================================================================
type PortfolioTab = 'assets' | 'financials'
interface CfoData {
computed_at: string
upcoming_30d_total_usd: number
upcoming_30d_rows: Array<{
domain_id: number
domain: string
renewal_date: string | null
renewal_cost_usd: number | null
cost_source: string
is_sold: boolean
}>
monthly: Array<{ month: string; total_cost_usd: number; domains: number }>
kill_list: Array<{
domain_id: number
domain: string
renewal_date: string | null
renewal_cost_usd: number | null
cost_source: string
auto_renew: boolean
is_dns_verified: boolean
yield_net_60d: number
yield_clicks_60d: number
reason: string
}>
}
interface EditFormData {
purchase_date: string
purchase_price: string
@ -628,12 +651,19 @@ export default function PortfolioPage() {
const { toast, showToast, hideToast } = useToast()
const openAnalyze = useAnalyzePanelStore((s) => s.open)
// Tab state
const [activeTab, setActiveTab] = useState<PortfolioTab>('assets')
const [domains, setDomains] = useState<PortfolioDomain[]>([])
const [summary, setSummary] = useState<PortfolioSummary | null>(null)
const [loading, setLoading] = useState(true)
const [refreshingId, setRefreshingId] = useState<number | null>(null)
const [deletingId, setDeletingId] = useState<number | null>(null)
// CFO / Financials data
const [cfoData, setCfoData] = useState<CfoData | null>(null)
const [cfoLoading, setCfoLoading] = useState(false)
// Modals
const [showAddModal, setShowAddModal] = useState(false)
const [editingDomain, setEditingDomain] = useState<PortfolioDomain | null>(null)
@ -698,7 +728,27 @@ export default function PortfolioPage() {
}
}, [])
// Load CFO/Financials data
const loadCfoData = useCallback(async () => {
setCfoLoading(true)
try {
const res = await api.getCfoSummary()
setCfoData(res)
} catch (err) {
console.error('Failed to load CFO data:', err)
} finally {
setCfoLoading(false)
}
}, [])
useEffect(() => { loadData() }, [loadData])
// Load CFO data when switching to financials tab
useEffect(() => {
if (activeTab === 'financials' && !cfoData && !cfoLoading) {
loadCfoData()
}
}, [activeTab, cfoData, cfoLoading, loadCfoData])
// Stats
const stats = useMemo(() => {
@ -710,8 +760,9 @@ export default function PortfolioPage() {
const days = getDaysUntil(d.renewal_date)
return days !== null && days <= 30 && days > 0
}).length
return { total: domains.length, active, sold, verified, expiringSoon }
}, [domains])
const upcoming30dCost = cfoData?.upcoming_30d_total_usd || 0
return { total: domains.length, active, sold, verified, expiringSoon, upcoming30dCost }
}, [domains, cfoData])
// Filter & Sort
const filteredDomains = useMemo(() => {
@ -824,16 +875,15 @@ export default function PortfolioPage() {
// Navigation
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/hunt', label: 'Hunt', icon: Crosshair, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/portfolio', label: 'Portfolio', icon: Briefcase, active: true },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
]
const drawerNavSections = [
{ title: 'Discover', items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/hunt', label: 'Hunt', icon: Crosshair },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]},
{ title: 'Manage', items: [
@ -1042,33 +1092,164 @@ export default function PortfolioPage() {
</div>
</section>
{/* FILTERS */}
{/* TABS */}
<section className="px-4 lg:px-10 py-4 border-y border-white/[0.08] bg-white/[0.01]">
<div className="flex items-center gap-3 overflow-x-auto">
{[
{ value: 'all', label: 'All', count: stats.total },
{ value: 'active', label: 'Active', count: stats.active },
{ value: 'sold', label: 'Sold', count: stats.sold },
{ value: 'expiring', label: 'Expiring Soon', count: stats.expiringSoon },
].map((item) => (
<button
key={item.value}
onClick={() => setFilter(item.value as any)}
className={clsx(
"shrink-0 px-3 py-2 text-[10px] font-mono uppercase tracking-wider border transition-colors",
filter === item.value
? "bg-white/10 text-white border-white/20"
: "text-white/40 border-transparent hover:text-white/60"
)}
>
{item.label} ({item.count})
</button>
))}
<div className="flex items-center gap-4 mb-4">
<button
onClick={() => setActiveTab('assets')}
className={clsx(
"flex items-center gap-2 px-4 py-2 text-xs font-bold uppercase tracking-wider border transition-colors",
activeTab === 'assets'
? "bg-accent/10 text-accent border-accent/30"
: "text-white/50 border-white/[0.08] hover:text-white hover:bg-white/[0.02]"
)}
>
<Briefcase className="w-4 h-4" />
Assets
</button>
<button
onClick={() => setActiveTab('financials')}
className={clsx(
"flex items-center gap-2 px-4 py-2 text-xs font-bold uppercase tracking-wider border transition-colors",
activeTab === 'financials'
? "bg-orange-500/10 text-orange-400 border-orange-500/30"
: "text-white/50 border-white/[0.08] hover:text-white hover:bg-white/[0.02]"
)}
>
<Wallet className="w-4 h-4" />
Financials
{stats.upcoming30dCost > 0 && (
<span className="ml-1 px-1.5 py-0.5 text-[9px] bg-orange-500/20 text-orange-400 border border-orange-500/20">
${Math.round(stats.upcoming30dCost)}
</span>
)}
</button>
</div>
{/* Asset Filters - only show when assets tab active */}
{activeTab === 'assets' && (
<div className="flex items-center gap-3 overflow-x-auto">
{[
{ value: 'all', label: 'All', count: stats.total },
{ value: 'active', label: 'Active', count: stats.active },
{ value: 'sold', label: 'Sold', count: stats.sold },
{ value: 'expiring', label: 'Expiring Soon', count: stats.expiringSoon },
].map((item) => (
<button
key={item.value}
onClick={() => setFilter(item.value as any)}
className={clsx(
"shrink-0 px-3 py-2 text-[10px] font-mono uppercase tracking-wider border transition-colors",
filter === item.value
? "bg-white/10 text-white border-white/20"
: "text-white/40 border-transparent hover:text-white/60"
)}
>
{item.label} ({item.count})
</button>
))}
</div>
)}
</section>
{/* DOMAIN LIST */}
{/* TAB CONTENT */}
<section className="px-4 lg:px-10 py-6">
{/* FINANCIALS TAB */}
{activeTab === 'financials' && (
<div className="space-y-4">
{cfoLoading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : !cfoData ? (
<div className="text-center py-16 border border-dashed border-white/[0.08]">
<Wallet className="w-10 h-10 text-white/10 mx-auto mb-4" />
<p className="text-white/40 text-sm font-mono mb-1">No financial data</p>
<p className="text-white/25 text-xs font-mono mb-4">Add domains to your portfolio to see costs</p>
<button
onClick={loadCfoData}
className="inline-flex items-center gap-2 px-4 py-2 border border-white/10 text-white/60 text-xs font-bold uppercase hover:bg-white/5"
>
<RefreshCw className="w-4 h-4" />Refresh
</button>
</div>
) : (
<>
{/* Summary Cards */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
<div className="border border-orange-500/20 bg-orange-500/[0.03] p-4">
<div className="flex items-center gap-2 mb-2">
<Wallet className="w-4 h-4 text-orange-400" />
<span className="text-[10px] font-mono text-orange-400/60 uppercase tracking-wider">Next 30 Days</span>
</div>
<div className="text-2xl font-bold text-orange-400 font-mono">${Math.round(cfoData.upcoming_30d_total_usd)}</div>
<div className="text-[10px] font-mono text-white/30 mt-1">{cfoData.upcoming_30d_rows.length} renewals due</div>
</div>
<div className="border border-white/[0.08] bg-white/[0.02] p-4">
<div className="flex items-center gap-2 mb-2">
<Calendar className="w-4 h-4 text-white/40" />
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">Annual Burn</span>
</div>
<div className="text-2xl font-bold text-white font-mono">
${Math.round(cfoData.monthly.reduce((sum, m) => sum + m.total_cost_usd, 0))}
</div>
<div className="text-[10px] font-mono text-white/30 mt-1">Based on 12-month forecast</div>
</div>
<div className="border border-white/[0.08] bg-white/[0.02] p-4">
<div className="flex items-center gap-2 mb-2">
<Trash2 className="w-4 h-4 text-white/40" />
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">Drop Candidates</span>
</div>
<div className="text-2xl font-bold text-white font-mono">{cfoData.kill_list.length}</div>
<div className="text-[10px] font-mono text-white/30 mt-1">No yield, expiring soon</div>
</div>
</div>
{/* Upcoming Renewals */}
{cfoData.upcoming_30d_rows.length > 0 && (
<div className="border border-white/[0.08] bg-[#020202]">
<div className="px-4 py-3 border-b border-white/[0.08]">
<div className="text-[10px] font-mono uppercase tracking-widest text-white/40">Upcoming Costs</div>
<div className="text-sm font-bold text-white">Next 30 days</div>
</div>
<div className="divide-y divide-white/[0.06]">
{cfoData.upcoming_30d_rows.slice(0, 10).map((r) => (
<div key={r.domain_id} className="px-4 py-2 flex items-center justify-between text-[11px] font-mono">
<span className="text-white/60 truncate">{r.domain}</span>
<span className="text-white/40 shrink-0">
{r.renewal_date ? r.renewal_date.slice(0, 10) : ''} · ${Math.round(r.renewal_cost_usd || 0)}
</span>
</div>
))}
</div>
</div>
)}
{/* Burn Rate Timeline */}
<BurnRateTimeline monthly={cfoData.monthly} />
{/* Kill List */}
<KillList rows={cfoData.kill_list} onChanged={loadCfoData} />
{/* Tips */}
<div className="border border-white/[0.08] bg-[#020202] p-4">
<div className="text-[10px] font-mono uppercase tracking-widest text-white/40 mb-2">What to do next</div>
<div className="text-[12px] font-mono text-white/50 space-y-1">
<div>- If a renewal cost is missing, fill it in on the domain in <span className="text-white/70">Assets → Edit</span>.</div>
<div>- "Set to Drop" is a local flag — you still need to disable auto-renew at your registrar.</div>
<div>- Want to cover costs? Activate Yield only for <span className="text-white/70">DNSverified</span> domains.</div>
</div>
</div>
</>
)}
</div>
)}
{/* ASSETS TAB (Domain List) */}
{activeTab === 'assets' && (
<>
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
@ -1390,6 +1571,8 @@ export default function PortfolioPage() {
})}
</div>
)}
</>
)}
</section>
{/* MOBILE BOTTOM NAV */}

View File

@ -1,882 +0,0 @@
'use client'
import { useEffect, useState, useCallback, useRef, useMemo } from 'react'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { Sidebar } from '@/components/Sidebar'
import { Toast, useToast } from '@/components/Toast'
import {
Eye,
Gavel,
ArrowRight,
CheckCircle2,
XCircle,
Loader2,
Crosshair,
Zap,
Globe,
Target,
Search,
X,
TrendingUp,
Settings,
Clock,
ChevronRight,
ChevronUp,
ChevronDown,
Sparkles,
Radio,
Activity,
Menu,
Tag,
Coins,
Shield,
LogOut,
Crown,
ExternalLink,
Briefcase
} from 'lucide-react'
import clsx from 'clsx'
import Link from 'next/link'
import Image from 'next/image'
// ============================================================================
// TYPES
// ============================================================================
interface HotAuction {
domain: string
current_bid: number
end_time?: string
platform: string
affiliate_url?: string
}
function calcTimeRemaining(endTimeIso?: string): string {
if (!endTimeIso) return 'N/A'
const end = new Date(endTimeIso).getTime()
const now = Date.now()
const diff = end - now
if (diff <= 0) return 'Ended'
const seconds = Math.floor(diff / 1000)
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const mins = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h`
if (hours > 0) return `${hours}h ${mins}m`
if (mins > 0) return `${mins}m`
return '< 1m'
}
interface SearchResult {
domain: string
status: string
is_available: boolean | null
registrar: string | null
expiration_date: string | null
loading: boolean
inAuction: boolean
auctionData?: HotAuction
}
// ============================================================================
// MAIN PAGE
// ============================================================================
export default function RadarPage() {
const { isAuthenticated, isLoading: authLoading, domains, addDomain, user, subscription, logout, checkAuth } = useStore()
const { toast, showToast, hideToast } = useToast()
const [hotAuctions, setHotAuctions] = useState<HotAuction[]>([])
const [marketStats, setMarketStats] = useState({ totalAuctions: 0, endingSoon: 0 })
const [loadingData, setLoadingData] = useState(true)
const [tick, setTick] = useState(0)
const [searchQuery, setSearchQuery] = useState('')
const [searchResult, setSearchResult] = useState<SearchResult | null>(null)
const [addingToWatchlist, setAddingToWatchlist] = useState(false)
const [searchFocused, setSearchFocused] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
// Mobile Menu State
const [menuOpen, setMenuOpen] = useState(false)
// Sorting for Auctions
const [auctionSort, setAuctionSort] = useState<'domain' | 'time' | 'bid'>('time')
const [auctionSortDir, setAuctionSortDir] = useState<'asc' | 'desc'>('asc')
// Check auth on mount
useEffect(() => {
checkAuth()
}, [checkAuth])
// Load Data - Using same API as Market page for consistency
const loadDashboardData = useCallback(async () => {
try {
// External auctions only (Pounce Direct has no end_time)
const [feed, ending24h] = await Promise.all([
api.getMarketFeed({ source: 'external', sortBy: 'time', limit: 10 }),
api.getMarketFeed({ source: 'external', endingWithin: 24, sortBy: 'time', limit: 1 }),
])
const auctions: HotAuction[] = (feed.items || [])
.filter((item: any) => item.status === 'auction' && item.end_time)
.slice(0, 6)
.map((item: any) => ({
domain: item.domain,
current_bid: item.price || 0,
end_time: item.end_time || undefined,
platform: item.source || 'Unknown',
affiliate_url: item.url || '',
}))
setHotAuctions(auctions)
setMarketStats({
totalAuctions: feed.total || feed.auction_count || 0,
endingSoon: ending24h.total || 0,
})
} catch (error) {
console.error('Failed to load data:', error)
} finally {
setLoadingData(false)
}
}, [])
useEffect(() => {
if (!authLoading) {
if (isAuthenticated) {
loadDashboardData()
const interval = setInterval(() => setTick(t => t + 1), 30000)
return () => clearInterval(interval)
} else {
setLoadingData(false)
}
}
}, [authLoading, isAuthenticated, loadDashboardData])
// Sorted auctions
const sortedAuctions = useMemo(() => {
const mult = auctionSortDir === 'asc' ? 1 : -1
return [...hotAuctions].sort((a, b) => {
switch (auctionSort) {
case 'domain': return mult * a.domain.localeCompare(b.domain)
case 'bid': return mult * (a.current_bid - b.current_bid)
case 'time':
const aTime = a.end_time ? new Date(a.end_time).getTime() : Infinity
const bTime = b.end_time ? new Date(b.end_time).getTime() : Infinity
return mult * (aTime - bTime)
default: return 0
}
})
}, [hotAuctions, auctionSort, auctionSortDir, tick])
const handleAuctionSort = useCallback((field: typeof auctionSort) => {
if (auctionSort === field) {
setAuctionSortDir(d => d === 'asc' ? 'desc' : 'asc')
} else {
setAuctionSort(field)
setAuctionSortDir(field === 'bid' ? 'desc' : 'asc')
}
}, [auctionSort])
// Search
const handleSearch = useCallback(async (domainInput: string) => {
if (!domainInput.trim()) { setSearchResult(null); return }
const cleanDomain = domainInput.trim().toLowerCase()
setSearchResult({ domain: cleanDomain, status: 'checking', is_available: null, registrar: null, expiration_date: null, loading: true, inAuction: false })
try {
const [whoisResult, auctionsResult] = await Promise.all([
api.checkDomain(cleanDomain).catch(() => null),
api.getAuctions(cleanDomain).catch(() => ({ auctions: [] })),
])
const auctionMatch = (auctionsResult as any).auctions?.find((a: any) => a.domain.toLowerCase() === cleanDomain)
setSearchResult({
domain: whoisResult?.domain || cleanDomain,
status: whoisResult?.status || 'unknown',
is_available: whoisResult?.is_available ?? null,
registrar: whoisResult?.registrar || null,
expiration_date: whoisResult?.expiration_date || null,
loading: false,
inAuction: !!auctionMatch,
auctionData: auctionMatch,
})
} catch {
setSearchResult({ domain: cleanDomain, status: 'error', is_available: null, registrar: null, expiration_date: null, loading: false, inAuction: false })
}
}, [])
const handleAddToWatchlist = useCallback(async () => {
if (!searchQuery.trim()) return
setAddingToWatchlist(true)
try {
await addDomain(searchQuery.trim())
showToast(`Added: ${searchQuery.trim()}`, 'success')
setSearchQuery('')
setSearchResult(null)
} catch (err: any) {
showToast(err.message || 'Failed', 'error')
} finally {
setAddingToWatchlist(false)
}
}, [searchQuery, addDomain, showToast])
useEffect(() => {
const timer = setTimeout(() => {
if (searchQuery.length > 3) handleSearch(searchQuery)
else setSearchResult(null)
}, 500)
return () => clearTimeout(timer)
}, [searchQuery, handleSearch])
// Computed
const availableDomains = domains?.filter(d => d.is_available) || []
const totalDomains = domains?.length || 0
// Nav Items for Mobile Bottom Bar
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: true },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
]
// Full Navigation for Drawer
const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
const drawerNavSections = [
{
title: 'Discover',
items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]
},
{
title: 'Manage',
items: [
{ href: '/terminal/watchlist', label: 'Watchlist', icon: Eye },
{ href: '/terminal/portfolio', label: 'Portfolio', icon: Briefcase },
{ href: '/terminal/sniper', label: 'Sniper', icon: Target },
]
},
{
title: 'Monetize',
items: [
{ href: '/terminal/yield', label: 'Yield', icon: Coins, isNew: true },
{ href: '/terminal/listing', label: 'For Sale', icon: Tag },
]
}
]
return (
<div className="min-h-screen bg-[#020202]">
{/* Desktop Sidebar */}
<div className="hidden lg:block">
<Sidebar />
</div>
{/* Main Content */}
<main className="lg:pl-[240px]">
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* MOBILE HEADER - Techy Angular */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<header
className="lg:hidden sticky top-0 z-40 bg-[#020202]/95 backdrop-blur-md border-b border-white/[0.08]"
style={{ paddingTop: 'env(safe-area-inset-top)' }}
>
<div className="px-4 py-3">
{/* Top Row: Brand + Menu */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Domain Radar</span>
</div>
<div className="flex items-center gap-2 text-[10px] font-mono text-white/40">
<span>{marketStats.totalAuctions.toLocaleString()} auctions</span>
<span className="text-accent">{marketStats.endingSoon} ending 24h</span>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-4 gap-2">
<div className="bg-white/[0.02] border border-white/[0.08] p-2">
<div className="text-lg font-bold text-white tabular-nums">{totalDomains}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Tracked</div>
</div>
<div className="bg-accent/[0.05] border border-accent/20 p-2">
<div className="text-lg font-bold text-accent tabular-nums">{availableDomains.length}</div>
<div className="text-[9px] font-mono text-accent/60 uppercase tracking-wider">Available</div>
</div>
<div className="bg-white/[0.02] border border-white/[0.08] p-2">
<div className="text-lg font-bold text-white tabular-nums">{marketStats.totalAuctions}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Auctions</div>
</div>
<div className="bg-orange-500/[0.05] border border-orange-500/20 p-2">
<div className="text-lg font-bold text-orange-400 tabular-nums">{marketStats.endingSoon}</div>
<div className="text-[9px] font-mono text-orange-400/60 uppercase tracking-wider">Ending</div>
</div>
</div>
</div>
</header>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* SEARCH SECTION */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="px-4 lg:px-10 pt-4 lg:pt-10 pb-4">
{/* Desktop Hero Text */}
<div className="hidden lg:block mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Domain Radar</span>
</div>
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em] text-white mb-2">
Domain Radar
</h1>
<p className="text-white/40 text-sm font-mono max-w-lg">
Check domain availability, track your watchlist, and discover live auctions.
</p>
</div>
{/* Search Card */}
<div className="relative">
{/* Desktop Glow */}
<div className="hidden lg:block absolute -inset-6 bg-gradient-to-tr from-accent/5 via-transparent to-accent/5 blur-3xl opacity-50 pointer-events-none" />
<div className="relative bg-[#0A0A0A] border border-white/[0.08] overflow-hidden">
{/* Terminal Header */}
<div className="flex items-center justify-between px-4 py-2 border-b border-white/[0.06] bg-black/40">
<span className="text-[10px] font-mono text-white/40 flex items-center gap-2">
<Crosshair className="w-3 h-3 text-accent" />
Domain Search
</span>
<div className="flex gap-1.5">
<div className="w-2 h-2 bg-white/10" />
<div className="w-2 h-2 bg-white/10" />
<div className="w-2 h-2 bg-accent/50" />
</div>
</div>
{/* Search Input */}
<div className="p-4 lg:p-6">
<div className={clsx(
"relative border-2 transition-all duration-200",
searchFocused
? "border-accent/50 bg-accent/[0.02]"
: "border-white/[0.08] bg-white/[0.02]"
)}>
<div className="flex items-center">
<Search className={clsx(
"w-5 h-5 ml-4 transition-colors",
searchFocused ? "text-accent" : "text-white/30"
)} />
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setSearchFocused(true)}
onBlur={() => setSearchFocused(false)}
placeholder="example.com"
className="flex-1 bg-transparent px-3 py-4 text-base lg:text-lg text-white placeholder:text-white/20 outline-none font-mono"
/>
{searchQuery && (
<button
onClick={() => { setSearchQuery(''); setSearchResult(null) }}
className="p-4 text-white/30 hover:text-white active:text-white transition-colors"
>
<X className="w-5 h-5" />
</button>
)}
</div>
</div>
{/* Search Result */}
{searchResult && (
<div className="mt-4 animate-in fade-in slide-in-from-bottom-2 duration-200">
{searchResult.loading ? (
<div className="flex items-center justify-center gap-3 py-8 bg-white/[0.02] border border-white/[0.06]">
<Loader2 className="w-5 h-5 animate-spin text-accent" />
<span className="text-sm text-white/50 font-mono">Scanning...</span>
</div>
) : (
<div className={clsx(
"border-2 overflow-hidden",
searchResult.is_available
? "border-accent/40 bg-accent/[0.03]"
: "border-white/[0.08] bg-white/[0.02]"
)}>
{/* Result Header */}
<div className={clsx(
"px-4 py-3 flex items-center justify-between",
searchResult.is_available ? "bg-accent/[0.05]" : "bg-white/[0.02]"
)}>
<div className="flex items-center gap-3">
{searchResult.is_available ? (
<CheckCircle2 className="w-5 h-5 text-accent" />
) : (
<XCircle className="w-5 h-5 text-white/30" />
)}
<div>
<div className="text-base font-bold text-white font-mono">{searchResult.domain}</div>
{!searchResult.is_available && searchResult.registrar && (
<div className="text-[10px] text-white/40 font-mono">Registrar: {searchResult.registrar}</div>
)}
</div>
</div>
<span className={clsx(
"text-[10px] font-bold px-2 py-1 uppercase tracking-wider",
searchResult.is_available
? "bg-accent text-black"
: "bg-white/10 text-white/50"
)}>
{searchResult.is_available ? 'Available' : 'Taken'}
</span>
</div>
{/* Actions */}
<div className="p-4 flex gap-3">
<button
onClick={handleAddToWatchlist}
disabled={addingToWatchlist}
className={clsx(
"flex-1 py-3 text-sm font-bold flex items-center justify-center gap-2 transition-all active:scale-[0.98]",
searchResult.is_available
? "border border-white/20 text-white hover:bg-white/5"
: "border-2 border-accent text-accent hover:bg-accent/10"
)}
>
{addingToWatchlist ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
{searchResult.is_available ? 'Track' : 'Monitor'}
</button>
{searchResult.is_available && (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${searchResult.domain}`}
target="_blank"
rel="noopener noreferrer"
className="flex-1 py-3 bg-accent text-black text-sm font-bold flex items-center justify-center gap-2 hover:bg-white active:scale-[0.98] transition-all"
>
Register
<ArrowRight className="w-4 h-4" />
</a>
)}
</div>
</div>
)}
</div>
)}
{/* Hint */}
{!searchResult && (
<p className="text-[10px] text-white/30 mt-3 font-mono">
Enter domain to check availability
</p>
)}
</div>
</div>
</div>
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* WATCHLIST PREVIEW */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{domains && domains.length > 0 && (
<section className="px-4 lg:px-10 py-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Eye className="w-4 h-4 text-accent" />
<span className="text-xs font-bold text-white uppercase tracking-wider">Your Watchlist</span>
<span className="text-[10px] font-mono text-white/30">({domains.length})</span>
</div>
<Link href="/terminal/watchlist" className="text-[10px] font-mono text-accent hover:text-white transition-colors flex items-center gap-1">
Manage
<ChevronRight className="w-3 h-3" />
</Link>
</div>
{/* Domain Grid */}
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{domains.slice(0, 6).map((domain) => (
<Link
key={domain.id}
href="/terminal/watchlist"
className={clsx(
"flex items-center justify-between p-3 bg-[#020202] hover:bg-white/[0.02] active:bg-white/[0.03] transition-all",
domain.is_available && "bg-accent/[0.02]"
)}
>
<div className="flex items-center gap-3">
<div className={clsx(
"w-8 h-8 flex items-center justify-center border",
domain.is_available
? "bg-accent/10 border-accent/20"
: "bg-white/[0.02] border-white/[0.06]"
)}>
{domain.is_available ? (
<CheckCircle2 className="w-4 h-4 text-accent" />
) : (
<Eye className="w-4 h-4 text-white/30" />
)}
</div>
<div>
<div className="text-sm font-bold text-white font-mono">{domain.name}</div>
<div className="text-[10px] font-mono text-white/30">
{domain.registrar || 'Unknown registrar'}
</div>
</div>
</div>
<div className="text-right">
<div className={clsx(
"text-[10px] font-bold uppercase tracking-wider",
domain.is_available ? "text-accent" : "text-white/30"
)}>
{domain.is_available ? 'AVAILABLE' : 'TAKEN'}
</div>
{domain.last_checked && (
<div className="text-[9px] font-mono text-white/20">
{new Date(domain.last_checked).toLocaleDateString()}
</div>
)}
</div>
</Link>
))}
</div>
{domains.length > 6 && (
<Link
href="/terminal/watchlist"
className="mt-2 flex items-center justify-center gap-2 py-2 border border-white/[0.08] text-white/40 text-[10px] font-mono uppercase tracking-wider hover:text-white hover:border-white/20 active:bg-white/[0.02] transition-all"
>
View all {domains.length} domains
<ArrowRight className="w-3 h-3" />
</Link>
)}
</section>
)}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* HOT AUCTIONS */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="px-4 lg:px-10 py-4 pb-28 lg:pb-10">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Activity className="w-4 h-4 text-accent" />
<span className="text-xs font-bold text-white uppercase tracking-wider">Market Feed</span>
<span className="text-[10px] font-mono text-white/30">({sortedAuctions.length})</span>
</div>
<Link href="/terminal/market" className="text-[10px] font-mono text-accent hover:text-white transition-colors flex items-center gap-1">
View all
<ChevronRight className="w-3 h-3" />
</Link>
</div>
{loadingData ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : sortedAuctions.length > 0 ? (
<>
{/* MOBILE Auction List */}
<div className="lg:hidden space-y-2">
{sortedAuctions.map((auction, i) => (
<a
key={i}
href={auction.affiliate_url || '#'}
target="_blank"
rel="noopener noreferrer"
className="block p-3 bg-[#0A0A0A] border border-white/[0.08] active:bg-white/[0.03] transition-all"
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center shrink-0">
<Gavel className="w-4 h-4 text-white/40" />
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-bold text-white font-mono truncate">
{auction.domain}
</div>
<div className="text-[10px] font-mono text-white/30 uppercase">
{auction.platform}
</div>
</div>
</div>
<div className="text-right shrink-0">
<div className="text-sm font-bold text-accent font-mono">
${auction.current_bid.toLocaleString()}
</div>
<div className="text-[10px] font-mono text-white/40 flex items-center justify-end gap-1">
<Clock className="w-3 h-3" />
{calcTimeRemaining(auction.end_time)}
</div>
</div>
</div>
</a>
))}
</div>
{/* DESKTOP Table */}
<div className="hidden lg:block space-y-px bg-white/[0.04] border border-white/[0.08]">
{/* Desktop Table Header */}
<div className="grid grid-cols-[1fr_100px_80px_100px_40px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<button onClick={() => handleAuctionSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
Domain
{auctionSort === 'domain' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div className="text-center">Platform</div>
<button onClick={() => handleAuctionSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60">
Time
{auctionSort === 'time' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleAuctionSort('bid')} className="flex items-center gap-1 justify-end hover:text-white/60">
Bid
{auctionSort === 'bid' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div></div>
</div>
{sortedAuctions.map((auction, i) => (
<a
key={i}
href={auction.affiliate_url || '#'}
target="_blank"
rel="noopener noreferrer"
className="grid grid-cols-[1fr_100px_80px_100px_40px] gap-4 items-center p-3 bg-[#020202] hover:bg-white/[0.02] active:bg-white/[0.03] transition-all group"
>
{/* Domain */}
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center shrink-0">
<Gavel className="w-4 h-4 text-white/40 group-hover:text-accent transition-colors" />
</div>
<div className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors">
{auction.domain}
</div>
</div>
{/* Platform */}
<div className="text-center">
<span className="text-[10px] font-mono text-white/40 uppercase">{auction.platform}</span>
</div>
{/* Time */}
<div className="text-center">
<span className="text-xs font-mono text-white/50 flex items-center justify-center gap-1">
<Clock className="w-3 h-3" />
{calcTimeRemaining(auction.end_time)}
</span>
</div>
{/* Bid */}
<div className="text-right">
<div className="text-sm font-bold text-accent font-mono">
${auction.current_bid.toLocaleString()}
</div>
</div>
{/* Link */}
<div className="flex justify-end">
<ExternalLink className="w-4 h-4 text-white/20 group-hover:text-accent transition-colors" />
</div>
</a>
))}
</div>
</>
) : (
<div className="text-center py-12 border border-dashed border-white/[0.08]">
<Gavel className="w-8 h-8 text-white/10 mx-auto mb-3" />
<p className="text-white/30 text-sm font-mono">No active auctions</p>
</div>
)}
{/* Desktop Quick Links */}
<div className="hidden lg:grid grid-cols-3 gap-4 mt-8">
{[
{ href: '/terminal/watchlist', icon: Eye, label: 'Watchlist', desc: 'Track domain availability' },
{ href: '/terminal/market', icon: Gavel, label: 'Market', desc: 'Browse all auctions' },
{ href: '/terminal/intel', icon: Globe, label: 'Intel', desc: 'TLD price analysis' },
].map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-4 p-5 border border-white/[0.06] hover:border-accent/30 hover:bg-accent/[0.02] transition-all group"
>
<div className="w-12 h-12 bg-white/[0.02] border border-white/[0.08] flex items-center justify-center group-hover:border-accent/20 group-hover:bg-accent/10 transition-all">
<item.icon className="w-5 h-5 text-white/40 group-hover:text-accent transition-colors" />
</div>
<div>
<div className="text-sm font-medium text-white group-hover:text-accent transition-colors">{item.label}</div>
<div className="text-xs text-white/30">{item.desc}</div>
</div>
<ArrowRight className="w-4 h-4 text-white/10 group-hover:text-accent group-hover:translate-x-1 transition-all ml-auto" />
</Link>
))}
</div>
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* MOBILE BOTTOM NAV */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<nav
className="lg:hidden fixed bottom-0 left-0 right-0 z-50 bg-[#020202] border-t border-white/[0.08]"
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
>
<div className="flex items-stretch h-14">
{mobileNavItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={clsx(
"flex-1 flex flex-col items-center justify-center gap-0.5 relative transition-colors",
item.active ? "text-accent" : "text-white/40 active:text-white/80"
)}
>
{item.active && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-0.5 bg-accent" />
)}
<item.icon className="w-5 h-5" />
<span className="text-[9px] font-mono uppercase tracking-wider">{item.label}</span>
</Link>
))}
{/* Menu Button */}
<button
onClick={() => setMenuOpen(true)}
className="flex-1 flex flex-col items-center justify-center gap-0.5 text-white/40 active:text-white/80 transition-all"
>
<Menu className="w-5 h-5" />
<span className="text-[9px] font-mono uppercase tracking-wider">Menu</span>
</button>
</div>
</nav>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* MOBILE DRAWER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{menuOpen && (
<div className="lg:hidden fixed inset-0 z-[100]">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/80 animate-in fade-in duration-200"
onClick={() => setMenuOpen(false)}
/>
{/* Drawer Panel */}
<div className="absolute top-0 right-0 bottom-0 w-[80%] max-w-[300px] bg-[#0A0A0A] border-l border-white/[0.08] animate-in slide-in-from-right duration-300 flex flex-col" style={{ paddingTop: 'env(safe-area-inset-top)', paddingBottom: 'env(safe-area-inset-bottom)' }}>
{/* Drawer Header */}
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
<div className="flex items-center gap-3">
<Image src="/pounce-puma.png" alt="Pounce" width={28} height={28} className="object-contain" />
<div>
<h2 className="text-sm font-bold text-white tracking-wider">POUNCE</h2>
<p className="text-[9px] text-white/40 font-mono uppercase tracking-widest">Terminal v1.0</p>
</div>
</div>
<button
onClick={() => setMenuOpen(false)}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/60 hover:text-white active:bg-white/5 transition-all"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Navigation Sections */}
<div className="flex-1 overflow-y-auto py-4">
{drawerNavSections.map((section) => (
<div key={section.title} className="mb-4">
<div className="flex items-center gap-2 px-4 mb-2">
<div className="w-1 h-3 bg-accent" />
<span className="text-[9px] font-bold text-white/30 uppercase tracking-[0.2em]">{section.title}</span>
</div>
<div>
{section.items.map((item: any) => (
<Link
key={item.href}
href={item.href}
onClick={() => setMenuOpen(false)}
className="flex items-center gap-3 px-4 py-2.5 text-white/60 active:text-white active:bg-white/[0.03] transition-colors border-l-2 border-transparent active:border-accent"
>
<item.icon className="w-4 h-4 text-white/30" />
<span className="text-sm font-medium flex-1">{item.label}</span>
{item.isNew && (
<span className="px-1.5 py-0.5 text-[8px] font-bold bg-accent text-black">NEW</span>
)}
</Link>
))}
</div>
</div>
))}
{/* Settings */}
<div className="pt-3 border-t border-white/[0.08] mx-4">
<Link
href="/terminal/settings"
onClick={() => setMenuOpen(false)}
className="flex items-center gap-3 py-2.5 text-white/50 active:text-white transition-colors"
>
<Settings className="w-4 h-4" />
<span className="text-sm font-medium">Settings</span>
</Link>
{user?.is_admin && (
<Link
href="/admin"
onClick={() => setMenuOpen(false)}
className="flex items-center gap-3 py-2.5 text-amber-500/70 active:text-amber-400 transition-colors"
>
<Shield className="w-4 h-4" />
<span className="text-sm font-medium">Admin</span>
</Link>
)}
</div>
</div>
{/* User Card */}
<div className="p-4 bg-white/[0.02] border-t border-white/[0.08]">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 bg-accent/10 border border-accent/20 flex items-center justify-center">
<TierIcon className="w-4 h-4 text-accent" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white truncate">
{user?.name || user?.email?.split('@')[0] || 'User'}
</p>
<p className="text-[9px] font-mono text-white/40 uppercase tracking-wider">{tierName}</p>
</div>
</div>
{tierName === 'Scout' && (
<Link
href="/pricing"
onClick={() => setMenuOpen(false)}
className="flex items-center justify-center gap-2 w-full py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2"
>
<Sparkles className="w-3 h-3" />
Upgrade
</Link>
)}
<button
onClick={() => { logout(); setMenuOpen(false) }}
className="flex items-center justify-center gap-2 w-full py-2 border border-white/10 text-white/40 text-[10px] font-mono uppercase tracking-wider active:bg-white/5 transition-all"
>
<LogOut className="w-3 h-3" />
Sign out
</button>
</div>
</div>
</div>
)}
</main>
{/* Toast */}
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
</div>
)
}

View File

@ -270,7 +270,7 @@ export default function SettingsPage() {
// Mobile Nav - same as Intel page
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/hunt', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
@ -278,7 +278,7 @@ export default function SettingsPage() {
const drawerNavSections = [
{ title: 'Discover', items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/hunt', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]},

View File

@ -145,7 +145,7 @@ export default function SniperAlertsPage() {
// Mobile Nav
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/hunt', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
@ -156,7 +156,7 @@ export default function SniperAlertsPage() {
const drawerNavSections = [
{ title: 'Discover', items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/hunt', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]},

View File

@ -247,9 +247,9 @@ export default function WatchlistPage() {
// Mobile Nav
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/hunt', label: 'Hunt', icon: Target, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: true },
{ href: '/terminal/portfolio', label: 'Portfolio', icon: Briefcase, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
]
@ -260,8 +260,7 @@ export default function WatchlistPage() {
{
title: 'Discover',
items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/hunt', label: 'Hunt', icon: Target },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]
},

View File

@ -188,7 +188,7 @@ export default function WelcomePage() {
{/* Go to Dashboard */}
<div className="text-center">
<Link
href="/terminal/radar"
href="/terminal/hunt"
className="inline-flex items-center gap-2 px-8 py-4 bg-accent text-background font-medium rounded-xl
hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
>

View File

@ -360,7 +360,7 @@ export default function YieldPage() {
const stats = dashboard?.stats
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/hunt', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
@ -371,7 +371,7 @@ export default function YieldPage() {
const drawerNavSections = [
{ title: 'Discover', items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/hunt', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]},

View File

@ -92,7 +92,7 @@ export function AdminLayout({
<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('/terminal/radar')}
onClick={() => router.push('/terminal/hunt')}
className="px-4 py-2 bg-accent text-background rounded-lg font-medium"
>
Go to Dashboard
@ -288,7 +288,7 @@ function AdminSidebar({
<div className="border-t border-border/30 py-4 px-3 space-y-2">
{/* Back to User Dashboard */}
<Link
href="/terminal/radar"
href="/terminal/hunt"
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"

View File

@ -219,7 +219,7 @@ export function DomainChecker() {
<span className="text-sm font-mono text-emerald-100/80">Market Open</span>
</div>
<Link
href={isAuthenticated ? '/terminal/radar' : '/register'}
href={isAuthenticated ? '/terminal/hunt' : '/register'}
className="group relative px-6 py-3 bg-emerald-500 hover:bg-emerald-400 transition-colors text-black font-bold uppercase tracking-wider text-xs flex items-center gap-2"
style={{ clipPath: 'polygon(12px 0, 100% 0, 100% 100%, 0 100%, 0 12px)' }}
>
@ -270,7 +270,7 @@ export function DomainChecker() {
<div className="p-4 bg-rose-950/[0.05] border-t border-rose-500/10 flex items-center justify-between">
<span className="text-xs text-rose-500/50 font-mono">Target this asset?</span>
<Link
href={isAuthenticated ? '/terminal/radar' : '/register'}
href={isAuthenticated ? '/terminal/hunt' : '/register'}
className="group relative px-6 py-3 bg-rose-500 hover:bg-rose-400 transition-colors text-black font-bold uppercase tracking-wider text-xs flex items-center gap-2"
style={{ clipPath: 'polygon(12px 0, 100% 0, 100% 100%, 0 100%, 0 12px)' }}
>

View File

@ -86,7 +86,7 @@ export function Header() {
<nav className="hidden sm:flex items-center h-full gap-3">
{isAuthenticated ? (
<Link
href="/terminal/radar"
href="/terminal/hunt"
className="flex items-center gap-2 h-9 px-5 text-xs bg-accent text-black font-bold uppercase tracking-wider hover:bg-white transition-colors"
>
<Target className="w-4 h-4" />
@ -194,7 +194,7 @@ export function Header() {
</div>
<Link
href="/terminal/radar"
href="/terminal/hunt"
onClick={() => setMenuOpen(false)}
className="flex items-center justify-center gap-2 w-full py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2"
>

View File

@ -5,9 +5,7 @@ import Image from 'next/image'
import { usePathname } from 'next/navigation'
import { useStore } from '@/lib/store'
import {
LayoutDashboard,
Eye,
Gavel,
TrendingUp,
Settings,
ChevronLeft,
@ -22,7 +20,6 @@ import {
Tag,
Target,
Coins,
Radar,
Briefcase,
MessageSquare,
} from 'lucide-react'
@ -67,7 +64,7 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
const availableCount = domains?.filter(d => d.is_available).length || 0
const isTycoon = tierName.toLowerCase() === 'tycoon'
// SECTION 1: Discover - Radar first, then external market data
// SECTION 1: Discover - Hunt is the main discovery hub
const discoverItems = [
{
href: '/terminal/hunt',
@ -75,18 +72,6 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
icon: Crosshair,
badge: null,
},
{
href: '/terminal/radar',
label: 'RADAR',
icon: Radar,
badge: null,
},
{
href: '/terminal/market',
label: 'MARKET',
icon: Gavel,
badge: null,
},
{
href: '/terminal/intel',
label: 'INTEL',
@ -115,12 +100,6 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
icon: Briefcase,
badge: null,
},
{
href: '/terminal/cfo',
label: 'CFO',
icon: Shield,
badge: null,
},
{
href: '/terminal/inbox',
label: 'INBOX',
@ -163,7 +142,7 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
]
const isActive = (href: string) => {
if (href === '/terminal/radar') return pathname === '/terminal/radar' || pathname === '/terminal' || pathname === '/terminal/dashboard'
if (href === '/terminal/hunt') return pathname === '/terminal/hunt' || pathname === '/terminal' || pathname === '/terminal/dashboard'
return pathname.startsWith(href)
}

View File

@ -0,0 +1,644 @@
'use client'
import { useEffect, useState, useMemo, useCallback } from 'react'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { useAnalyzePanelStore } from '@/lib/analyze-store'
import {
ExternalLink,
Loader2,
Diamond,
Zap,
ChevronLeft,
ChevronRight,
ChevronUp,
ChevronDown,
RefreshCw,
Clock,
Search,
Eye,
EyeOff,
ShieldCheck,
Ban,
X,
Filter,
Shield,
} from 'lucide-react'
import clsx from 'clsx'
// ============================================================================
// TYPES
// ============================================================================
interface MarketItem {
id: string
domain: string
tld: string
price: number
currency: string
price_type: 'bid' | 'fixed' | 'negotiable'
status: 'auction' | 'instant'
source: string
is_pounce: boolean
verified: boolean
time_remaining?: string
end_time?: string
num_bids?: number
slug?: string
seller_verified: boolean
url: string
is_external: boolean
pounce_score: number
}
type SourceFilter = 'all' | 'pounce' | 'external'
type PriceRange = 'all' | 'low' | 'mid' | 'high'
// ============================================================================
// HELPERS
// ============================================================================
function calcTimeRemaining(endTimeIso?: string): string {
if (!endTimeIso) return 'N/A'
const end = new Date(endTimeIso).getTime()
const now = Date.now()
const diff = end - now
if (diff <= 0) return 'Ended'
const seconds = Math.floor(diff / 1000)
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const mins = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h`
if (hours > 0) return `${hours}h ${mins}m`
if (mins > 0) return `${mins}m`
return '< 1m'
}
function getSecondsUntilEnd(endTimeIso?: string): number {
if (!endTimeIso) return Infinity
const diff = new Date(endTimeIso).getTime() - Date.now()
return diff > 0 ? diff / 1000 : -1
}
function formatPrice(price: number, currency = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
maximumFractionDigits: 0,
}).format(price)
}
function isSpam(domain: string): boolean {
const name = domain.split('.')[0]
if (name.includes('-')) return true
if (name.length > 4 && /\d/.test(name)) return true
if (/^\d+$/.test(name)) return true
return false
}
// ============================================================================
// COMPONENT
// ============================================================================
interface AuctionsTabProps {
showToast: (message: string, type?: 'success' | 'error' | 'info') => void
}
export function AuctionsTab({ showToast }: AuctionsTabProps) {
const openAnalyze = useAnalyzePanelStore((s) => s.open)
const [items, setItems] = useState<MarketItem[]>([])
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [stats, setStats] = useState({ total: 0, pounceCount: 0, auctionCount: 0, highScore: 0 })
const [sourceFilter, setSourceFilter] = useState<SourceFilter>('all')
const [searchQuery, setSearchQuery] = useState('')
const [priceRange, setPriceRange] = useState<PriceRange>('all')
const [hideSpam, setHideSpam] = useState(true)
const [tldFilter, setTldFilter] = useState<string>('all')
const [searchFocused, setSearchFocused] = useState(false)
const [trackedDomains, setTrackedDomains] = useState<Set<string>>(new Set())
const [trackingInProgress, setTrackingInProgress] = useState<string | null>(null)
const [page, setPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const ITEMS_PER_PAGE = 50
const [filtersOpen, setFiltersOpen] = useState(false)
const [sortField, setSortField] = useState<'domain' | 'score' | 'price' | 'time'>('time')
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
const loadData = useCallback(async (currentPage = 1) => {
setLoading(true)
try {
const result = await api.getMarketFeed({
source: sourceFilter,
keyword: searchQuery || undefined,
tld: tldFilter === 'all' ? undefined : tldFilter,
minPrice: priceRange === 'low' ? undefined : priceRange === 'mid' ? 100 : priceRange === 'high' ? 1000 : undefined,
maxPrice: priceRange === 'low' ? 100 : priceRange === 'mid' ? 1000 : undefined,
sortBy: 'time',
limit: ITEMS_PER_PAGE,
offset: (currentPage - 1) * ITEMS_PER_PAGE,
})
setItems(result.items || [])
setStats({
total: result.total,
pounceCount: result.pounce_direct_count,
auctionCount: result.auction_count,
highScore: (result.items || []).filter((i: MarketItem) => i.pounce_score >= 80).length,
})
setTotalPages(Math.ceil((result.total || 0) / ITEMS_PER_PAGE))
} catch (error) {
console.error('Failed to load market data:', error)
setItems([])
} finally {
setLoading(false)
}
}, [sourceFilter, searchQuery, priceRange, tldFilter])
useEffect(() => {
setPage(1)
loadData(1)
}, [loadData])
const handlePageChange = useCallback((newPage: number) => {
setPage(newPage)
loadData(newPage)
}, [loadData])
const handleRefresh = useCallback(async () => {
setRefreshing(true)
await loadData()
setRefreshing(false)
}, [loadData])
useEffect(() => {
const loadTrackedDomains = async () => {
try {
const result = await api.getDomains(1, 100)
const domainSet = new Set(result.domains.map((d: any) => d.name))
setTrackedDomains(domainSet)
} catch (error) {
console.error('Failed to load tracked domains:', error)
}
}
loadTrackedDomains()
}, [])
const handleTrack = useCallback(async (domain: string) => {
if (trackingInProgress) return
setTrackingInProgress(domain)
try {
if (trackedDomains.has(domain)) {
const result = await api.getDomains(1, 100)
const domainToDelete = result.domains.find((d: any) => d.name === domain)
if (domainToDelete) {
await api.deleteDomain(domainToDelete.id)
setTrackedDomains((prev) => {
const next = new Set(Array.from(prev))
next.delete(domain)
return next
})
showToast(`Removed: ${domain}`, 'success')
}
} else {
await api.addDomain(domain)
setTrackedDomains((prev) => new Set([...Array.from(prev), domain]))
showToast(`Tracking: ${domain}`, 'success')
}
} catch (error: any) {
showToast(error.message || 'Failed', 'error')
} finally {
setTrackingInProgress(null)
}
}, [trackedDomains, trackingInProgress, showToast])
const filteredItems = useMemo(() => {
let filtered = items
const nowMs = Date.now()
filtered = filtered.filter((item) => {
if (item.status !== 'auction') return true
if (!item.end_time) return true
const t = Date.parse(item.end_time)
if (Number.isNaN(t)) return true
return t > nowMs - 2000
})
if (searchQuery && !loading) {
const query = searchQuery.toLowerCase()
filtered = filtered.filter((item) => item.domain.toLowerCase().includes(query))
}
if (hideSpam) {
filtered = filtered.filter((item) => !isSpam(item.domain))
}
const mult = sortDirection === 'asc' ? 1 : -1
filtered.sort((a, b) => {
switch (sortField) {
case 'domain':
return mult * a.domain.localeCompare(b.domain)
case 'score':
return mult * ((a.pounce_score || 0) - (b.pounce_score || 0))
case 'price':
return mult * (a.price - b.price)
case 'time':
const aTime = a.end_time ? new Date(a.end_time).getTime() : Infinity
const bTime = b.end_time ? new Date(b.end_time).getTime() : Infinity
return mult * (aTime - bTime)
default:
return 0
}
})
return filtered
}, [items, searchQuery, loading, hideSpam, sortField, sortDirection])
const handleSort = useCallback((field: typeof sortField) => {
if (sortField === field) {
setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc'))
} else {
setSortField(field)
setSortDirection(field === 'time' || field === 'price' ? 'asc' : 'desc')
}
}, [sortField])
const activeFiltersCount = [sourceFilter !== 'all', priceRange !== 'all', tldFilter !== 'all', hideSpam].filter(Boolean).length
if (loading && items.length === 0) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
)
}
return (
<div className="space-y-4">
{/* Search & Filters */}
<div className="space-y-3">
{/* Search */}
<div className={clsx(
"relative border transition-all duration-200",
searchFocused ? "border-accent/50 bg-accent/[0.02]" : "border-white/[0.08] bg-white/[0.02]"
)}>
<div className="flex items-center">
<Search className={clsx("w-4 h-4 ml-3 transition-colors", searchFocused ? "text-accent" : "text-white/30")} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setSearchFocused(true)}
onBlur={() => setSearchFocused(false)}
placeholder="Filter auctions..."
className="flex-1 bg-transparent px-3 py-3 text-sm text-white placeholder:text-white/20 outline-none font-mono"
/>
{searchQuery && (
<button onClick={() => setSearchQuery('')} className="p-3 text-white/30 hover:text-white transition-colors">
<X className="w-4 h-4" />
</button>
)}
<button onClick={handleRefresh} disabled={refreshing} className="p-3 text-white/30 hover:text-white transition-colors">
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
</button>
</div>
</div>
{/* Filter Toggle */}
<button
onClick={() => setFiltersOpen(!filtersOpen)}
className={clsx(
"flex items-center justify-between w-full py-2 px-3 border transition-colors",
filtersOpen ? "border-accent/30 bg-accent/[0.05]" : "border-white/[0.08] bg-white/[0.02]"
)}
>
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-white/40" />
<span className="text-xs font-mono text-white/60">Filters</span>
{activeFiltersCount > 0 && <span className="px-1.5 py-0.5 text-[9px] font-bold bg-accent text-black">{activeFiltersCount}</span>}
</div>
<ChevronRight className={clsx("w-4 h-4 text-white/30 transition-transform", filtersOpen && "rotate-90")} />
</button>
{/* Filters Panel */}
{filtersOpen && (
<div className="p-3 border border-white/[0.08] bg-white/[0.02] space-y-3 animate-in fade-in slide-in-from-top-2 duration-200">
{/* Source */}
<div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider mb-2">Source</div>
<div className="flex gap-2">
{[
{ value: 'all', label: 'All' },
{ value: 'pounce', label: 'Pounce', icon: Diamond },
{ value: 'external', label: 'External' },
].map((item) => (
<button
key={item.value}
onClick={() => setSourceFilter(item.value as SourceFilter)}
className={clsx(
"flex-1 py-2 text-[10px] font-bold uppercase tracking-wider border transition-colors flex items-center justify-center gap-1",
sourceFilter === item.value ? "border-accent bg-accent/10 text-accent" : "border-white/[0.08] text-white/40"
)}
>
{item.icon && <item.icon className="w-3 h-3" />}
{item.label}
</button>
))}
</div>
</div>
{/* TLD */}
<div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider mb-2">TLD</div>
<div className="flex gap-2 flex-wrap">
{['all', 'com', 'ai', 'io', 'net'].map((tld) => (
<button
key={tld}
onClick={() => setTldFilter(tld)}
className={clsx(
"px-3 py-1.5 text-[10px] font-mono uppercase border transition-colors",
tldFilter === tld ? "border-accent bg-accent/10 text-accent" : "border-white/[0.08] text-white/40"
)}
>
{tld === 'all' ? 'All' : `.${tld}`}
</button>
))}
</div>
</div>
{/* Price */}
<div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider mb-2">Price</div>
<div className="flex gap-2">
{[
{ value: 'all', label: 'All' },
{ value: 'low', label: '< $100' },
{ value: 'mid', label: '< $1k' },
{ value: 'high', label: '$1k+' },
].map((item) => (
<button
key={item.value}
onClick={() => setPriceRange(item.value as PriceRange)}
className={clsx(
"flex-1 py-1.5 text-[10px] font-mono border transition-colors",
priceRange === item.value ? "border-amber-400 bg-amber-400/10 text-amber-400" : "border-white/[0.08] text-white/40"
)}
>
{item.label}
</button>
))}
</div>
</div>
{/* Spam Filter */}
<button
onClick={() => setHideSpam(!hideSpam)}
className={clsx(
"flex items-center justify-between w-full py-2 px-3 border transition-colors",
hideSpam ? "border-white/20 bg-white/[0.05]" : "border-white/[0.08]"
)}
>
<div className="flex items-center gap-2">
<Ban className="w-4 h-4 text-white/40" />
<span className="text-xs font-mono text-white/60">Hide spam domains</span>
</div>
<div className={clsx("w-4 h-4 border flex items-center justify-center", hideSpam ? "border-accent bg-accent" : "border-white/30")}>
{hideSpam && <span className="text-black text-[10px] font-bold"></span>}
</div>
</button>
</div>
)}
</div>
{/* Stats Bar */}
<div className="flex items-center justify-between text-[10px] font-mono text-white/40">
<span>{filteredItems.length} domains shown</span>
<span>{filteredItems.filter((i) => i.pounce_score >= 80).length} high score</span>
</div>
{/* Results */}
{filteredItems.length === 0 ? (
<div className="text-center py-16 border border-dashed border-white/[0.08]">
<Search className="w-8 h-8 text-white/10 mx-auto mb-3" />
<p className="text-white/40 text-sm font-mono">No domains found</p>
<p className="text-white/25 text-xs font-mono mt-1">Try adjusting filters</p>
</div>
) : (
<>
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{/* Desktop Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_80px_100px_80px_120px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<button onClick={() => handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
Domain
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('score')} className="flex items-center gap-1 justify-center hover:text-white/60">
Score
{sortField === 'score' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('price')} className="flex items-center gap-1 justify-end hover:text-white/60">
Price
{sortField === 'price' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60">
Time
{sortField === 'time' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div className="text-right">Actions</div>
</div>
{filteredItems.map((item) => {
const timeLeftSec = getSecondsUntilEnd(item.end_time)
const isUrgent = timeLeftSec > 0 && timeLeftSec < 3600
const isPounce = item.is_pounce
const displayTime = item.status === 'auction' ? calcTimeRemaining(item.end_time) : null
const isTracked = trackedDomains.has(item.domain)
const isTracking = trackingInProgress === item.domain
return (
<div key={item.id} className={clsx("bg-[#020202] hover:bg-white/[0.02] transition-all", isPounce && "bg-accent/[0.02]")}>
{/* Mobile Row */}
<div className="lg:hidden p-3">
<div className="flex items-start justify-between gap-3 mb-2">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className={clsx("w-8 h-8 flex items-center justify-center border shrink-0", isPounce ? "bg-accent/10 border-accent/20" : "bg-white/[0.02] border-white/[0.06]")}>
{isPounce ? <Diamond className="w-4 h-4 text-accent" /> : <span className="text-[9px] font-mono text-white/40">{item.source.substring(0, 2).toUpperCase()}</span>}
</div>
<div className="min-w-0 flex-1">
<button onClick={() => openAnalyze(item.domain)} className="text-sm font-bold text-white font-mono truncate text-left">
{item.domain}
</button>
<div className="flex items-center gap-2 text-[10px] font-mono text-white/30">
<span className="uppercase">{item.source}</span>
<span className="text-white/10">|</span>
<span className={clsx(isUrgent && "text-orange-400")}>{isPounce ? 'Instant' : displayTime || 'N/A'}</span>
</div>
</div>
</div>
<div className="text-right shrink-0">
<div className={clsx("text-base font-bold font-mono", isPounce ? "text-accent" : "text-white")}>{formatPrice(item.price)}</div>
<div className={clsx("text-[9px] font-mono px-1 py-0.5 inline-block", item.pounce_score >= 80 ? "text-accent bg-accent/10" : item.pounce_score >= 50 ? "text-amber-400 bg-amber-400/10" : "text-white/30 bg-white/5")}>
Score {item.pounce_score}
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => handleTrack(item.domain)}
disabled={isTracking}
className={clsx(
"flex-1 py-2 text-[10px] font-bold uppercase tracking-wider border flex items-center justify-center gap-1.5 transition-all",
isTracked ? "border-accent bg-accent/10 text-accent" : "border-white/[0.08] text-white/40"
)}
>
{isTracking ? <Loader2 className="w-3 h-3 animate-spin" /> : isTracked ? <Eye className="w-3 h-3" /> : <EyeOff className="w-3 h-3" />}
{isTracked ? 'Tracked' : 'Track'}
</button>
<button onClick={() => openAnalyze(item.domain)} className="w-10 py-2 text-[10px] font-bold uppercase tracking-wider border border-white/[0.08] text-white/50 flex items-center justify-center transition-all hover:text-white hover:bg-white/5">
<Shield className="w-3.5 h-3.5" />
</button>
<a
href={item.url}
target={isPounce ? '_self' : '_blank'}
rel={isPounce ? undefined : 'noopener noreferrer'}
className={clsx("flex-1 py-2 text-[10px] font-bold uppercase tracking-wider flex items-center justify-center gap-1.5 transition-all", isPounce ? "bg-accent text-black" : "bg-white/10 text-white")}
>
{isPounce ? 'Buy' : 'Bid'}
{!isPounce && <ExternalLink className="w-3 h-3" />}
</a>
</div>
</div>
{/* Desktop Row */}
<div className="hidden lg:grid grid-cols-[1fr_80px_100px_80px_120px] gap-4 items-center p-3 group">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className={clsx("w-8 h-8 flex items-center justify-center border shrink-0", isPounce ? "bg-accent/10 border-accent/20" : "bg-white/[0.02] border-white/[0.06]")}>
{isPounce ? <Diamond className="w-4 h-4 text-accent" /> : <span className="text-[9px] font-mono text-white/40">{item.source.substring(0, 2).toUpperCase()}</span>}
</div>
<div className="min-w-0">
<button onClick={() => openAnalyze(item.domain)} className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left">
{item.domain}
</button>
<div className="flex items-center gap-2 text-[10px] font-mono text-white/30">
<span className="uppercase">{item.source}</span>
{isPounce && item.verified && (
<>
<span className="text-white/10">|</span>
<span className="text-accent flex items-center gap-0.5">
<ShieldCheck className="w-3 h-3" />
Verified
</span>
</>
)}
{item.num_bids ? (
<>
<span className="text-white/10">|</span>
{item.num_bids} bids
</>
) : null}
</div>
</div>
</div>
<div className="w-16 text-center shrink-0">
<span className={clsx("text-xs font-mono font-bold px-2 py-0.5", item.pounce_score >= 80 ? "text-accent bg-accent/10" : item.pounce_score >= 50 ? "text-amber-400 bg-amber-400/10" : "text-white/40 bg-white/5")}>
{item.pounce_score}
</span>
</div>
<div className="w-24 text-right shrink-0">
<div className={clsx("font-mono text-sm font-bold", isPounce ? "text-accent" : "text-white")}>{formatPrice(item.price)}</div>
<div className="text-[9px] font-mono text-white/30 uppercase">{item.price_type === 'bid' ? 'Bid' : 'Buy Now'}</div>
</div>
<div className="w-20 text-center shrink-0">
{isPounce ? (
<span className="text-xs text-accent font-mono flex items-center justify-center gap-1">
<Zap className="w-3 h-3" />
Instant
</span>
) : (
<span className={clsx("text-xs font-mono", isUrgent ? "text-orange-400" : "text-white/50")}>{displayTime || 'N/A'}</span>
)}
</div>
<div className="flex items-center gap-2 shrink-0 opacity-50 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleTrack(item.domain)}
disabled={isTracking}
className={clsx(
"w-7 h-7 flex items-center justify-center border transition-colors",
isTracked ? "bg-accent/10 text-accent border-accent/20 hover:bg-red-500/10 hover:text-red-400 hover:border-red-500/20" : "text-white/30 border-white/10 hover:text-accent hover:bg-accent/10 hover:border-accent/20"
)}
>
{isTracking ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : isTracked ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
</button>
<button onClick={() => openAnalyze(item.domain)} className="w-7 h-7 flex items-center justify-center border transition-colors text-white/30 border-white/10 hover:text-accent hover:bg-accent/10 hover:border-accent/20">
<Shield className="w-3.5 h-3.5" />
</button>
<a
href={item.url}
target={isPounce ? '_self' : '_blank'}
rel={isPounce ? undefined : 'noopener noreferrer'}
className={clsx("h-7 px-3 flex items-center gap-1.5 text-xs font-bold transition-colors", isPounce ? "bg-accent text-black hover:bg-white" : "bg-white/10 text-white hover:bg-white/20")}
>
{isPounce ? 'Buy' : 'Bid'}
{!isPounce && <ExternalLink className="w-3 h-3" />}
</a>
</div>
</div>
</div>
)
})}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between pt-4">
<div className="text-[10px] text-white/40 font-mono uppercase tracking-wider">
Page {page}/{totalPages}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => handlePageChange(page - 1)}
disabled={page === 1}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-xs text-white/50 font-mono px-2">
{page}/{totalPages}
</span>
<button
onClick={() => handlePageChange(page + 1)}
disabled={page === totalPages}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</>
)}
</div>
)
}

View File

@ -0,0 +1,169 @@
'use client'
import { useState } from 'react'
import { Download, Clock, Globe, Loader2, Search, Filter, ChevronRight, AlertCircle } from 'lucide-react'
import clsx from 'clsx'
// ============================================================================
// DROPS TAB - Zone File Analysis
// Placeholder component - User will set up the data source
// ============================================================================
interface DropsTabProps {
showToast: (message: string, type?: 'success' | 'error' | 'info') => void
}
export function DropsTab({ showToast }: DropsTabProps) {
const [selectedTld, setSelectedTld] = useState<string>('com')
const [loading, setLoading] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [searchFocused, setSearchFocused] = useState(false)
// TODO: Replace with real API call to zone file analysis endpoint
const handleRefresh = async () => {
setLoading(true)
// Simulated delay - replace with actual API call
await new Promise(resolve => setTimeout(resolve, 1500))
setLoading(false)
showToast('Zone file data will be available once configured', 'info')
}
const tlds = ['com', 'net', 'org', 'io', 'ai', 'co']
return (
<div className="space-y-4">
{/* Header Controls */}
<div className="space-y-3">
{/* Search */}
<div className={clsx(
"relative border transition-all duration-200",
searchFocused ? "border-accent/50 bg-accent/[0.02]" : "border-white/[0.08] bg-white/[0.02]"
)}>
<div className="flex items-center">
<Search className={clsx("w-4 h-4 ml-3 transition-colors", searchFocused ? "text-accent" : "text-white/30")} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setSearchFocused(true)}
onBlur={() => setSearchFocused(false)}
placeholder="Search freshly dropped domains..."
className="flex-1 bg-transparent px-3 py-3 text-sm text-white placeholder:text-white/20 outline-none font-mono"
/>
</div>
</div>
{/* TLD Selector */}
<div className="flex gap-2 flex-wrap">
{tlds.map((tld) => (
<button
key={tld}
onClick={() => setSelectedTld(tld)}
className={clsx(
"px-3 py-1.5 text-[10px] font-mono uppercase border transition-colors",
selectedTld === tld
? "border-accent bg-accent/10 text-accent"
: "border-white/[0.08] text-white/40 hover:text-white/60"
)}
>
.{tld}
</button>
))}
</div>
</div>
{/* Info Card - Setup Required */}
<div className="border border-amber-500/20 bg-amber-500/[0.05] p-4">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
<div className="flex-1">
<h3 className="text-sm font-bold text-amber-400 mb-1">Zone File Integration</h3>
<p className="text-xs text-white/50 leading-relaxed mb-3">
This feature analyzes zone files to find freshly dropped domains.
Configure your zone file data source to see real-time drops.
</p>
<div className="flex gap-2">
<button
onClick={handleRefresh}
disabled={loading}
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider border border-amber-500/30 text-amber-400 hover:bg-amber-500/10 transition-colors flex items-center gap-1.5"
>
{loading ? <Loader2 className="w-3 h-3 animate-spin" /> : <Download className="w-3 h-3" />}
Refresh Data
</button>
</div>
</div>
</div>
</div>
{/* Feature Preview Cards */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
<div className="border border-white/[0.08] bg-white/[0.02] p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-accent/10 border border-accent/20 flex items-center justify-center">
<Clock className="w-5 h-5 text-accent" />
</div>
<div>
<div className="text-sm font-bold text-white">Real-time Drops</div>
<div className="text-[10px] font-mono text-white/40">Updated every hour</div>
</div>
</div>
<p className="text-xs text-white/30">
Monitor domains as they expire and become available for registration.
</p>
</div>
<div className="border border-white/[0.08] bg-white/[0.02] p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-white/[0.02] border border-white/[0.08] flex items-center justify-center">
<Filter className="w-5 h-5 text-white/40" />
</div>
<div>
<div className="text-sm font-bold text-white">Smart Filters</div>
<div className="text-[10px] font-mono text-white/40">Length, keywords, patterns</div>
</div>
</div>
<p className="text-xs text-white/30">
Filter by domain length, keywords, character patterns, and more.
</p>
</div>
<div className="border border-white/[0.08] bg-white/[0.02] p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 bg-white/[0.02] border border-white/[0.08] flex items-center justify-center">
<Globe className="w-5 h-5 text-white/40" />
</div>
<div>
<div className="text-sm font-bold text-white">Multi-TLD</div>
<div className="text-[10px] font-mono text-white/40">All major TLDs</div>
</div>
</div>
<p className="text-xs text-white/30">
Track drops across .com, .net, .org, .io, .ai, and more.
</p>
</div>
</div>
{/* Placeholder Table */}
<div className="border border-white/[0.08] bg-white/[0.02]">
<div className="px-4 py-3 border-b border-white/[0.08] flex items-center justify-between">
<div>
<div className="text-[10px] font-mono uppercase tracking-widest text-white/40">Freshly Dropped</div>
<div className="text-sm font-bold text-white">.{selectedTld.toUpperCase()} Domains</div>
</div>
<div className="text-[10px] font-mono text-white/30">
Awaiting data...
</div>
</div>
<div className="p-8 text-center">
<Globe className="w-12 h-12 text-white/10 mx-auto mb-4" />
<p className="text-white/40 text-sm font-mono mb-2">No zone file data available</p>
<p className="text-white/25 text-xs font-mono">
Configure zone file integration to see dropped domains
</p>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,349 @@
'use client'
import { useState, useCallback, useEffect, useRef } from 'react'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { useAnalyzePanelStore } from '@/lib/analyze-store'
import {
Search,
X,
Loader2,
CheckCircle2,
XCircle,
Eye,
ArrowRight,
Shield,
Globe,
Calendar,
Building,
} from 'lucide-react'
import clsx from 'clsx'
// ============================================================================
// TYPES
// ============================================================================
interface SearchResult {
domain: string
status: string
is_available: boolean | null
registrar: string | null
expiration_date: string | null
loading: boolean
}
// ============================================================================
// COMPONENT
// ============================================================================
interface SearchTabProps {
showToast: (message: string, type?: 'success' | 'error' | 'info') => void
}
export function SearchTab({ showToast }: SearchTabProps) {
const { addDomain } = useStore()
const openAnalyze = useAnalyzePanelStore((s) => s.open)
const [searchQuery, setSearchQuery] = useState('')
const [searchResult, setSearchResult] = useState<SearchResult | null>(null)
const [addingToWatchlist, setAddingToWatchlist] = useState(false)
const [searchFocused, setSearchFocused] = useState(false)
const [recentSearches, setRecentSearches] = useState<string[]>([])
const searchInputRef = useRef<HTMLInputElement>(null)
// Load recent searches from localStorage
useEffect(() => {
const stored = localStorage.getItem('pounce_recent_searches')
if (stored) {
try {
setRecentSearches(JSON.parse(stored).slice(0, 5))
} catch {
// Ignore invalid JSON
}
}
}, [])
// Save to recent searches
const saveToRecent = useCallback((domain: string) => {
setRecentSearches((prev) => {
const filtered = prev.filter((d) => d !== domain)
const updated = [domain, ...filtered].slice(0, 5)
localStorage.setItem('pounce_recent_searches', JSON.stringify(updated))
return updated
})
}, [])
// Search Handler
const handleSearch = useCallback(async (domainInput: string) => {
if (!domainInput.trim()) {
setSearchResult(null)
return
}
const cleanDomain = domainInput.trim().toLowerCase()
setSearchResult({ domain: cleanDomain, status: 'checking', is_available: null, registrar: null, expiration_date: null, loading: true })
try {
const whoisResult = await api.checkDomain(cleanDomain).catch(() => null)
setSearchResult({
domain: whoisResult?.domain || cleanDomain,
status: whoisResult?.status || 'unknown',
is_available: whoisResult?.is_available ?? null,
registrar: whoisResult?.registrar || null,
expiration_date: whoisResult?.expiration_date || null,
loading: false,
})
saveToRecent(cleanDomain)
} catch {
setSearchResult({ domain: cleanDomain, status: 'error', is_available: null, registrar: null, expiration_date: null, loading: false })
}
}, [saveToRecent])
const handleAddToWatchlist = useCallback(async () => {
if (!searchQuery.trim()) return
setAddingToWatchlist(true)
try {
await addDomain(searchQuery.trim())
showToast(`Added: ${searchQuery.trim()}`, 'success')
} catch (err: any) {
showToast(err.message || 'Failed', 'error')
} finally {
setAddingToWatchlist(false)
}
}, [searchQuery, addDomain, showToast])
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
if (searchQuery.length > 3) handleSearch(searchQuery)
else setSearchResult(null)
}, 500)
return () => clearTimeout(timer)
}, [searchQuery, handleSearch])
// Focus input on mount
useEffect(() => {
searchInputRef.current?.focus()
}, [])
return (
<div className="space-y-6">
{/* Search Card */}
<div className="relative">
<div className="absolute -inset-6 bg-gradient-to-tr from-accent/5 via-transparent to-accent/5 blur-3xl opacity-50 pointer-events-none hidden lg:block" />
<div className="relative bg-[#0A0A0A] border border-white/[0.08] overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 border-b border-white/[0.06] bg-black/40">
<span className="text-[10px] font-mono text-white/40 flex items-center gap-2">
<Search className="w-3 h-3 text-accent" />
Domain Search
</span>
<div className="flex gap-1.5">
<div className="w-2 h-2 bg-white/10" />
<div className="w-2 h-2 bg-white/10" />
<div className="w-2 h-2 bg-accent/50" />
</div>
</div>
<div className="p-4 lg:p-6">
<div className={clsx(
"relative border-2 transition-all duration-200",
searchFocused ? "border-accent/50 bg-accent/[0.02]" : "border-white/[0.08] bg-white/[0.02]"
)}>
<div className="flex items-center">
<Search className={clsx("w-5 h-5 ml-4 transition-colors", searchFocused ? "text-accent" : "text-white/30")} />
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setSearchFocused(true)}
onBlur={() => setSearchFocused(false)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch(searchQuery)}
placeholder="example.com"
className="flex-1 bg-transparent px-3 py-4 text-base lg:text-lg text-white placeholder:text-white/20 outline-none font-mono"
/>
{searchQuery && (
<button
onClick={() => { setSearchQuery(''); setSearchResult(null) }}
className="p-4 text-white/30 hover:text-white transition-colors"
>
<X className="w-5 h-5" />
</button>
)}
</div>
</div>
{!searchResult && (
<p className="text-[10px] text-white/30 mt-3 font-mono">
Enter a domain to check availability, WHOIS data, and registration options
</p>
)}
</div>
</div>
</div>
{/* Search Result */}
{searchResult && (
<div className="animate-in fade-in slide-in-from-bottom-2 duration-200">
{searchResult.loading ? (
<div className="flex items-center justify-center gap-3 py-12 bg-white/[0.02] border border-white/[0.06]">
<Loader2 className="w-6 h-6 animate-spin text-accent" />
<span className="text-sm text-white/50 font-mono">Checking availability...</span>
</div>
) : (
<div className={clsx(
"border-2 overflow-hidden",
searchResult.is_available ? "border-accent/40 bg-accent/[0.03]" : "border-white/[0.08] bg-white/[0.02]"
)}>
{/* Result Header */}
<div className={clsx(
"px-4 py-4 flex items-center justify-between",
searchResult.is_available ? "bg-accent/[0.05]" : "bg-white/[0.02]"
)}>
<div className="flex items-center gap-3">
{searchResult.is_available ? (
<div className="w-12 h-12 bg-accent/20 border border-accent/30 flex items-center justify-center">
<CheckCircle2 className="w-6 h-6 text-accent" />
</div>
) : (
<div className="w-12 h-12 bg-white/[0.02] border border-white/[0.08] flex items-center justify-center">
<XCircle className="w-6 h-6 text-white/30" />
</div>
)}
<div>
<div className="text-lg font-bold text-white font-mono">{searchResult.domain}</div>
<div className="text-[10px] text-white/40 font-mono uppercase tracking-wider">
{searchResult.is_available ? 'Available for registration' : 'Already registered'}
</div>
</div>
</div>
<span className={clsx(
"text-xs font-bold px-3 py-1.5 uppercase tracking-wider",
searchResult.is_available ? "bg-accent text-black" : "bg-white/10 text-white/50"
)}>
{searchResult.is_available ? 'Available' : 'Taken'}
</span>
</div>
{/* WHOIS Info (if taken) */}
{!searchResult.is_available && (searchResult.registrar || searchResult.expiration_date) && (
<div className="px-4 py-3 border-t border-white/[0.06] grid grid-cols-2 gap-4">
{searchResult.registrar && (
<div className="flex items-center gap-2">
<Building className="w-4 h-4 text-white/30" />
<div>
<div className="text-[9px] font-mono text-white/30 uppercase">Registrar</div>
<div className="text-xs text-white/60 font-mono">{searchResult.registrar}</div>
</div>
</div>
)}
{searchResult.expiration_date && (
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-white/30" />
<div>
<div className="text-[9px] font-mono text-white/30 uppercase">Expires</div>
<div className="text-xs text-white/60 font-mono">
{new Date(searchResult.expiration_date).toLocaleDateString()}
</div>
</div>
</div>
)}
</div>
)}
{/* Actions */}
<div className="p-4 flex gap-3 border-t border-white/[0.06]">
<button
onClick={() => openAnalyze(searchResult.domain)}
className="flex-1 py-3 text-sm font-bold flex items-center justify-center gap-2 transition-all active:scale-[0.98] border border-white/20 text-white hover:bg-white/5"
>
<Shield className="w-4 h-4" />
Analyze
</button>
<button
onClick={handleAddToWatchlist}
disabled={addingToWatchlist}
className={clsx(
"flex-1 py-3 text-sm font-bold flex items-center justify-center gap-2 transition-all active:scale-[0.98]",
searchResult.is_available
? "border border-white/20 text-white hover:bg-white/5"
: "border-2 border-accent text-accent hover:bg-accent/10"
)}
>
{addingToWatchlist ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
{searchResult.is_available ? 'Track' : 'Monitor'}
</button>
{searchResult.is_available && (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${searchResult.domain}`}
target="_blank"
rel="noopener noreferrer"
className="flex-1 py-3 bg-accent text-black text-sm font-bold flex items-center justify-center gap-2 hover:bg-white active:scale-[0.98] transition-all"
>
Register
<ArrowRight className="w-4 h-4" />
</a>
)}
</div>
</div>
)}
</div>
)}
{/* Recent Searches */}
{!searchResult && recentSearches.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<div className="w-1 h-4 bg-white/20" />
<span className="text-[10px] font-mono text-white/30 uppercase tracking-[0.2em]">Recent Searches</span>
</div>
<div className="flex flex-wrap gap-2">
{recentSearches.map((domain) => (
<button
key={domain}
onClick={() => {
setSearchQuery(domain)
handleSearch(domain)
}}
className="px-3 py-1.5 border border-white/[0.08] text-xs font-mono text-white/50 hover:text-white hover:border-white/20 transition-colors"
>
{domain}
</button>
))}
</div>
</div>
)}
{/* Quick Tips */}
{!searchResult && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
<div className="border border-white/[0.08] bg-white/[0.02] p-4">
<Globe className="w-5 h-5 text-accent mb-2" />
<div className="text-sm font-bold text-white mb-1">Instant Check</div>
<p className="text-xs text-white/30">
Check any domain's availability in real-time using RDAP/WHOIS.
</p>
</div>
<div className="border border-white/[0.08] bg-white/[0.02] p-4">
<Eye className="w-5 h-5 text-white/40 mb-2" />
<div className="text-sm font-bold text-white mb-1">Track Changes</div>
<p className="text-xs text-white/30">
Add domains to your watchlist to monitor when they become available.
</p>
</div>
<div className="border border-white/[0.08] bg-white/[0.02] p-4">
<Shield className="w-5 h-5 text-white/40 mb-2" />
<div className="text-sm font-bold text-white mb-1">Deep Analysis</div>
<p className="text-xs text-white/30">
Run full analysis to check backlinks, SEO metrics, and domain history.
</p>
</div>
</div>
)}
</div>
)
}

View File

@ -239,7 +239,7 @@ export function useUserShortcuts() {
useEffect(() => {
const userShortcuts: Shortcut[] = [
// Navigation
{ key: 'g', label: 'Go to Dashboard', description: 'Navigate to dashboard', action: () => router.push('/terminal/radar'), category: 'navigation' },
{ key: 'g', label: 'Go to Hunt', description: 'Navigate to Hunt page', action: () => router.push('/terminal/hunt'), category: 'navigation' },
{ key: 'w', label: 'Go to Watchlist', description: 'Navigate to watchlist', action: () => router.push('/terminal/watchlist'), category: 'navigation' },
{ key: 'p', label: 'Go to Watchlist', description: 'Navigate to watchlist', action: () => router.push('/terminal/watchlist'), category: 'navigation' },
{ key: 'a', label: 'Go to Market', description: 'Navigate to market', action: () => router.push('/terminal/market'), category: 'navigation' },
@ -281,7 +281,7 @@ export function useAdminShortcuts() {
{ 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('/terminal/radar'), category: 'global' },
{ key: 'd', label: 'Back to Hunt', description: 'Return to Hunt page', action: () => router.push('/terminal/hunt'), category: 'global' },
]
adminShortcuts.forEach(registerShortcut)