fix: Conservative yield calculator, real TLD data on discover, fix acquire/pricing
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-13 18:04:09 +01:00
parent 356db5afee
commit 31b02e6790
12 changed files with 862 additions and 395 deletions

View File

@ -0,0 +1,34 @@
"""Add DNS verification fields to portfolio_domains
Revision ID: 006
Revises: 005
Create Date: 2025-12-13
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '006'
down_revision = '005'
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Add DNS verification columns to portfolio_domains table."""
# Add columns with default values (nullable to avoid issues with existing rows)
op.add_column('portfolio_domains', sa.Column('is_dns_verified', sa.Boolean(), nullable=True, server_default='0'))
op.add_column('portfolio_domains', sa.Column('verification_status', sa.String(50), nullable=True, server_default='unverified'))
op.add_column('portfolio_domains', sa.Column('verification_code', sa.String(100), nullable=True))
op.add_column('portfolio_domains', sa.Column('verification_started_at', sa.DateTime(), nullable=True))
op.add_column('portfolio_domains', sa.Column('verified_at', sa.DateTime(), nullable=True))
def downgrade() -> None:
"""Remove DNS verification columns from portfolio_domains table."""
op.drop_column('portfolio_domains', 'verified_at')
op.drop_column('portfolio_domains', 'verification_started_at')
op.drop_column('portfolio_domains', 'verification_code')
op.drop_column('portfolio_domains', 'verification_status')
op.drop_column('portfolio_domains', 'is_dns_verified')

View File

@ -230,10 +230,10 @@ async def get_portfolio(
notes=d.notes,
tags=d.tags,
roi=d.roi,
is_dns_verified=d.is_dns_verified,
verification_status=d.verification_status,
verification_code=d.verification_code,
verified_at=d.verified_at,
is_dns_verified=getattr(d, 'is_dns_verified', False) or False,
verification_status=getattr(d, 'verification_status', 'unverified') or 'unverified',
verification_code=getattr(d, 'verification_code', None),
verified_at=getattr(d, 'verified_at', None),
created_at=d.created_at,
updated_at=d.updated_at,
)
@ -381,10 +381,10 @@ async def add_portfolio_domain(
notes=domain.notes,
tags=domain.tags,
roi=domain.roi,
is_dns_verified=domain.is_dns_verified,
verification_status=domain.verification_status,
verification_code=domain.verification_code,
verified_at=domain.verified_at,
is_dns_verified=getattr(domain, 'is_dns_verified', False) or False,
verification_status=getattr(domain, 'verification_status', 'unverified') or 'unverified',
verification_code=getattr(domain, 'verification_code', None),
verified_at=getattr(domain, 'verified_at', None),
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@ -432,10 +432,10 @@ async def get_portfolio_domain(
notes=domain.notes,
tags=domain.tags,
roi=domain.roi,
is_dns_verified=domain.is_dns_verified,
verification_status=domain.verification_status,
verification_code=domain.verification_code,
verified_at=domain.verified_at,
is_dns_verified=getattr(domain, 'is_dns_verified', False) or False,
verification_status=getattr(domain, 'verification_status', 'unverified') or 'unverified',
verification_code=getattr(domain, 'verification_code', None),
verified_at=getattr(domain, 'verified_at', None),
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@ -492,10 +492,10 @@ async def update_portfolio_domain(
notes=domain.notes,
tags=domain.tags,
roi=domain.roi,
is_dns_verified=domain.is_dns_verified,
verification_status=domain.verification_status,
verification_code=domain.verification_code,
verified_at=domain.verified_at,
is_dns_verified=getattr(domain, 'is_dns_verified', False) or False,
verification_status=getattr(domain, 'verification_status', 'unverified') or 'unverified',
verification_code=getattr(domain, 'verification_code', None),
verified_at=getattr(domain, 'verified_at', None),
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@ -552,10 +552,10 @@ async def mark_domain_sold(
notes=domain.notes,
tags=domain.tags,
roi=domain.roi,
is_dns_verified=domain.is_dns_verified,
verification_status=domain.verification_status,
verification_code=domain.verification_code,
verified_at=domain.verified_at,
is_dns_verified=getattr(domain, 'is_dns_verified', False) or False,
verification_status=getattr(domain, 'verification_status', 'unverified') or 'unverified',
verification_code=getattr(domain, 'verification_code', None),
verified_at=getattr(domain, 'verified_at', None),
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@ -639,10 +639,10 @@ async def refresh_domain_value(
notes=domain.notes,
tags=domain.tags,
roi=domain.roi,
is_dns_verified=domain.is_dns_verified,
verification_status=domain.verification_status,
verification_code=domain.verification_code,
verified_at=domain.verified_at,
is_dns_verified=getattr(domain, 'is_dns_verified', False) or False,
verification_status=getattr(domain, 'verification_status', 'unverified') or 'unverified',
verification_code=getattr(domain, 'verification_code', None),
verified_at=getattr(domain, 'verified_at', None),
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@ -696,10 +696,11 @@ def _domain_to_response(domain: PortfolioDomain) -> PortfolioDomainResponse:
notes=domain.notes,
tags=domain.tags,
roi=domain.roi,
is_dns_verified=domain.is_dns_verified,
verification_status=domain.verification_status,
verification_code=domain.verification_code,
verified_at=domain.verified_at,
# Use getattr with defaults for new fields that may not exist in DB yet
is_dns_verified=getattr(domain, 'is_dns_verified', False) or False,
verification_status=getattr(domain, 'verification_status', 'unverified') or 'unverified',
verification_code=getattr(domain, 'verification_code', None),
verified_at=getattr(domain, 'verified_at', None),
created_at=domain.created_at,
updated_at=domain.updated_at,
)

View File

@ -180,6 +180,26 @@ async def apply_migrations(conn: AsyncConnection) -> None:
logger.info("DB migrations: adding column users.referral_code")
await conn.execute(text("ALTER TABLE users ADD COLUMN referral_code VARCHAR(100)"))
# ----------------------------------------------------
# 7) Portfolio DNS verification columns
# ----------------------------------------------------
if await _table_exists(conn, "portfolio_domains"):
if not await _has_column(conn, "portfolio_domains", "is_dns_verified"):
logger.info("DB migrations: adding column portfolio_domains.is_dns_verified")
await conn.execute(text("ALTER TABLE portfolio_domains ADD COLUMN is_dns_verified BOOLEAN DEFAULT 0"))
if not await _has_column(conn, "portfolio_domains", "verification_status"):
logger.info("DB migrations: adding column portfolio_domains.verification_status")
await conn.execute(text("ALTER TABLE portfolio_domains ADD COLUMN verification_status VARCHAR(50) DEFAULT 'unverified'"))
if not await _has_column(conn, "portfolio_domains", "verification_code"):
logger.info("DB migrations: adding column portfolio_domains.verification_code")
await conn.execute(text("ALTER TABLE portfolio_domains ADD COLUMN verification_code VARCHAR(100)"))
if not await _has_column(conn, "portfolio_domains", "verification_started_at"):
logger.info("DB migrations: adding column portfolio_domains.verification_started_at")
await conn.execute(text("ALTER TABLE portfolio_domains ADD COLUMN verification_started_at DATETIME"))
if not await _has_column(conn, "portfolio_domains", "verified_at"):
logger.info("DB migrations: adding column portfolio_domains.verified_at")
await conn.execute(text("ALTER TABLE portfolio_domains ADD COLUMN verified_at DATETIME"))
logger.info("DB migrations: done")

View File

@ -46,8 +46,9 @@ class PortfolioDomain(Base):
status: Mapped[str] = mapped_column(String(50), default="active") # active, expired, sold, parked
# DNS Verification (required for Yield and For Sale)
is_dns_verified: Mapped[bool] = mapped_column(Boolean, default=False)
verification_status: Mapped[str] = mapped_column(String(50), default="unverified") # unverified, pending, verified, failed
# All fields nullable=True to avoid migration issues on existing databases
is_dns_verified: Mapped[Optional[bool]] = mapped_column(Boolean, default=False, nullable=True)
verification_status: Mapped[Optional[str]] = mapped_column(String(50), default="unverified", nullable=True)
verification_code: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
verification_started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
verified_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)

View File

@ -411,15 +411,15 @@ export default function AcquirePage() {
</div>
<div className="grid grid-cols-3 gap-8 text-right">
<div>
<div className="text-3xl font-display text-white mb-1">{allAuctions.length}</div>
<div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">Live</div>
<div className="text-3xl font-display text-white mb-1">{allAuctions.length.toLocaleString()}</div>
<div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">Live Auctions</div>
</div>
<div>
<div className="text-3xl font-display text-accent mb-1">{endingSoon.length}</div>
<div className="text-[10px] uppercase tracking-widest text-accent/60 font-mono">Ending</div>
<div className="text-3xl font-display text-accent mb-1">{endingSoon.length.toLocaleString()}</div>
<div className="text-[10px] uppercase tracking-widest text-accent/60 font-mono">Ending 24h</div>
</div>
<div>
<div className="text-3xl font-display text-white mb-1">{pounceItems.length}</div>
<div className="text-3xl font-display text-white mb-1">{pounceItems.length.toLocaleString()}</div>
<div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">Direct</div>
</div>
</div>

View File

@ -6,80 +6,34 @@ import { Footer } from '@/components/Footer'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import {
TrendingUp,
TrendingDown,
Search,
ShieldAlert,
Lock,
ArrowRight,
Zap,
Flame,
AlertTriangle,
BarChart3,
ArrowUpRight,
Minus
Globe,
DollarSign,
Loader2
} from 'lucide-react'
import Link from 'next/link'
import clsx from 'clsx'
interface TldData {
tld: string
price: number
renewal_price: number
change_24h: number
change_7d: number
volume: number
risk_level: 'Low' | 'Medium' | 'High'
trend: 'up' | 'down' | 'stable'
registration_count: number
cheapest_registrar: string
registration_price: number | null
renewal_price: number | null
type: string | null
registrar_count: number
}
function StatCard({ label, value, subvalue, trend, icon: Icon, color }: {
label: string,
value: string,
subvalue: string,
trend?: 'up' | 'down',
icon: any,
color: 'accent' | 'red' | 'blue'
}) {
return (
<div className="bg-[#050505] border border-white/10 p-6 relative overflow-hidden group hover:border-white/20 transition-colors">
<div className={clsx(
"absolute top-0 right-0 p-4 opacity-10 transition-opacity group-hover:opacity-20",
color === 'accent' ? "text-accent" : color === 'red' ? "text-red-500" : "text-blue-500"
)}>
<Icon className="w-16 h-16" />
</div>
<div className="relative z-10">
<div className="flex items-center gap-2 mb-4">
<div className={clsx(
"w-8 h-8 flex items-center justify-center border",
color === 'accent' ? "border-accent/20 bg-accent/5 text-accent" :
color === 'red' ? "border-red-500/20 bg-red-500/5 text-red-500" :
"border-blue-500/20 bg-blue-500/5 text-blue-500"
)}>
<Icon className="w-4 h-4" />
</div>
<span className="text-xs font-mono uppercase tracking-widest text-white/40">{label}</span>
</div>
<div className="text-3xl font-display text-white mb-1">{value}</div>
<div className="flex items-center gap-2">
{trend && (
<span className={clsx(
"text-xs font-bold uppercase tracking-wide",
trend === 'up' ? "text-accent" : "text-red-500"
)}>
{trend === 'up' ? '↗' : '↘'} {trend === 'up' ? 'Bullish' : 'Bearish'}
</span>
)}
<span className="text-xs font-mono text-white/30">{subvalue}</span>
</div>
</div>
</div>
)
function getRiskLevel(regPrice: number | null, renewPrice: number | null): 'Low' | 'Medium' | 'High' {
if (!regPrice || !renewPrice) return 'Medium'
const ratio = renewPrice / regPrice
if (ratio > 5) return 'High'
if (ratio > 2) return 'Medium'
return 'Low'
}
export default function DiscoverPage() {
@ -87,305 +41,487 @@ export default function DiscoverPage() {
const [tlds, setTlds] = useState<TldData[]>([])
const [loading, setLoading] = useState(true)
const [searchQuery, setSearchQuery] = useState('')
const [sortBy, setSortBy] = useState<'volume' | 'change' | 'price' | 'risk'>('volume')
const [sortBy, setSortBy] = useState<'popular' | 'price' | 'renewal' | 'risk'>('popular')
useEffect(() => {
checkAuth()
// Expanded Mock Data
const mockData: TldData[] = [
{ tld: 'ai', price: 65, renewal_price: 85, change_24h: 2.4, change_7d: 15.2, volume: 12500, risk_level: 'High', trend: 'up', registration_count: 1500, cheapest_registrar: 'Namecheap' },
{ tld: 'io', price: 35, renewal_price: 45, change_24h: -0.5, change_7d: 1.2, volume: 8400, risk_level: 'Medium', trend: 'stable', registration_count: 800, cheapest_registrar: 'Dynadot' },
{ tld: 'com', price: 12, renewal_price: 14, change_24h: 0.1, change_7d: 0.3, volume: 450000, risk_level: 'Low', trend: 'stable', registration_count: 25000, cheapest_registrar: 'Cloudflare' },
{ tld: 'net', price: 10, renewal_price: 12, change_24h: 0.0, change_7d: -0.1, volume: 120000, risk_level: 'Low', trend: 'stable', registration_count: 5000, cheapest_registrar: 'Porkbun' },
{ tld: 'org', price: 11, renewal_price: 13, change_24h: 0.2, change_7d: 0.5, volume: 95000, risk_level: 'Low', trend: 'stable', registration_count: 4200, cheapest_registrar: 'NameSilo' },
{ tld: 'xyz', price: 2, renewal_price: 15, change_24h: 5.1, change_7d: 22.4, volume: 65000, risk_level: 'High', trend: 'up', registration_count: 12000, cheapest_registrar: 'GenXYZ' },
{ tld: 'app', price: 18, renewal_price: 22, change_24h: 1.2, change_7d: 4.5, volume: 15000, risk_level: 'Medium', trend: 'up', registration_count: 900, cheapest_registrar: 'Google' },
{ tld: 'dev', price: 16, renewal_price: 20, change_24h: 0.8, change_7d: 3.2, volume: 12000, risk_level: 'Medium', trend: 'up', registration_count: 750, cheapest_registrar: 'Google' },
{ tld: 'me', price: 25, renewal_price: 28, change_24h: -1.2, change_7d: -2.5, volume: 8000, risk_level: 'Medium', trend: 'down', registration_count: 300, cheapest_registrar: 'GoDaddy' },
{ tld: 'co', price: 28, renewal_price: 32, change_24h: 0.5, change_7d: 1.8, volume: 25000, risk_level: 'Low', trend: 'stable', registration_count: 1100, cheapest_registrar: 'Namecheap' },
{ tld: 'so', price: 45, renewal_price: 55, change_24h: 3.2, change_7d: 8.5, volume: 4500, risk_level: 'High', trend: 'up', registration_count: 200, cheapest_registrar: 'Hexonet' },
{ tld: 'tv', price: 32, renewal_price: 38, change_24h: 0.1, change_7d: 0.9, volume: 6000, risk_level: 'Medium', trend: 'stable', registration_count: 150, cheapest_registrar: 'Dynadot' },
]
setTlds(mockData)
setLoading(false)
loadTlds()
}, [checkAuth])
const loadTlds = async () => {
setLoading(true)
try {
// Load real TLD data from the API
const response = await api.getTldOverview({ limit: 100, offset: 0 })
// Transform the API response to our format
const tldList: TldData[] = response.tlds.map((tld: any) => ({
tld: tld.tld.replace('.', ''),
registration_price: tld.min_registration_price || tld.avg_registration_price,
renewal_price: tld.min_renewal_price || tld.avg_renewal_price,
type: tld.type,
registrar_count: tld.registrar_count || 1
}))
setTlds(tldList)
} catch (error) {
console.error('Failed to load TLDs:', error)
// Fallback to basic data if API fails
setTlds([
{ tld: 'com', registration_price: 12, renewal_price: 14, type: 'gTLD', registrar_count: 25 },
{ tld: 'net', registration_price: 10, renewal_price: 12, type: 'gTLD', registrar_count: 20 },
{ tld: 'org', registration_price: 11, renewal_price: 13, type: 'gTLD', registrar_count: 18 },
{ tld: 'io', registration_price: 35, renewal_price: 45, type: 'ccTLD', registrar_count: 15 },
{ tld: 'ai', registration_price: 65, renewal_price: 85, type: 'ccTLD', registrar_count: 10 },
{ tld: 'co', registration_price: 28, renewal_price: 32, type: 'ccTLD', registrar_count: 12 },
{ tld: 'app', registration_price: 18, renewal_price: 22, type: 'gTLD', registrar_count: 8 },
{ tld: 'dev', registration_price: 16, renewal_price: 20, type: 'gTLD', registrar_count: 8 },
])
} finally {
setLoading(false)
}
}
const filteredTlds = tlds
.filter(t => t.tld.includes(searchQuery.toLowerCase()))
.filter(t => t.tld.toLowerCase().includes(searchQuery.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'volume') return b.volume - a.volume
if (sortBy === 'change') return b.change_7d - a.change_7d
if (sortBy === 'price') return b.price - a.price
if (sortBy === 'risk') return (a.risk_level === 'High' ? 1 : 0) - (b.risk_level === 'High' ? 1 : 0)
if (sortBy === 'popular') return b.registrar_count - a.registrar_count
if (sortBy === 'price') return (a.registration_price || 999) - (b.registration_price || 999)
if (sortBy === 'renewal') return (a.renewal_price || 999) - (b.renewal_price || 999)
if (sortBy === 'risk') {
const riskA = getRiskLevel(a.registration_price, a.renewal_price)
const riskB = getRiskLevel(b.registration_price, b.renewal_price)
const order = { 'High': 0, 'Medium': 1, 'Low': 2 }
return order[riskA] - order[riskB]
}
return 0
})
// Top Movers Logic
const topGainer = [...tlds].sort((a, b) => b.change_7d - a.change_7d)[0]
const topVolume = [...tlds].sort((a, b) => b.volume - a.volume)[0]
const highRisk = [...tlds].find(t => t.risk_level === 'High')
// Stats
const cheapestTld = [...tlds].filter(t => t.registration_price).sort((a, b) => (a.registration_price || 999) - (b.registration_price || 999))[0]
const mostExpensive = [...tlds].filter(t => t.registration_price).sort((a, b) => (b.registration_price || 0) - (a.registration_price || 0))[0]
const highRiskTld = [...tlds].find(t => getRiskLevel(t.registration_price, t.renewal_price) === 'High')
// Gatekeeper Logic
const isPublicTld = (tld: string) => ['com', 'net', 'org'].includes(tld)
if (authLoading) return null
if (authLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#020202]">
<Loader2 className="w-8 h-8 text-accent animate-spin" />
</div>
)
}
return (
<div className="min-h-screen bg-[#020202] text-white relative overflow-x-hidden selection:bg-accent/30 selection:text-white">
{/* Background Atmosphere - Matched with Acquire */}
<div className="min-h-screen bg-[#020202] text-white relative overflow-x-hidden">
{/* Background */}
<div className="fixed inset-0 pointer-events-none z-0">
<div className="absolute inset-0 bg-[url('/noise.png')] opacity-[0.03] mix-blend-overlay" />
<div
className="absolute inset-0 opacity-[0.03]"
className="absolute inset-0 opacity-[0.02]"
style={{
backgroundImage: `linear-gradient(rgba(255,255,255,0.3) 0.5px, transparent 0.5px), linear-gradient(90deg, rgba(255,255,255,0.3) 0.5px, transparent 0.5px)`,
backgroundSize: '160px 160px',
backgroundSize: '80px 80px',
}}
/>
<div className="absolute top-[20%] right-[-10%] w-[800px] h-[800px] bg-accent/[0.02] rounded-full blur-[150px]" />
</div>
<Header />
<main className="relative pt-20 sm:pt-32 pb-16 sm:pb-24 px-4 sm:px-6 flex-1">
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* MOBILE HEADER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<header className="lg:hidden sticky top-14 z-40 bg-[#020202]/95 backdrop-blur-md border-b border-white/[0.08]">
<div className="px-4 py-3">
<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">TLD Intel</span>
</div>
<div className="flex items-center gap-2 text-[10px] font-mono text-white/40">
<span>{tlds.length} TLDs</span>
</div>
</div>
<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">{tlds.length}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Total</div>
</div>
<div className="bg-accent/[0.05] border border-accent/20 p-2">
<div className="text-lg font-bold text-accent tabular-nums">
${cheapestTld?.registration_price || '—'}
</div>
<div className="text-[9px] font-mono text-accent/60 uppercase tracking-wider">Cheapest</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">
${mostExpensive?.registration_price || '—'}
</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Premium</div>
</div>
</div>
</div>
</header>
{/* MOBILE SEARCH & FILTERS */}
<section className="lg:hidden px-4 py-3 border-b border-white/[0.08]">
<div className="relative border-2 border-white/[0.08] bg-white/[0.02] mb-3">
<div className="flex items-center">
<Search className="w-4 h-4 text-white/30 ml-3" />
<input
type="text"
placeholder="search tlds..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="flex-1 bg-transparent px-3 py-2.5 text-sm font-mono text-white placeholder:text-white/20 outline-none"
/>
</div>
</div>
<div className="flex gap-2 overflow-x-auto pb-1">
{[
{ id: 'popular' as const, label: 'Popular' },
{ id: 'price' as const, label: 'Price' },
{ id: 'renewal' as const, label: 'Renewal' },
{ id: 'risk' as const, label: 'Risk' },
].map((filter) => (
<button
key={filter.id}
onClick={() => setSortBy(filter.id)}
className={clsx(
"px-3 py-2 text-[10px] font-mono uppercase tracking-wider border transition-all whitespace-nowrap",
sortBy === filter.id
? "bg-accent text-black border-accent"
: "bg-white/[0.02] border-white/[0.08] text-white/50"
)}
>
{filter.label}
</button>
))}
</div>
</section>
{/* MOBILE TLD LIST */}
<section className="lg:hidden px-4 py-4 pb-20">
{!isAuthenticated && (
<div className="mb-4 p-3 bg-accent/5 border border-accent/20">
<div className="flex items-center gap-3">
<Lock className="w-4 h-4 text-accent shrink-0" />
<div className="flex-1">
<p className="text-xs font-bold text-white">Unlock Full Data</p>
<p className="text-[10px] text-white/50">Renewal prices & risk levels</p>
</div>
<Link href="/register" className="px-3 py-1.5 bg-accent text-black text-[10px] font-bold uppercase">
Join
</Link>
</div>
</div>
)}
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : filteredTlds.length === 0 ? (
<div className="text-center py-20 text-white/30 font-mono text-sm">No TLDs found</div>
) : (
<div className="space-y-2">
{filteredTlds.slice(0, 50).map((tld) => {
const isLocked = !isAuthenticated && !isPublicTld(tld.tld)
const riskLevel = getRiskLevel(tld.registration_price, tld.renewal_price)
return (
<Link
key={tld.tld}
href={`/terminal/intel/${tld.tld}`}
className="block p-3 bg-[#0A0A0A] border border-white/[0.08] active:bg-white/[0.03] transition-all"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center">
<Globe className="w-4 h-4 text-white/40" />
</div>
<div>
<div className="text-sm font-bold text-white font-mono">.{tld.tld}</div>
<div className="text-[10px] text-white/30 font-mono uppercase">{tld.type || 'TLD'}</div>
</div>
</div>
<div className="text-right">
<div className="text-sm font-bold text-accent font-mono">
${tld.registration_price || '—'}
</div>
<div className="flex items-center gap-1 justify-end">
{isLocked ? (
<span className="text-[10px] text-white/20 flex items-center gap-1">
<Lock className="w-3 h-3" /> Renew
</span>
) : (
<span className={clsx(
"text-[10px] font-mono",
riskLevel === 'High' ? 'text-red-400' :
riskLevel === 'Medium' ? 'text-amber-400' : 'text-white/40'
)}>
${tld.renewal_price || '—'}/yr
</span>
)}
</div>
</div>
</div>
</Link>
)
})}
</div>
)}
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* DESKTOP LAYOUT */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<main className="hidden lg:block relative pt-32 pb-24 px-6">
<div className="max-w-[1400px] mx-auto">
{/* Header Section */}
<div className="mb-12 sm:mb-16 animate-fade-in text-left">
<div className="flex flex-col lg:flex-row justify-between items-end gap-6 sm:gap-8 border-b border-white/[0.08] pb-8 sm:pb-12">
<div className="mb-12 animate-fade-in">
<div className="flex justify-between items-end gap-8 border-b border-white/[0.08] pb-10">
<div>
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block flex items-center gap-2">
<span className="text-accent font-mono text-xs uppercase tracking-[0.2em] mb-4 block flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
Global Intelligence
TLD Intelligence
</span>
<h1 className="font-display text-[2.5rem] sm:text-[4rem] md:text-[5rem] lg:text-[6rem] leading-[0.9] tracking-[-0.03em] text-white">
Market Pulse.
<h1 className="font-display text-[5rem] leading-[0.9] tracking-[-0.03em] text-white">
Discover TLDs.
</h1>
<p className="mt-5 sm:mt-8 text-sm sm:text-lg lg:text-xl text-white/50 max-w-2xl font-light leading-relaxed">
Real-time inflation monitor. Track pricing and breakout trends.
<span className="block mt-2 text-white/80">886+ TLDs monitored.</span>
<p className="mt-6 text-lg text-white/50 max-w-2xl font-light leading-relaxed">
Compare registration and renewal prices across {tlds.length}+ top-level domains.
<span className="block mt-2 text-white/80">Find the best value for your next domain.</span>
</p>
</div>
<div className="grid grid-cols-2 gap-12 text-right hidden lg:grid">
<div className="grid grid-cols-3 gap-8 text-right">
<div>
<div className="text-3xl font-display text-white mb-1">886+</div>
<div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">TLDs Monitored</div>
<div className="text-3xl font-display text-white mb-1">{tlds.length}</div>
<div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">TLDs</div>
</div>
<div>
<div className="text-3xl font-display text-white mb-1">24/7</div>
<div className="text-[10px] uppercase tracking-widest text-accent font-mono">Live Sync</div>
<div className="text-3xl font-display text-accent mb-1">${cheapestTld?.registration_price || '—'}</div>
<div className="text-[10px] uppercase tracking-widest text-accent/60 font-mono">Cheapest</div>
</div>
<div>
<div className="text-3xl font-display text-white mb-1">${mostExpensive?.registration_price || '—'}</div>
<div className="text-[10px] uppercase tracking-widest text-white/30 font-mono">Premium</div>
</div>
</div>
</div>
</div>
{/* Top Movers Cards */}
{topGainer && topVolume && highRisk && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-6 mb-10 sm:mb-16 animate-fade-in">
<StatCard
label="Top Gainer (7d)"
value={`.${topGainer.tld.toUpperCase()}`}
subvalue={`+${topGainer.change_7d}% Growth`}
trend="up"
icon={TrendingUp}
color="accent"
/>
<StatCard
label="Highest Volume"
value={`.${topVolume.tld.toUpperCase()}`}
subvalue={`${(topVolume.volume / 1000).toFixed(0)}k Daily Ops`}
trend="up"
icon={BarChart3}
color="blue"
/>
<StatCard
label="Inflation Risk"
value={`.${highRisk.tld.toUpperCase()}`}
subvalue="Renewal Price Spike"
trend="down"
icon={AlertTriangle}
color="red"
/>
{/* Stats Cards */}
{cheapestTld && mostExpensive && highRiskTld && (
<div className="grid grid-cols-3 gap-4 mb-10">
<div className="bg-[#050505] border border-white/10 p-6">
<div className="flex items-center gap-2 mb-3">
<div className="w-8 h-8 bg-accent/10 border border-accent/20 flex items-center justify-center">
<DollarSign className="w-4 h-4 text-accent" />
</div>
<span className="text-xs font-mono text-white/40 uppercase">Best Value</span>
</div>
<div className="text-2xl font-display text-white">.{cheapestTld.tld.toUpperCase()}</div>
<div className="text-sm text-accent font-mono">${cheapestTld.registration_price}/yr</div>
</div>
<div className="bg-[#050505] border border-white/10 p-6">
<div className="flex items-center gap-2 mb-3">
<div className="w-8 h-8 bg-blue-500/10 border border-blue-500/20 flex items-center justify-center">
<BarChart3 className="w-4 h-4 text-blue-400" />
</div>
<span className="text-xs font-mono text-white/40 uppercase">Most Tracked</span>
</div>
<div className="text-2xl font-display text-white">.COM</div>
<div className="text-sm text-blue-400 font-mono">Industry Standard</div>
</div>
<div className="bg-[#050505] border border-white/10 p-6">
<div className="flex items-center gap-2 mb-3">
<div className="w-8 h-8 bg-red-500/10 border border-red-500/20 flex items-center justify-center">
<AlertTriangle className="w-4 h-4 text-red-400" />
</div>
<span className="text-xs font-mono text-white/40 uppercase">Watch Renewal</span>
</div>
<div className="text-2xl font-display text-white">.{highRiskTld.tld.toUpperCase()}</div>
<div className="text-sm text-red-400 font-mono">High renewal cost</div>
</div>
</div>
)}
{/* Tech Bar - Filter & Search - Matched Style */}
<div className="mb-10 animate-slide-up sticky top-20 z-30 backdrop-blur-xl bg-[#020202]/90 border-y border-white/[0.08] py-5 -mx-4 px-4 sm:mx-0 sm:px-0">
<div className="flex flex-col lg:flex-row gap-5 justify-between items-center max-w-[1400px] mx-auto">
<div className="relative w-full lg:w-[480px] group">
<Search className="absolute left-5 top-1/2 -translate-y-1/2 w-5 h-5 text-white/40 group-focus-within:text-accent transition-colors" />
<input
type="text"
placeholder="SEARCH_TLDS..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-14 pr-5 py-3.5 bg-[#0A0A0A] border border-white/10 text-white placeholder:text-white/20 font-mono text-base focus:outline-none focus:border-accent focus:bg-[#0F0F0F] transition-all rounded-none"
/>
</div>
<div className="flex gap-4 overflow-x-auto pb-2 lg:pb-0 w-full lg:w-auto">
{[
{ id: 'volume', label: 'Volume' },
{ id: 'change', label: 'Momentum' },
{ id: 'price', label: 'Buy Price' },
{ id: 'risk', label: 'Risk Factor' }
].map((filter) => (
<button
key={filter.id}
onClick={() => setSortBy(filter.id as any)}
className={clsx(
"px-6 py-3.5 flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-all border border-white/10 bg-[#0A0A0A] hover:bg-white/5 whitespace-nowrap",
sortBy === filter.id ? "text-accent border-accent" : "text-white/40 hover:text-white"
)}
>
{filter.label}
</button>
))}
</div>
</div>
</div>
{/* Data Grid - Matched Font Sizes and Styles */}
<div className="border border-white/[0.08] bg-[#050505] animate-slide-up shadow-2xl mb-20">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-[#0A0A0A] border-b border-white/[0.08]">
<th className="text-left px-8 py-5 text-xs uppercase tracking-widest text-white/40 font-bold">Asset Class</th>
<th className="text-right px-8 py-5 text-xs uppercase tracking-widest text-white/40 font-bold">Buy Price</th>
<th className="text-right px-8 py-5 text-xs uppercase tracking-widest text-white/40 font-bold">Trend (7d)</th>
<th className="text-right px-8 py-5 text-xs uppercase tracking-widest text-white/40 font-bold hidden md:table-cell">Renewal Cost</th>
<th className="text-right px-8 py-5 text-xs uppercase tracking-widest text-white/40 font-bold hidden lg:table-cell">Registrations (24h)</th>
<th className="text-center px-8 py-5 text-xs uppercase tracking-widest text-white/40 font-bold hidden lg:table-cell">Risk Level</th>
<th className="px-8 py-5"></th>
</tr>
</thead>
<tbody className="divide-y divide-white/[0.03]">
{filteredTlds.map((tld) => {
const isLocked = !isAuthenticated && !isPublicTld(tld.tld)
return (
<tr key={tld.tld} className="group hover:bg-[#0F0F0F] transition-all duration-200 border-l-2 border-transparent hover:border-accent">
<td className="px-8 py-6">
<Link href={`/discover/${tld.tld}`} className="flex items-center gap-4 group-hover:translate-x-2 transition-transform duration-300">
{/* Changed to font-mono and adjusted size to match Acquire's tech look while being distinct for TLDs */}
<span className="font-mono text-2xl font-medium text-white group-hover:text-accent transition-colors tracking-tight">.{tld.tld}</span>
{tld.change_7d > 10 && <Flame className="w-4 h-4 text-accent animate-pulse" />}
</Link>
</td>
<td className="px-8 py-6 text-right">
<div className="flex flex-col items-end">
{/* Matched font-mono text-lg from Acquire */}
<span className="font-mono text-lg font-medium text-white">${tld.price}</span>
<span className="text-[10px] text-white/30 uppercase tracking-widest">Entry</span>
</div>
</td>
<td className="px-8 py-6 text-right">
<div className="flex flex-col items-end">
<span className={clsx(
"font-mono text-base flex items-center gap-1",
tld.change_7d > 0 ? "text-accent" : tld.change_7d < 0 ? "text-red-400" : "text-white/40"
)}>
{tld.change_7d > 0 ? <TrendingUp className="w-3 h-3"/> : tld.change_7d < 0 ? <TrendingDown className="w-3 h-3"/> : <Minus className="w-3 h-3"/>}
{Math.abs(tld.change_7d)}%
</span>
<span className="text-[10px] text-white/30 uppercase tracking-widest">Momentum</span>
</div>
</td>
{/* Renewal Price - Locked for guests */}
<td className="px-8 py-6 text-right hidden md:table-cell">
{isLocked ? (
<div className="flex justify-end items-center gap-2 text-white/20 select-none">
<Lock className="w-3 h-3" />
<span className="blur-[4px] font-mono text-lg">$XX.XX</span>
</div>
) : (
<div className="flex flex-col items-end">
<span className={clsx(
"font-mono text-lg font-medium text-white/80",
tld.renewal_price > tld.price * 2 ? "text-red-400" : "text-white/80"
)}>${tld.renewal_price}</span>
<span className="text-[10px] text-white/30 uppercase tracking-widest">Recurring</span>
</div>
)}
</td>
{/* Volume - Locked for guests */}
<td className="px-8 py-6 text-right hidden lg:table-cell">
{isLocked ? (
<div className="flex justify-end items-center gap-2 text-white/20 select-none">
<span className="blur-[4px] font-mono text-base">XXXX</span>
</div>
) : (
<span className="font-mono text-base text-white/60">{tld.registration_count.toLocaleString()}</span>
)}
</td>
{/* Risk Level - Locked for guests */}
<td className="px-8 py-6 text-center hidden lg:table-cell">
{isLocked ? (
<div className="flex justify-center">
<span className="bg-white/5 text-white/20 text-[10px] font-bold uppercase tracking-widest px-3 py-1 blur-[2px]">LOCKED</span>
</div>
) : (
<span className={clsx(
"text-[10px] font-bold uppercase tracking-widest px-3 py-1 border",
tld.risk_level === 'Low' ? "border-accent/20 text-accent bg-accent/5" :
tld.risk_level === 'Medium' ? "border-amber-500/20 text-amber-500 bg-amber-500/5" :
"border-red-500/20 text-red-500 bg-red-500/5"
)}>
{tld.risk_level}
</span>
)}
</td>
<td className="px-8 py-6 text-right">
<Link
href={`/discover/${tld.tld}`}
className="inline-flex items-center justify-center w-10 h-10 border border-white/10 text-white/40 hover:text-black hover:bg-white hover:border-white transition-all duration-300 group-hover:border-accent group-hover:text-accent opacity-0 group-hover:opacity-100 transform translate-x-2 group-hover:translate-x-0"
>
<ArrowUpRight className="w-4 h-4" />
</Link>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
{/* CTA for Locked Data */}
{/* Login Banner */}
{!isAuthenticated && (
<div className="mt-20 p-px bg-gradient-to-r from-accent/20 via-white/5 to-accent/20 max-w-4xl mx-auto">
<div className="bg-[#080808] p-12 text-center relative overflow-hidden">
<div className="absolute inset-0 bg-[url('/noise.png')] opacity-[0.05]" />
<div className="relative z-10">
<div className="inline-flex items-center justify-center w-12 h-12 border border-accent/20 bg-accent/5 text-accent mb-6">
<ShieldAlert className="w-6 h-6" />
</div>
<h3 className="text-3xl font-display text-white mb-4">Protect Your Capital.</h3>
<p className="text-white/50 mb-8 max-w-lg mx-auto text-lg font-light leading-relaxed">
Don't buy a cheap domain with expensive renewal fees.
Our 'Trader' plan reveals hidden costs and risk levels for every TLD.
</p>
<Link
href="/pricing"
className="inline-flex items-center gap-3 px-8 py-4 bg-accent text-black text-xs font-bold uppercase tracking-widest hover:bg-white transition-all"
style={{ clipPath: 'polygon(10px 0, 100% 0, 100% 100%, 0 100%, 0 10px)' }}
>
Unlock Intelligence
<Zap className="w-4 h-4" />
</Link>
</div>
<div className="mb-8 p-1 border border-accent/20 bg-accent/5 max-w-3xl">
<div className="bg-[#050505] p-6 flex items-center justify-between gap-6">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-accent/10 border border-accent/20 flex items-center justify-center text-accent">
<Lock className="w-5 h-5" />
</div>
<div>
<p className="text-base font-bold text-white mb-0.5">Unlock Full Intelligence</p>
<p className="text-sm font-mono text-white/50">See renewal prices and risk levels for all TLDs.</p>
</div>
</div>
<Link
href="/register"
className="shrink-0 px-6 py-3 bg-accent text-black text-xs font-bold uppercase tracking-widest hover:bg-white transition-all"
>
Sign Up Free
</Link>
</div>
</div>
)}
{/* Search & Filters Bar */}
<div className="mb-6 sticky top-20 z-30 backdrop-blur-xl bg-[#020202]/90 border-y border-white/[0.08] py-4 -mx-6 px-6">
<div className="flex gap-4 justify-between items-center">
<div className="relative w-[400px] group">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/40 group-focus-within:text-accent transition-colors" />
<input
type="text"
placeholder="Search TLDs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-12 pr-4 py-3 bg-[#0A0A0A] border border-white/10 text-white placeholder:text-white/20 font-mono text-sm focus:outline-none focus:border-accent transition-all"
/>
</div>
<div className="flex gap-2">
{[
{ id: 'popular' as const, label: 'Popular' },
{ id: 'price' as const, label: 'Price' },
{ id: 'renewal' as const, label: 'Renewal' },
{ id: 'risk' as const, label: 'Risk' },
].map((filter) => (
<button
key={filter.id}
onClick={() => setSortBy(filter.id)}
className={clsx(
"px-5 py-3 text-xs font-bold uppercase tracking-widest transition-all border",
sortBy === filter.id
? "bg-white/10 text-white border-accent"
: "bg-[#0A0A0A] border-white/10 text-white/40 hover:text-white"
)}
>
{filter.label}
</button>
))}
</div>
</div>
</div>
{/* Desktop Table */}
<div className="border border-white/[0.08] bg-[#050505]">
{/* Table Header */}
<div className="grid grid-cols-[1fr_120px_120px_100px_80px] gap-4 px-6 py-4 bg-[#0A0A0A] border-b border-white/[0.08] text-[10px] font-mono text-white/40 uppercase tracking-wider">
<div>Extension</div>
<div className="text-right">Registration</div>
<div className="text-right">Renewal</div>
<div className="text-center">Risk</div>
<div></div>
</div>
{/* Table Body */}
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : filteredTlds.length === 0 ? (
<div className="text-center py-20 text-white/30 font-mono">No TLDs found</div>
) : (
<div>
{filteredTlds.map((tld) => {
const isLocked = !isAuthenticated && !isPublicTld(tld.tld)
const riskLevel = getRiskLevel(tld.registration_price, tld.renewal_price)
return (
<Link
key={tld.tld}
href={`/terminal/intel/${tld.tld}`}
className="grid grid-cols-[1fr_120px_120px_100px_80px] gap-4 items-center px-6 py-4 border-b border-white/[0.03] hover:bg-white/[0.02] transition-all group"
>
<div className="flex items-center gap-3">
<span className="font-mono text-xl text-white group-hover:text-accent transition-colors">.{tld.tld}</span>
<span className="text-[10px] text-white/30 font-mono uppercase">{tld.type}</span>
</div>
<div className="text-right font-mono text-base text-accent">
${tld.registration_price || '—'}
</div>
<div className="text-right">
{isLocked ? (
<span className="text-white/20 flex items-center gap-1 justify-end">
<Lock className="w-3 h-3" />
<span className="blur-[3px]">$XX</span>
</span>
) : (
<span className={clsx(
"font-mono text-base",
riskLevel === 'High' ? 'text-red-400' : 'text-white/60'
)}>
${tld.renewal_price || '—'}
</span>
)}
</div>
<div className="flex justify-center">
{isLocked ? (
<span className="text-[10px] text-white/20 blur-[2px]">---</span>
) : (
<span className={clsx(
"text-[10px] font-bold uppercase tracking-widest px-2 py-1 border",
riskLevel === 'Low' ? "border-accent/20 text-accent bg-accent/5" :
riskLevel === 'Medium' ? "border-amber-500/20 text-amber-500 bg-amber-500/5" :
"border-red-500/20 text-red-500 bg-red-500/5"
)}>
{riskLevel}
</span>
)}
</div>
<div className="flex justify-end">
<div className="w-8 h-8 border border-white/10 flex items-center justify-center text-white/30 group-hover:bg-white group-hover:text-black transition-all">
<ArrowUpRight className="w-4 h-4" />
</div>
</div>
</Link>
)
})}
</div>
)}
</div>
{/* Stats Footer */}
{!loading && (
<div className="mt-4 flex justify-between text-[10px] font-mono text-white/30 uppercase tracking-widest">
<span>Data updated daily</span>
<span>TLDs: {filteredTlds.length}</span>
</div>
)}
{/* Bottom CTA */}
{!isAuthenticated && (
<div className="mt-16 p-px bg-gradient-to-r from-accent/20 via-white/5 to-accent/20 max-w-3xl">
<div className="bg-[#080808] p-10 text-left">
<div className="inline-flex items-center justify-center w-10 h-10 border border-accent/20 bg-accent/5 text-accent mb-4">
<ShieldAlert className="w-5 h-5" />
</div>
<h3 className="text-xl font-display text-white mb-2">Avoid Hidden Costs.</h3>
<p className="text-white/50 mb-6 max-w-md text-sm">
Some TLDs have renewal prices 5-10x higher than registration.
Unlock full pricing data to make informed decisions.
</p>
<Link
href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-black text-xs font-bold uppercase tracking-widest hover:bg-white transition-all"
>
Unlock Intel <Zap className="w-4 h-4" />
</Link>
</div>
</div>
)}
</div>
</main>
<Footer />
</div>
)

View File

@ -198,16 +198,16 @@ export default function PricingPage() {
)}
{/* Hero */}
<div className="text-left mb-12 sm:mb-16 animate-fade-in">
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block flex items-center gap-2">
<div className="text-left lg:text-center mb-12 sm:mb-16 animate-fade-in">
<span className="text-accent font-mono text-[10px] sm:text-xs uppercase tracking-[0.2em] mb-3 sm:mb-4 block flex items-center gap-2 lg:justify-center">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
Clearance Levels
</span>
<h1 className="font-display text-[2.5rem] sm:text-[4rem] md:text-[5rem] lg:text-[6rem] leading-[0.9] tracking-[-0.03em] text-white">
Pick your weapon.
</h1>
<p className="mt-5 sm:mt-8 text-sm sm:text-lg lg:text-xl text-white/50 max-w-xl font-light leading-relaxed">
Start free. Scale when you&apos;re ready.
<p className="mt-5 sm:mt-8 text-sm sm:text-lg lg:text-xl text-white/50 max-w-xl lg:mx-auto font-light leading-relaxed">
Start free. Scale when you&apos;re ready. All plans include core features.
</p>
</div>
@ -244,9 +244,9 @@ export default function PricingPage() {
<div className="relative flex-1 flex flex-col">
{/* Header */}
<div className="mb-6 sm:mb-8 text-left sm:text-center pb-6 sm:pb-8 border-b border-white/10">
<div className="mb-6 sm:mb-8 text-left lg:text-center pb-6 sm:pb-8 border-b border-white/10">
<h3 className="text-xl sm:text-2xl font-display text-white mb-3 sm:mb-4">{tier.name}</h3>
<div className="flex items-baseline justify-start sm:justify-center gap-1 mb-3 sm:mb-4">
<div className="flex items-baseline justify-start lg:justify-center gap-1 mb-3 sm:mb-4">
{tier.price === '0' ? (
<span className="text-3xl sm:text-5xl font-mono text-white tracking-tight">Free</span>
) : (
@ -355,8 +355,8 @@ export default function PricingPage() {
</div>
{/* FAQ */}
<div className="max-w-3xl mx-auto lg:mx-0">
<h2 className="text-2xl sm:text-3xl font-display text-white text-left mb-8 sm:mb-12">Mission Support</h2>
<div className="max-w-3xl mx-auto">
<h2 className="text-2xl sm:text-3xl font-display text-white text-left lg:text-center mb-8 sm:mb-12">Mission Support</h2>
<div className="space-y-4">
{faqs.map((faq, i) => (
<div

View File

@ -532,6 +532,26 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
const [verificationData, setVerificationData] = useState<any>(null)
const [verifying, setVerifying] = useState(false)
const [verified, setVerified] = useState(false)
// Portfolio domains (for dropdown selection)
const [portfolioDomains, setPortfolioDomains] = useState<{ id: number; domain: string }[]>([])
const [loadingDomains, setLoadingDomains] = useState(true)
useEffect(() => {
const fetchPortfolioDomains = async () => {
setLoadingDomains(true)
try {
const domains = await api.getPortfolio()
// Filter out sold domains
setPortfolioDomains(domains.filter(d => !d.is_sold).map(d => ({ id: d.id, domain: d.domain })))
} catch (err) {
console.error('Failed to load portfolio domains:', err)
} finally {
setLoadingDomains(false)
}
}
fetchPortfolioDomains()
}, [])
// Step 1: Create listing
const handleCreateListing = async () => {
@ -641,15 +661,34 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
</div>
<div>
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Domain Name *</label>
<input
type="text"
value={domain}
onChange={(e) => setDomain(e.target.value)}
required
className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50"
placeholder="yourdomain.com"
/>
<label className="block text-[10px] font-mono text-white/40 uppercase mb-2">Select Domain from Portfolio *</label>
{loadingDomains ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="w-5 h-5 text-accent animate-spin" />
</div>
) : portfolioDomains.length === 0 ? (
<div className="p-4 bg-amber-400/5 border border-amber-400/20 text-center">
<AlertCircle className="w-6 h-6 text-amber-400 mx-auto mb-2" />
<p className="text-xs text-amber-400 font-mono mb-3">No domains in your portfolio</p>
<a href="/terminal/portfolio" className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-xs font-bold uppercase">
Add Domains to Portfolio
</a>
</div>
) : (
<select
value={domain}
onChange={(e) => setDomain(e.target.value)}
className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white text-sm font-mono outline-none focus:border-accent/50"
>
<option value=""> Select a domain </option>
{portfolioDomains.map(d => (
<option key={d.id} value={d.domain}>{d.domain}</option>
))}
</select>
)}
<p className="text-[9px] font-mono text-white/30 mt-2">
Only domains from your portfolio can be listed. DNS verification required after listing.
</p>
</div>
<div>

View File

@ -446,15 +446,19 @@ export default function PortfolioPage() {
<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",
domain.is_sold ? "bg-white/[0.02] border-white/[0.06]" : "bg-accent/10 border-accent/20"
domain.is_sold ? "bg-white/[0.02] border-white/[0.06]" :
domain.is_dns_verified ? "bg-accent/10 border-accent/20" : "bg-blue-400/10 border-blue-400/20"
)}>
{domain.is_sold ? <CheckCircle className="w-4 h-4 text-white/30" /> : <Briefcase className="w-4 h-4 text-accent" />}
{domain.is_sold ? <CheckCircle className="w-4 h-4 text-white/30" /> :
domain.is_dns_verified ? <ShieldCheck className="w-4 h-4 text-accent" /> : <ShieldAlert className="w-4 h-4 text-blue-400" />}
</div>
<div className="min-w-0">
<div className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors">{domain.domain}</div>
<div className="flex items-center gap-2 text-[10px] font-mono text-white/30">
<span>{domain.registrar || 'Unknown'}</span>
{domain.is_sold && <span className="px-1 py-0.5 bg-white/5 text-white/40">SOLD</span>}
{!domain.is_sold && domain.is_dns_verified && <span className="px-1 py-0.5 bg-accent/10 text-accent">VERIFIED</span>}
{!domain.is_sold && !domain.is_dns_verified && <span className="px-1 py-0.5 bg-blue-400/10 text-blue-400">UNVERIFIED</span>}
</div>
</div>
<a href={`https://${domain.domain}`} target="_blank" className="opacity-0 group-hover:opacity-50 hover:!opacity-100 transition-opacity ml-2">
@ -572,6 +576,9 @@ export default function PortfolioPage() {
{/* DOMAIN DETAIL MODAL */}
{selectedDomain && <DomainDetailModal domain={selectedDomain} onClose={() => setSelectedDomain(null)} onUpdate={loadData} canListForSale={canListForSale} />}
{/* DNS VERIFICATION MODAL */}
{verifyingDomain && <DnsVerificationModal domain={verifyingDomain} onClose={() => setVerifyingDomain(null)} onSuccess={() => { loadData(); setVerifyingDomain(null) }} />}
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
</div>
)
@ -873,3 +880,169 @@ function MobileDrawer({ user, tierName, TierIcon, sections, onClose, onLogout }:
</div>
)
}
// ============================================================================
// DNS VERIFICATION MODAL
// ============================================================================
function DnsVerificationModal({ domain, onClose, onSuccess }: { domain: PortfolioDomain; onClose: () => void; onSuccess: () => void }) {
const [step, setStep] = useState<'loading' | 'instructions' | 'checking'>('loading')
const [verificationData, setVerificationData] = useState<{
verification_code: string
dns_record_name: string
dns_record_value: string
instructions: string
} | null>(null)
const [error, setError] = useState<string | null>(null)
const [checkResult, setCheckResult] = useState<string | null>(null)
const [copied, setCopied] = useState(false)
useEffect(() => {
const startVerification = async () => {
try {
const data = await api.startPortfolioDnsVerification(domain.id)
setVerificationData({
verification_code: data.verification_code,
dns_record_name: data.dns_record_name,
dns_record_value: data.dns_record_value,
instructions: data.instructions,
})
setStep('instructions')
} catch (err: any) {
setError(err.message || 'Failed to start verification')
setStep('instructions')
}
}
startVerification()
}, [domain.id])
const handleCheck = async () => {
setStep('checking')
setCheckResult(null)
setError(null)
try {
const result = await api.checkPortfolioDnsVerification(domain.id)
if (result.verified) {
onSuccess()
} else {
setCheckResult(result.message)
setStep('instructions')
}
} catch (err: any) {
setError(err.message || 'Verification check failed')
setStep('instructions')
}
}
const handleCopy = (text: string) => {
navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div className="fixed inset-0 z-[110] bg-black/80 flex items-center justify-center p-4" onClick={onClose}>
<div className="w-full max-w-lg bg-[#0A0A0A] border border-white/[0.08]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
<div className="flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-blue-400" />
<span className="text-xs font-mono text-blue-400 uppercase tracking-wider">Verify Domain Ownership</span>
</div>
<button onClick={onClose} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40">
<X className="w-4 h-4" />
</button>
</div>
<div className="p-4 space-y-4">
{/* Domain Header */}
<div className="text-center py-3 border-b border-white/[0.08]">
<h2 className="text-xl font-bold font-mono text-white">{domain.domain}</h2>
<p className="text-xs font-mono text-white/40 mt-1">DNS Verification Required</p>
</div>
{step === 'loading' && (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
)}
{step === 'instructions' && verificationData && (
<>
{/* Instructions */}
<div className="p-4 bg-blue-400/5 border border-blue-400/20">
<h3 className="text-sm font-bold text-white mb-2">Add this TXT record to your DNS:</h3>
<div className="space-y-3">
<div>
<div className="text-[9px] font-mono text-white/40 uppercase mb-1">Host / Name</div>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-black/50 text-sm font-mono text-white break-all">_pounce</code>
<button onClick={() => handleCopy('_pounce')} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40 hover:text-white">
{copied ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />}
</button>
</div>
</div>
<div>
<div className="text-[9px] font-mono text-white/40 uppercase mb-1">Type</div>
<code className="block px-3 py-2 bg-black/50 text-sm font-mono text-white">TXT</code>
</div>
<div>
<div className="text-[9px] font-mono text-white/40 uppercase mb-1">Value</div>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-black/50 text-sm font-mono text-accent break-all">{verificationData.verification_code}</code>
<button onClick={() => handleCopy(verificationData.verification_code)} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40 hover:text-white">
{copied ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />}
</button>
</div>
</div>
</div>
</div>
{/* Info */}
<div className="p-3 bg-white/[0.02] border border-white/[0.08] text-xs text-white/50 font-mono">
<p>DNS changes can take up to 48 hours to propagate, but usually complete within minutes.</p>
</div>
{/* Error/Check Result */}
{error && (
<div className="p-3 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs font-mono">
{error}
</div>
)}
{checkResult && (
<div className="p-3 bg-amber-400/10 border border-amber-400/20 text-amber-400 text-xs font-mono">
{checkResult}
</div>
)}
{/* Actions */}
<div className="flex gap-3 pt-2">
<button onClick={onClose} className="flex-1 py-2.5 border border-white/10 text-white/60 text-xs font-mono uppercase">
Cancel
</button>
<button onClick={handleCheck} className="flex-1 py-2.5 bg-blue-400 text-black text-xs font-bold uppercase flex items-center justify-center gap-2">
<RefreshCw className="w-4 h-4" />Check Verification
</button>
</div>
</>
)}
{step === 'checking' && (
<div className="flex flex-col items-center justify-center py-8 gap-3">
<Loader2 className="w-6 h-6 text-blue-400 animate-spin" />
<p className="text-sm font-mono text-white/60">Checking DNS records...</p>
</div>
)}
{step === 'instructions' && !verificationData && error && (
<div className="p-4 bg-rose-500/10 border border-rose-500/20">
<p className="text-rose-400 text-sm">{error}</p>
<button onClick={onClose} className="mt-4 w-full py-2.5 border border-white/10 text-white/60 text-xs font-mono uppercase">
Close
</button>
</div>
)}
</div>
</div>
</div>
)
}

View File

@ -362,18 +362,14 @@ export default function WatchlistPage() {
{/* ADD DOMAIN + FILTERS */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="px-4 lg:px-10 py-4 border-b border-white/[0.08]">
{/* Add Domain Form */}
{/* Add Domain Form - Always visible with accent border */}
<form onSubmit={handleAdd} className="relative mb-4">
<div className={clsx(
"flex items-center border-2 transition-all duration-200",
searchFocused
? "border-accent/50 bg-accent/[0.02]"
: "border-white/[0.08] bg-white/[0.02]"
"border-accent/50 bg-accent/[0.03]",
searchFocused && "border-accent bg-accent/[0.05]"
)}>
<Plus className={clsx(
"w-4 h-4 ml-4 transition-colors",
searchFocused ? "text-accent" : "text-white/30"
)} />
<Plus className="w-4 h-4 ml-4 text-accent transition-colors" />
<input
type="text"
value={newDomain}

View File

@ -35,23 +35,41 @@ function StatusBadge({ status }: { status: string }) {
}
// ============================================================================
// ACTIVATE MODAL (simplified)
// ACTIVATE MODAL - Only verified portfolio domains
// ============================================================================
function ActivateModal({ isOpen, onClose, onSuccess }: { isOpen: boolean; onClose: () => void; onSuccess: () => void }) {
const [domain, setDomain] = useState('')
const [selectedDomain, setSelectedDomain] = useState('')
const [verifiedDomains, setVerifiedDomains] = useState<{ id: number; domain: string }[]>([])
const [loadingDomains, setLoadingDomains] = useState(true)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!isOpen) return
const fetchVerifiedDomains = async () => {
setLoadingDomains(true)
try {
const domains = await api.getVerifiedPortfolioDomains()
setVerifiedDomains(domains.map(d => ({ id: d.id, domain: d.domain })))
} catch (err) {
console.error('Failed to load verified domains:', err)
} finally {
setLoadingDomains(false)
}
}
fetchVerifiedDomains()
}, [isOpen])
const handleActivate = async () => {
if (!domain.trim()) return
if (!selectedDomain) return
setLoading(true)
setError(null)
try {
await api.activateYieldDomain(domain.trim(), true)
await api.activateYieldDomain(selectedDomain, true)
onSuccess()
onClose()
setDomain('')
setSelectedDomain('')
} catch (err: any) {
setError(err.message || 'Failed')
} finally {
@ -67,22 +85,52 @@ function ActivateModal({ isOpen, onClose, onSuccess }: { isOpen: boolean; onClos
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-accent" />
<span className="text-xs font-mono text-accent uppercase tracking-wider">Add Domain</span>
<span className="text-xs font-mono text-accent uppercase tracking-wider">Activate Yield</span>
</div>
<button onClick={onClose} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40"><X className="w-4 h-4" /></button>
</div>
<div className="p-4 space-y-4">
<div>
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Domain</label>
<input type="text" value={domain} onChange={(e) => setDomain(e.target.value)} placeholder="example.com"
className="w-full px-3 py-2.5 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50" />
</div>
{error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>}
<button onClick={handleActivate} disabled={loading || !domain.trim()}
className="w-full py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50">
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
Activate
</button>
{loadingDomains ? (
<div className="flex items-center justify-center py-6">
<Loader2 className="w-5 h-5 text-accent animate-spin" />
</div>
) : verifiedDomains.length === 0 ? (
<div className="text-center py-6">
<AlertCircle className="w-8 h-8 text-amber-400 mx-auto mb-3" />
<h3 className="text-sm font-bold text-white mb-2">No Verified Domains</h3>
<p className="text-xs text-white/50 mb-4">
You need to add domains to your portfolio and verify DNS ownership before activating Yield.
</p>
<a href="/terminal/portfolio" className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-xs font-bold uppercase">
Go to Portfolio
</a>
</div>
) : (
<>
<div>
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Select Domain (DNS Verified)</label>
<select
value={selectedDomain}
onChange={(e) => setSelectedDomain(e.target.value)}
className="w-full px-3 py-2.5 bg-white/5 border border-white/10 text-white text-sm font-mono outline-none focus:border-accent/50"
>
<option value=""> Select a domain </option>
{verifiedDomains.map(d => (
<option key={d.id} value={d.domain}>{d.domain}</option>
))}
</select>
</div>
<div className="p-3 bg-accent/5 border border-accent/20 text-xs text-accent/80 font-mono">
<p>Only DNS-verified domains from your portfolio can be activated for Yield.</p>
</div>
{error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>}
<button onClick={handleActivate} disabled={loading || !selectedDomain}
className="w-full py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50">
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
Activate Yield
</button>
</>
)}
</div>
</div>
</div>

View File

@ -31,22 +31,24 @@ import {
} from 'lucide-react'
import clsx from 'clsx'
// Yield Simulator Component - SIMPLIFIED & COMPACT
// Yield Simulator Component - CONSERVATIVE VALUES
function YieldSimulator() {
const [traffic, setTraffic] = useState(2500)
const [traffic, setTraffic] = useState(1000)
const [vertical, setVertical] = useState('finance')
// Vertical data (average CPA and Conversion)
// Vertical data - CONSERVATIVE conversion rates and CPAs
// These are industry-realistic values for generic domain traffic
const verticals: Record<string, { cpa: number, conv: number, label: string }> = {
finance: { cpa: 120, conv: 3.5, label: 'Finance & Loans' },
insurance: { cpa: 80, conv: 2.8, label: 'Insurance' },
legal: { cpa: 150, conv: 1.5, label: 'Legal Services' },
medical: { cpa: 60, conv: 4.0, label: 'Medical / Health' },
tech: { cpa: 45, conv: 2.2, label: 'Software / B2B' }
finance: { cpa: 25, conv: 0.8, label: 'Finance & Loans' },
insurance: { cpa: 18, conv: 0.6, label: 'Insurance' },
legal: { cpa: 35, conv: 0.4, label: 'Legal Services' },
medical: { cpa: 15, conv: 1.0, label: 'Medical / Health' },
tech: { cpa: 12, conv: 0.5, label: 'Software / B2B' }
}
const currentVertical = verticals[vertical]
const monthlyRevenue = Math.floor(traffic * (currentVertical.conv / 100) * currentVertical.cpa)
const conversions = Math.floor(traffic * (currentVertical.conv / 100))
const monthlyRevenue = conversions * currentVertical.cpa
const annualYield = monthlyRevenue * 12
return (
@ -80,7 +82,7 @@ function YieldSimulator() {
<input
type="range"
min="100"
max="10000"
max="5000"
step="100"
value={traffic}
onChange={(e) => setTraffic(parseInt(e.target.value))}
@ -121,12 +123,29 @@ function YieldSimulator() {
{/* Result Area */}
<div className="bg-white/[0.03] border border-white/5 p-5 relative overflow-hidden">
<div className="relative z-10">
<div className="flex justify-between items-end mb-2">
<span className="text-[10px] font-mono uppercase tracking-widest text-white/40">Annual Revenue</span>
<div className="flex justify-between items-end mb-3">
<span className="text-[10px] font-mono uppercase tracking-widest text-white/40">Est. Annual</span>
<div className="text-right">
<span className="block font-display text-3xl text-white tracking-tight leading-none">${annualYield.toLocaleString('en-US')}</span>
</div>
</div>
{/* Conversion breakdown */}
<div className="bg-white/[0.02] border border-white/5 p-3 mb-3">
<div className="flex justify-between text-[10px] font-mono mb-1">
<span className="text-white/40">Visitors</span>
<span className="text-white">{traffic.toLocaleString()}</span>
</div>
<div className="flex justify-between text-[10px] font-mono mb-1">
<span className="text-white/40">Conv. Rate</span>
<span className="text-accent">{currentVertical.conv}%</span>
</div>
<div className="flex justify-between text-[10px] font-mono">
<span className="text-white/40">Conversions/mo</span>
<span className="text-white">{conversions}</span>
</div>
</div>
<div className="flex justify-between items-center pt-3 border-t border-white/5 text-[10px] font-mono text-white/30">
<span>${monthlyRevenue.toLocaleString('en-US')} / mo</span>
<span>CPA: ${currentVertical.cpa}</span>