diff --git a/backend/app/routes/portfolio.py b/backend/app/routes/portfolio.py
new file mode 100755
index 0000000..543cb32
--- /dev/null
+++ b/backend/app/routes/portfolio.py
@@ -0,0 +1,542 @@
+"""Portfolio API routes."""
+from datetime import datetime
+from typing import Optional, List
+from fastapi import APIRouter, Depends, HTTPException, status, Query
+from pydantic import BaseModel, Field
+from sqlalchemy import select, func, and_
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.database import get_db
+from app.routes.auth import get_current_user
+from app.models.user import User
+from app.models.portfolio import PortfolioDomain, DomainValuation
+from app.services.valuation import valuation_service
+
+router = APIRouter(prefix="/portfolio", tags=["portfolio"])
+
+
+# ============== Schemas ==============
+
+class PortfolioDomainCreate(BaseModel):
+ """Schema for creating a portfolio domain."""
+ domain: str = Field(..., min_length=3, max_length=255)
+ purchase_date: Optional[datetime] = None
+ purchase_price: Optional[float] = Field(None, ge=0)
+ purchase_registrar: Optional[str] = None
+ registrar: Optional[str] = None
+ renewal_date: Optional[datetime] = None
+ renewal_cost: Optional[float] = Field(None, ge=0)
+ auto_renew: bool = True
+ notes: Optional[str] = None
+ tags: Optional[str] = None
+
+
+class PortfolioDomainUpdate(BaseModel):
+ """Schema for updating a portfolio domain."""
+ purchase_date: Optional[datetime] = None
+ purchase_price: Optional[float] = Field(None, ge=0)
+ purchase_registrar: Optional[str] = None
+ registrar: Optional[str] = None
+ renewal_date: Optional[datetime] = None
+ renewal_cost: Optional[float] = Field(None, ge=0)
+ auto_renew: Optional[bool] = None
+ status: Optional[str] = None
+ notes: Optional[str] = None
+ tags: Optional[str] = None
+
+
+class PortfolioDomainSell(BaseModel):
+ """Schema for marking a domain as sold."""
+ sale_date: datetime
+ sale_price: float = Field(..., ge=0)
+
+
+class PortfolioDomainResponse(BaseModel):
+ """Response schema for portfolio domain."""
+ id: int
+ domain: str
+ purchase_date: Optional[datetime]
+ purchase_price: Optional[float]
+ purchase_registrar: Optional[str]
+ registrar: Optional[str]
+ renewal_date: Optional[datetime]
+ renewal_cost: Optional[float]
+ auto_renew: bool
+ estimated_value: Optional[float]
+ value_updated_at: Optional[datetime]
+ is_sold: bool
+ sale_date: Optional[datetime]
+ sale_price: Optional[float]
+ status: str
+ notes: Optional[str]
+ tags: Optional[str]
+ roi: Optional[float]
+ created_at: datetime
+ updated_at: datetime
+
+ class Config:
+ from_attributes = True
+
+
+class PortfolioSummary(BaseModel):
+ """Summary of user's portfolio."""
+ total_domains: int
+ active_domains: int
+ sold_domains: int
+ total_invested: float
+ total_value: float
+ total_sold_value: float
+ unrealized_profit: float
+ realized_profit: float
+ overall_roi: float
+
+
+class ValuationResponse(BaseModel):
+ """Response schema for domain valuation."""
+ domain: str
+ estimated_value: float
+ currency: str
+ scores: dict
+ factors: dict
+ confidence: str
+ source: str
+ calculated_at: str
+
+
+# ============== Portfolio Endpoints ==============
+
+@router.get("", response_model=List[PortfolioDomainResponse])
+async def get_portfolio(
+ status: Optional[str] = Query(None, description="Filter by status"),
+ sort_by: str = Query("created_at", description="Sort field"),
+ sort_order: str = Query("desc", description="Sort order (asc/desc)"),
+ limit: int = Query(100, le=500),
+ offset: int = Query(0, ge=0),
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Get user's portfolio domains."""
+ query = select(PortfolioDomain).where(PortfolioDomain.user_id == current_user.id)
+
+ # Filter by status
+ if status:
+ query = query.where(PortfolioDomain.status == status)
+
+ # Sorting
+ sort_column = getattr(PortfolioDomain, sort_by, PortfolioDomain.created_at)
+ if sort_order == "asc":
+ query = query.order_by(sort_column.asc())
+ else:
+ query = query.order_by(sort_column.desc())
+
+ # Pagination
+ query = query.offset(offset).limit(limit)
+
+ result = await db.execute(query)
+ domains = result.scalars().all()
+
+ # Calculate ROI for each domain
+ responses = []
+ for d in domains:
+ response = PortfolioDomainResponse(
+ id=d.id,
+ domain=d.domain,
+ purchase_date=d.purchase_date,
+ purchase_price=d.purchase_price,
+ purchase_registrar=d.purchase_registrar,
+ registrar=d.registrar,
+ renewal_date=d.renewal_date,
+ renewal_cost=d.renewal_cost,
+ auto_renew=d.auto_renew,
+ estimated_value=d.estimated_value,
+ value_updated_at=d.value_updated_at,
+ is_sold=d.is_sold,
+ sale_date=d.sale_date,
+ sale_price=d.sale_price,
+ status=d.status,
+ notes=d.notes,
+ tags=d.tags,
+ roi=d.roi,
+ created_at=d.created_at,
+ updated_at=d.updated_at,
+ )
+ responses.append(response)
+
+ return responses
+
+
+@router.get("/summary", response_model=PortfolioSummary)
+async def get_portfolio_summary(
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Get portfolio summary statistics."""
+ result = await db.execute(
+ select(PortfolioDomain).where(PortfolioDomain.user_id == current_user.id)
+ )
+ domains = result.scalars().all()
+
+ total_domains = len(domains)
+ active_domains = sum(1 for d in domains if d.status == "active" and not d.is_sold)
+ sold_domains = sum(1 for d in domains if d.is_sold)
+
+ total_invested = sum(d.purchase_price or 0 for d in domains)
+ total_value = sum(d.estimated_value or 0 for d in domains if not d.is_sold)
+ total_sold_value = sum(d.sale_price or 0 for d in domains if d.is_sold)
+
+ # Calculate active investment for ROI
+ active_investment = sum(d.purchase_price or 0 for d in domains if not d.is_sold)
+ sold_investment = sum(d.purchase_price or 0 for d in domains if d.is_sold)
+
+ unrealized_profit = total_value - active_investment
+ realized_profit = total_sold_value - sold_investment
+
+ overall_roi = 0.0
+ if total_invested > 0:
+ overall_roi = ((total_value + total_sold_value - total_invested) / total_invested) * 100
+
+ return PortfolioSummary(
+ total_domains=total_domains,
+ active_domains=active_domains,
+ sold_domains=sold_domains,
+ total_invested=round(total_invested, 2),
+ total_value=round(total_value, 2),
+ total_sold_value=round(total_sold_value, 2),
+ unrealized_profit=round(unrealized_profit, 2),
+ realized_profit=round(realized_profit, 2),
+ overall_roi=round(overall_roi, 2),
+ )
+
+
+@router.post("", response_model=PortfolioDomainResponse, status_code=status.HTTP_201_CREATED)
+async def add_portfolio_domain(
+ data: PortfolioDomainCreate,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Add a domain to portfolio."""
+ # Check if domain already exists in user's portfolio
+ existing = await db.execute(
+ select(PortfolioDomain).where(
+ and_(
+ PortfolioDomain.user_id == current_user.id,
+ PortfolioDomain.domain == data.domain.lower(),
+ )
+ )
+ )
+ if existing.scalar_one_or_none():
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Domain already in portfolio",
+ )
+
+ # Get initial valuation
+ valuation = await valuation_service.estimate_value(data.domain, db, save_result=True)
+ estimated_value = valuation.get("estimated_value") if "error" not in valuation else None
+
+ # Create portfolio entry
+ domain = PortfolioDomain(
+ user_id=current_user.id,
+ domain=data.domain.lower(),
+ purchase_date=data.purchase_date,
+ purchase_price=data.purchase_price,
+ purchase_registrar=data.purchase_registrar,
+ registrar=data.registrar or data.purchase_registrar,
+ renewal_date=data.renewal_date,
+ renewal_cost=data.renewal_cost,
+ auto_renew=data.auto_renew,
+ estimated_value=estimated_value,
+ value_updated_at=datetime.utcnow() if estimated_value else None,
+ notes=data.notes,
+ tags=data.tags,
+ )
+
+ db.add(domain)
+ await db.commit()
+ await db.refresh(domain)
+
+ return PortfolioDomainResponse(
+ id=domain.id,
+ domain=domain.domain,
+ purchase_date=domain.purchase_date,
+ purchase_price=domain.purchase_price,
+ purchase_registrar=domain.purchase_registrar,
+ registrar=domain.registrar,
+ renewal_date=domain.renewal_date,
+ renewal_cost=domain.renewal_cost,
+ auto_renew=domain.auto_renew,
+ estimated_value=domain.estimated_value,
+ value_updated_at=domain.value_updated_at,
+ is_sold=domain.is_sold,
+ sale_date=domain.sale_date,
+ sale_price=domain.sale_price,
+ status=domain.status,
+ notes=domain.notes,
+ tags=domain.tags,
+ roi=domain.roi,
+ created_at=domain.created_at,
+ updated_at=domain.updated_at,
+ )
+
+
+@router.get("/{domain_id}", response_model=PortfolioDomainResponse)
+async def get_portfolio_domain(
+ domain_id: int,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Get a specific portfolio domain."""
+ result = await db.execute(
+ select(PortfolioDomain).where(
+ and_(
+ PortfolioDomain.id == domain_id,
+ PortfolioDomain.user_id == current_user.id,
+ )
+ )
+ )
+ domain = result.scalar_one_or_none()
+
+ if not domain:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Domain not found in portfolio",
+ )
+
+ return PortfolioDomainResponse(
+ id=domain.id,
+ domain=domain.domain,
+ purchase_date=domain.purchase_date,
+ purchase_price=domain.purchase_price,
+ purchase_registrar=domain.purchase_registrar,
+ registrar=domain.registrar,
+ renewal_date=domain.renewal_date,
+ renewal_cost=domain.renewal_cost,
+ auto_renew=domain.auto_renew,
+ estimated_value=domain.estimated_value,
+ value_updated_at=domain.value_updated_at,
+ is_sold=domain.is_sold,
+ sale_date=domain.sale_date,
+ sale_price=domain.sale_price,
+ status=domain.status,
+ notes=domain.notes,
+ tags=domain.tags,
+ roi=domain.roi,
+ created_at=domain.created_at,
+ updated_at=domain.updated_at,
+ )
+
+
+@router.put("/{domain_id}", response_model=PortfolioDomainResponse)
+async def update_portfolio_domain(
+ domain_id: int,
+ data: PortfolioDomainUpdate,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Update a portfolio domain."""
+ result = await db.execute(
+ select(PortfolioDomain).where(
+ and_(
+ PortfolioDomain.id == domain_id,
+ PortfolioDomain.user_id == current_user.id,
+ )
+ )
+ )
+ domain = result.scalar_one_or_none()
+
+ if not domain:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Domain not found in portfolio",
+ )
+
+ # Update fields
+ update_data = data.model_dump(exclude_unset=True)
+ for field, value in update_data.items():
+ setattr(domain, field, value)
+
+ await db.commit()
+ await db.refresh(domain)
+
+ return PortfolioDomainResponse(
+ id=domain.id,
+ domain=domain.domain,
+ purchase_date=domain.purchase_date,
+ purchase_price=domain.purchase_price,
+ purchase_registrar=domain.purchase_registrar,
+ registrar=domain.registrar,
+ renewal_date=domain.renewal_date,
+ renewal_cost=domain.renewal_cost,
+ auto_renew=domain.auto_renew,
+ estimated_value=domain.estimated_value,
+ value_updated_at=domain.value_updated_at,
+ is_sold=domain.is_sold,
+ sale_date=domain.sale_date,
+ sale_price=domain.sale_price,
+ status=domain.status,
+ notes=domain.notes,
+ tags=domain.tags,
+ roi=domain.roi,
+ created_at=domain.created_at,
+ updated_at=domain.updated_at,
+ )
+
+
+@router.post("/{domain_id}/sell", response_model=PortfolioDomainResponse)
+async def mark_domain_sold(
+ domain_id: int,
+ data: PortfolioDomainSell,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Mark a domain as sold."""
+ result = await db.execute(
+ select(PortfolioDomain).where(
+ and_(
+ PortfolioDomain.id == domain_id,
+ PortfolioDomain.user_id == current_user.id,
+ )
+ )
+ )
+ domain = result.scalar_one_or_none()
+
+ if not domain:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Domain not found in portfolio",
+ )
+
+ domain.is_sold = True
+ domain.sale_date = data.sale_date
+ domain.sale_price = data.sale_price
+ domain.status = "sold"
+
+ await db.commit()
+ await db.refresh(domain)
+
+ return PortfolioDomainResponse(
+ id=domain.id,
+ domain=domain.domain,
+ purchase_date=domain.purchase_date,
+ purchase_price=domain.purchase_price,
+ purchase_registrar=domain.purchase_registrar,
+ registrar=domain.registrar,
+ renewal_date=domain.renewal_date,
+ renewal_cost=domain.renewal_cost,
+ auto_renew=domain.auto_renew,
+ estimated_value=domain.estimated_value,
+ value_updated_at=domain.value_updated_at,
+ is_sold=domain.is_sold,
+ sale_date=domain.sale_date,
+ sale_price=domain.sale_price,
+ status=domain.status,
+ notes=domain.notes,
+ tags=domain.tags,
+ roi=domain.roi,
+ created_at=domain.created_at,
+ updated_at=domain.updated_at,
+ )
+
+
+@router.delete("/{domain_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_portfolio_domain(
+ domain_id: int,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Delete a domain from portfolio."""
+ result = await db.execute(
+ select(PortfolioDomain).where(
+ and_(
+ PortfolioDomain.id == domain_id,
+ PortfolioDomain.user_id == current_user.id,
+ )
+ )
+ )
+ domain = result.scalar_one_or_none()
+
+ if not domain:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Domain not found in portfolio",
+ )
+
+ await db.delete(domain)
+ await db.commit()
+
+
+@router.post("/{domain_id}/refresh-value", response_model=PortfolioDomainResponse)
+async def refresh_domain_value(
+ domain_id: int,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Refresh the estimated value of a portfolio domain."""
+ result = await db.execute(
+ select(PortfolioDomain).where(
+ and_(
+ PortfolioDomain.id == domain_id,
+ PortfolioDomain.user_id == current_user.id,
+ )
+ )
+ )
+ domain = result.scalar_one_or_none()
+
+ if not domain:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Domain not found in portfolio",
+ )
+
+ # Get new valuation
+ valuation = await valuation_service.estimate_value(domain.domain, db, save_result=True)
+
+ if "error" not in valuation:
+ domain.estimated_value = valuation["estimated_value"]
+ domain.value_updated_at = datetime.utcnow()
+ await db.commit()
+ await db.refresh(domain)
+
+ return PortfolioDomainResponse(
+ id=domain.id,
+ domain=domain.domain,
+ purchase_date=domain.purchase_date,
+ purchase_price=domain.purchase_price,
+ purchase_registrar=domain.purchase_registrar,
+ registrar=domain.registrar,
+ renewal_date=domain.renewal_date,
+ renewal_cost=domain.renewal_cost,
+ auto_renew=domain.auto_renew,
+ estimated_value=domain.estimated_value,
+ value_updated_at=domain.value_updated_at,
+ is_sold=domain.is_sold,
+ sale_date=domain.sale_date,
+ sale_price=domain.sale_price,
+ status=domain.status,
+ notes=domain.notes,
+ tags=domain.tags,
+ roi=domain.roi,
+ created_at=domain.created_at,
+ updated_at=domain.updated_at,
+ )
+
+
+# ============== Valuation Endpoints ==============
+
+@router.get("/valuation/{domain}", response_model=ValuationResponse)
+async def get_domain_valuation(
+ domain: str,
+ current_user: User = Depends(get_current_user),
+ db: AsyncSession = Depends(get_db),
+):
+ """Get estimated value for any domain."""
+ valuation = await valuation_service.estimate_value(domain, db, save_result=True)
+
+ if "error" in valuation:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=valuation["error"],
+ )
+
+ return ValuationResponse(**valuation)
+
diff --git a/backend/scripts/seed_auctions.py b/backend/scripts/seed_auctions.py
new file mode 100755
index 0000000..bc0f975
--- /dev/null
+++ b/backend/scripts/seed_auctions.py
@@ -0,0 +1,36 @@
+"""Seed auction data for development."""
+import asyncio
+import sys
+import os
+
+# Add parent directory to path
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from app.database import AsyncSessionLocal
+from app.services.auction_scraper import auction_scraper
+
+
+async def main():
+ """Seed auction data."""
+ async with AsyncSessionLocal() as db:
+ print("Seeding sample auction data...")
+ result = await auction_scraper.seed_sample_auctions(db)
+ print(f"✓ Seeded {result['found']} auctions ({result['new']} new, {result['updated']} updated)")
+
+ # Also try to scrape real data
+ print("\nAttempting to scrape real auction data...")
+ try:
+ scrape_result = await auction_scraper.scrape_all_platforms(db)
+ print(f"✓ Scraped {scrape_result['total_found']} auctions from platforms:")
+ for platform, stats in scrape_result['platforms'].items():
+ print(f" - {platform}: {stats.get('found', 0)} found")
+ if scrape_result['errors']:
+ print(f" Errors: {scrape_result['errors']}")
+ except Exception as e:
+ print(f" Scraping failed (this is okay): {e}")
+
+ print("\n✓ Done!")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/frontend/next.config.js b/frontend/next.config.js
index 3ebd517..a34a437 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -103,6 +103,28 @@ const nextConfig = {
destination: '/terminal/intel/:tld*',
permanent: true,
},
+ // Public Intel → Discover
+ {
+ source: '/intel',
+ destination: '/discover',
+ permanent: true,
+ },
+ {
+ source: '/intel/:tld*',
+ destination: '/discover/:tld*',
+ permanent: true,
+ },
+ // Old TLD pricing → Discover
+ {
+ source: '/tld-pricing',
+ destination: '/discover',
+ permanent: true,
+ },
+ {
+ source: '/tld-pricing/:tld*',
+ destination: '/discover/:tld*',
+ permanent: true,
+ },
// Listings → LISTING
{
source: '/terminal/listings',
diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt
index 786c58a..d6ab936 100644
--- a/frontend/public/robots.txt
+++ b/frontend/public/robots.txt
@@ -9,9 +9,9 @@ Disallow: /forgot-password
Disallow: /reset-password
# Allow specific public pages
-Allow: /intel/$
-Allow: /intel/*.css
-Allow: /intel/*.js
+Allow: /discover/$
+Allow: /discover/*.css
+Allow: /discover/*.js
Allow: /market
Allow: /pricing
Allow: /about
diff --git a/frontend/src/app/auctions/layout.tsx b/frontend/src/app/auctions/layout.tsx
new file mode 100755
index 0000000..7438375
--- /dev/null
+++ b/frontend/src/app/auctions/layout.tsx
@@ -0,0 +1,106 @@
+import { Metadata } from 'next'
+
+const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch'
+
+export const metadata: Metadata = {
+ title: 'Domain Auctions — Smart Pounce',
+ description: 'Find undervalued domain auctions from GoDaddy, Sedo, NameJet, SnapNames & DropCatch. Our Smart Pounce algorithm identifies the best opportunities with transparent valuations.',
+ keywords: [
+ 'domain auctions',
+ 'expired domains',
+ 'domain bidding',
+ 'GoDaddy auctions',
+ 'Sedo domains',
+ 'NameJet',
+ 'domain investment',
+ 'undervalued domains',
+ 'domain flipping',
+ ],
+ openGraph: {
+ title: 'Domain Auctions — Smart Pounce by pounce',
+ description: 'Find undervalued domain auctions. Transparent valuations, multiple platforms, no payment handling.',
+ url: `${siteUrl}/auctions`,
+ type: 'website',
+ images: [
+ {
+ url: `${siteUrl}/og-auctions.png`,
+ width: 1200,
+ height: 630,
+ alt: 'Smart Pounce - Domain Auction Aggregator',
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Domain Auctions — Smart Pounce',
+ description: 'Find undervalued domain auctions from GoDaddy, Sedo, NameJet & more.',
+ },
+ alternates: {
+ canonical: `${siteUrl}/auctions`,
+ },
+}
+
+// JSON-LD for Auctions page
+const jsonLd = {
+ '@context': 'https://schema.org',
+ '@type': 'WebPage',
+ name: 'Domain Auctions — Smart Pounce',
+ description: 'Aggregated domain auctions from multiple platforms with transparent algorithmic valuations.',
+ url: `${siteUrl}/auctions`,
+ isPartOf: {
+ '@type': 'WebSite',
+ name: 'pounce',
+ url: siteUrl,
+ },
+ about: {
+ '@type': 'Service',
+ name: 'Smart Pounce',
+ description: 'Domain auction aggregation and opportunity analysis',
+ provider: {
+ '@type': 'Organization',
+ name: 'pounce',
+ },
+ },
+ mainEntity: {
+ '@type': 'ItemList',
+ name: 'Domain Auctions',
+ description: 'Live domain auctions from GoDaddy, Sedo, NameJet, SnapNames, and DropCatch',
+ itemListElement: [
+ {
+ '@type': 'ListItem',
+ position: 1,
+ name: 'GoDaddy Auctions',
+ url: 'https://auctions.godaddy.com',
+ },
+ {
+ '@type': 'ListItem',
+ position: 2,
+ name: 'Sedo',
+ url: 'https://sedo.com',
+ },
+ {
+ '@type': 'ListItem',
+ position: 3,
+ name: 'NameJet',
+ url: 'https://namejet.com',
+ },
+ ],
+ },
+}
+
+export default function AuctionsLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+ <>
+
+ {children}
+ >
+ )
+}
+
diff --git a/frontend/src/app/auctions/page.tsx b/frontend/src/app/auctions/page.tsx
index ac76358..3b4cbe3 100644
--- a/frontend/src/app/auctions/page.tsx
+++ b/frontend/src/app/auctions/page.tsx
@@ -9,11 +9,11 @@ import { useRouter } from 'next/navigation'
*/
export default function AuctionsRedirect() {
const router = useRouter()
-
+
useEffect(() => {
router.replace('/market')
}, [router])
-
+
return (
diff --git a/frontend/src/app/careers/page.tsx b/frontend/src/app/careers/page.tsx
new file mode 100755
index 0000000..e5ea387
--- /dev/null
+++ b/frontend/src/app/careers/page.tsx
@@ -0,0 +1,209 @@
+'use client'
+
+import { Header } from '@/components/Header'
+import { Footer } from '@/components/Footer'
+import { Briefcase, MapPin, Clock, ArrowRight, Code, Palette, LineChart, Users, Heart, Coffee, Laptop, Globe } from 'lucide-react'
+import Link from 'next/link'
+
+const openPositions = [
+ {
+ title: 'Senior Backend Engineer',
+ department: 'Engineering',
+ location: 'Remote (Europe)',
+ type: 'Full-time',
+ description: 'Build and scale our domain intelligence infrastructure using Python and FastAPI.',
+ icon: Code,
+ },
+ {
+ title: 'Frontend Developer',
+ department: 'Engineering',
+ location: 'Remote (Worldwide)',
+ type: 'Full-time',
+ description: 'Create beautiful, performant user interfaces with React and Next.js.',
+ icon: Palette,
+ },
+ {
+ title: 'Data Engineer',
+ department: 'Data',
+ location: 'Zurich, Switzerland',
+ type: 'Full-time',
+ description: 'Design data pipelines for domain pricing and market analytics.',
+ icon: LineChart,
+ },
+ {
+ title: 'Customer Success Manager',
+ department: 'Customer Success',
+ location: 'Remote (Europe)',
+ type: 'Full-time',
+ description: 'Help our customers succeed and get the most out of pounce.',
+ icon: Users,
+ },
+]
+
+const benefits = [
+ {
+ icon: Globe,
+ title: 'Remote-First',
+ description: 'Work from anywhere in the world with flexible hours.',
+ },
+ {
+ icon: Heart,
+ title: 'Health & Wellness',
+ description: 'Comprehensive health insurance and wellness budget.',
+ },
+ {
+ icon: Coffee,
+ title: 'Learning Budget',
+ description: 'Annual budget for courses, conferences, and books.',
+ },
+ {
+ icon: Laptop,
+ title: 'Equipment',
+ description: 'Top-of-the-line hardware and home office setup.',
+ },
+]
+
+const values = [
+ 'We ship fast and iterate based on feedback',
+ 'We value transparency and open communication',
+ 'We prioritize user experience over features',
+ 'We believe in work-life balance',
+]
+
+export default function CareersPage() {
+ return (
+
+ {/* Ambient glow */}
+
+
+
+
+
+
+ {/* Hero */}
+
+
+
+ Join Our Team
+
+
+ Build the future of
+
+ domain intelligence
+
+
+ We're a small, focused team building tools that help thousands of people
+ monitor and acquire valuable domains. Join us.
+
+
+
+ {/* Values */}
+
+
How We Work
+
+ {values.map((value) => (
+
+
+ {value}
+
+ ))}
+
+
+
+ {/* Benefits */}
+
+ Benefits & Perks
+
+ {benefits.map((benefit, i) => (
+
+
+
+
+
{benefit.title}
+
{benefit.description}
+
+ ))}
+
+
+
+ {/* Open Positions */}
+
+
+ Open Positions
+
+
+ {openPositions.map((position, i) => (
+
+
+
+
+
+
+ {position.title}
+
+
{position.description}
+
+
+ {position.department}
+
+
+
+ {position.location}
+
+
+
+ {position.type}
+
+
+
+
+
+ Apply
+
+
+
+
+ ))}
+
+
+
+ {/* CTA */}
+
+
+ Don't see the right role?
+
+
+ We're always looking for talented people. Send us your resume
+ and we'll keep you in mind for future opportunities.
+
+
+ Send General Application
+
+
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/src/app/command/alerts/page.tsx b/frontend/src/app/command/alerts/page.tsx
new file mode 100755
index 0000000..f72098d
--- /dev/null
+++ b/frontend/src/app/command/alerts/page.tsx
@@ -0,0 +1,597 @@
+'use client'
+
+import { useEffect, useState, useMemo, useCallback, memo } from 'react'
+import { useStore } from '@/lib/store'
+import { api } from '@/lib/api'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import { PageContainer, StatCard, Badge, ActionButton } from '@/components/PremiumTable'
+import {
+ Plus,
+ Bell,
+ Target,
+ Zap,
+ Loader2,
+ Trash2,
+ CheckCircle,
+ AlertCircle,
+ X,
+ Play,
+ Pause,
+ Mail,
+ Settings,
+ TestTube,
+ ChevronDown,
+ ChevronUp,
+} from 'lucide-react'
+import clsx from 'clsx'
+
+interface SniperAlert {
+ id: number
+ name: string
+ description: string | null
+ tlds: string | null
+ keywords: string | null
+ exclude_keywords: string | null
+ max_length: number | null
+ min_length: number | null
+ max_price: number | null
+ min_price: number | null
+ max_bids: number | null
+ ending_within_hours: number | null
+ platforms: string | null
+ no_numbers: boolean
+ no_hyphens: boolean
+ exclude_chars: string | null
+ notify_email: boolean
+ notify_sms: boolean
+ is_active: boolean
+ matches_count: number
+ notifications_sent: number
+ last_matched_at: string | null
+ created_at: string
+}
+
+interface TestResult {
+ alert_name: string
+ auctions_checked: number
+ matches_found: number
+ matches: Array<{
+ domain: string
+ platform: string
+ current_bid: number
+ num_bids: number
+ end_time: string
+ }>
+ message: string
+}
+
+export default function SniperAlertsPage() {
+ const { subscription } = useStore()
+
+ const [alerts, setAlerts] = useState
([])
+ const [loading, setLoading] = useState(true)
+ const [showCreateModal, setShowCreateModal] = useState(false)
+ const [creating, setCreating] = useState(false)
+ const [testing, setTesting] = useState(null)
+ const [testResult, setTestResult] = useState(null)
+ const [expandedAlert, setExpandedAlert] = useState(null)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+
+ // Create form
+ const [newAlert, setNewAlert] = useState({
+ name: '',
+ description: '',
+ tlds: '',
+ keywords: '',
+ exclude_keywords: '',
+ max_length: '',
+ min_length: '',
+ max_price: '',
+ min_price: '',
+ max_bids: '',
+ no_numbers: false,
+ no_hyphens: false,
+ exclude_chars: '',
+ notify_email: true,
+ })
+
+ const loadAlerts = useCallback(async () => {
+ setLoading(true)
+ try {
+ const data = await api.request('/sniper-alerts')
+ setAlerts(data)
+ } catch (err: any) {
+ setError(err.message)
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ loadAlerts()
+ }, [loadAlerts])
+
+ const handleCreate = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault()
+ setCreating(true)
+ setError(null)
+
+ try {
+ await api.request('/sniper-alerts', {
+ method: 'POST',
+ body: JSON.stringify({
+ name: newAlert.name,
+ description: newAlert.description || null,
+ tlds: newAlert.tlds || null,
+ keywords: newAlert.keywords || null,
+ exclude_keywords: newAlert.exclude_keywords || null,
+ max_length: newAlert.max_length ? parseInt(newAlert.max_length) : null,
+ min_length: newAlert.min_length ? parseInt(newAlert.min_length) : null,
+ max_price: newAlert.max_price ? parseFloat(newAlert.max_price) : null,
+ min_price: newAlert.min_price ? parseFloat(newAlert.min_price) : null,
+ max_bids: newAlert.max_bids ? parseInt(newAlert.max_bids) : null,
+ no_numbers: newAlert.no_numbers,
+ no_hyphens: newAlert.no_hyphens,
+ exclude_chars: newAlert.exclude_chars || null,
+ notify_email: newAlert.notify_email,
+ }),
+ })
+ setSuccess('Sniper Alert created!')
+ setShowCreateModal(false)
+ setNewAlert({
+ name: '', description: '', tlds: '', keywords: '', exclude_keywords: '',
+ max_length: '', min_length: '', max_price: '', min_price: '', max_bids: '',
+ no_numbers: false, no_hyphens: false, exclude_chars: '', notify_email: true,
+ })
+ loadAlerts()
+ } catch (err: any) {
+ setError(err.message)
+ } finally {
+ setCreating(false)
+ }
+ }, [newAlert, loadAlerts])
+
+ const handleToggle = useCallback(async (alert: SniperAlert) => {
+ try {
+ await api.request(`/sniper-alerts/${alert.id}`, {
+ method: 'PUT',
+ body: JSON.stringify({ is_active: !alert.is_active }),
+ })
+ loadAlerts()
+ } catch (err: any) {
+ setError(err.message)
+ }
+ }, [loadAlerts])
+
+ const handleDelete = useCallback(async (alert: SniperAlert) => {
+ if (!confirm(`Delete alert "${alert.name}"?`)) return
+
+ try {
+ await api.request(`/sniper-alerts/${alert.id}`, { method: 'DELETE' })
+ setSuccess('Alert deleted')
+ loadAlerts()
+ } catch (err: any) {
+ setError(err.message)
+ }
+ }, [loadAlerts])
+
+ const handleTest = useCallback(async (alert: SniperAlert) => {
+ setTesting(alert.id)
+ setTestResult(null)
+
+ try {
+ const result = await api.request(`/sniper-alerts/${alert.id}/test`, {
+ method: 'POST',
+ })
+ setTestResult(result)
+ setExpandedAlert(alert.id)
+ } catch (err: any) {
+ setError(err.message)
+ } finally {
+ setTesting(null)
+ }
+ }, [])
+
+ // Memoized stats
+ const stats = useMemo(() => ({
+ activeAlerts: alerts.filter(a => a.is_active).length,
+ totalMatches: alerts.reduce((sum, a) => sum + a.matches_count, 0),
+ notificationsSent: alerts.reduce((sum, a) => sum + a.notifications_sent, 0),
+ }), [alerts])
+
+ const tier = subscription?.tier || 'scout'
+ const limits = { scout: 2, trader: 10, tycoon: 50 }
+ const maxAlerts = limits[tier as keyof typeof limits] || 2
+
+ return (
+ setShowCreateModal(true)} disabled={alerts.length >= maxAlerts} icon={Plus}>
+ New Alert
+
+ }
+ >
+
+ {/* Messages */}
+ {error && (
+
+
+
{error}
+
setError(null)}>
+
+ )}
+
+ {success && (
+
+
+
{success}
+
setSuccess(null)}>
+
+ )}
+
+ {/* Stats */}
+
+
+
+
+
+
+
+ {/* Alerts List */}
+ {loading ? (
+
+
+
+ ) : alerts.length === 0 ? (
+
+
+
No Sniper Alerts
+
+ Create alerts to get notified when domains matching your criteria appear in auctions.
+
+
setShowCreateModal(true)}
+ className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
+ >
+
+ Create Alert
+
+
+ ) : (
+
+ {alerts.map((alert) => (
+
+ {/* Header */}
+
+
+
+
+
{alert.name}
+
+ {alert.is_active ? 'Active' : 'Paused'}
+
+
+ {alert.description && (
+
{alert.description}
+ )}
+
+
+ {/* Stats */}
+
+
+
{alert.matches_count}
+
Matches
+
+
+
{alert.notifications_sent}
+
Notified
+
+
+
+ {/* Actions */}
+
+
handleTest(alert)}
+ disabled={testing === alert.id}
+ className="flex items-center gap-1.5 px-3 py-2 bg-foreground/5 text-foreground-muted text-sm font-medium rounded-lg hover:bg-foreground/10 transition-all"
+ >
+ {testing === alert.id ? (
+
+ ) : (
+
+ )}
+ Test
+
+
+
handleToggle(alert)}
+ className={clsx(
+ "flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-lg transition-all",
+ alert.is_active
+ ? "bg-amber-500/10 text-amber-400 hover:bg-amber-500/20"
+ : "bg-accent/10 text-accent hover:bg-accent/20"
+ )}
+ >
+ {alert.is_active ? : }
+ {alert.is_active ? 'Pause' : 'Activate'}
+
+
+
handleDelete(alert)}
+ className="p-2 text-foreground-subtle hover:text-red-400 transition-colors"
+ >
+
+
+
+
setExpandedAlert(expandedAlert === alert.id ? null : alert.id)}
+ className="p-2 text-foreground-subtle hover:text-foreground transition-colors"
+ >
+ {expandedAlert === alert.id ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Filter Summary */}
+
+ {alert.tlds && (
+
+ TLDs: {alert.tlds}
+
+ )}
+ {alert.max_length && (
+
+ Max {alert.max_length} chars
+
+ )}
+ {alert.max_price && (
+
+ Max ${alert.max_price}
+
+ )}
+ {alert.no_numbers && (
+
+ No numbers
+
+ )}
+ {alert.no_hyphens && (
+
+ No hyphens
+
+ )}
+ {alert.notify_email && (
+
+ Email
+
+ )}
+
+
+
+ {/* Test Results */}
+ {expandedAlert === alert.id && testResult && testResult.alert_name === alert.name && (
+
+
+
+
Test Results
+
+ Checked {testResult.auctions_checked} auctions
+
+
+
+ {testResult.matches_found === 0 ? (
+
{testResult.message}
+ ) : (
+
+
+ Found {testResult.matches_found} matching domains!
+
+
+ {testResult.matches.map((match, idx) => (
+
+ {match.domain}
+ ${match.current_bid}
+
+ ))}
+
+
+ )}
+
+
+ )}
+
+ ))}
+
+ )}
+
+
+ {/* Create Modal */}
+ {showCreateModal && (
+
+
+
Create Sniper Alert
+
+ Get notified when domains matching your criteria appear in auctions.
+
+
+
+
+
+ setShowCreateModal(false)}
+ className="flex-1 px-4 py-3 border border-border text-foreground-muted rounded-xl hover:bg-foreground/5 transition-all"
+ >
+ Cancel
+
+
+ {creating ? : }
+ {creating ? 'Creating...' : 'Create Alert'}
+
+
+
+
+ )}
+
+ )
+}
+
diff --git a/frontend/src/app/command/auctions/page.tsx b/frontend/src/app/command/auctions/page.tsx
new file mode 100755
index 0000000..811b3ea
--- /dev/null
+++ b/frontend/src/app/command/auctions/page.tsx
@@ -0,0 +1,578 @@
+'use client'
+
+import { useEffect, useState, useMemo, useCallback, memo } from 'react'
+import { useStore } from '@/lib/store'
+import { api } from '@/lib/api'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import {
+ PremiumTable,
+ Badge,
+ PlatformBadge,
+ StatCard,
+ PageContainer,
+ SearchInput,
+ TabBar,
+ FilterBar,
+ SelectDropdown,
+ ActionButton,
+} from '@/components/PremiumTable'
+import {
+ Clock,
+ ExternalLink,
+ Flame,
+ Timer,
+ Gavel,
+ DollarSign,
+ RefreshCw,
+ Target,
+ Loader2,
+ Sparkles,
+ Eye,
+ Zap,
+ Crown,
+ Plus,
+ Check,
+} from 'lucide-react'
+import Link from 'next/link'
+import clsx from 'clsx'
+
+interface Auction {
+ domain: string
+ platform: string
+ platform_url: string
+ current_bid: number
+ currency: string
+ num_bids: number
+ end_time: string
+ time_remaining: string
+ buy_now_price: number | null
+ reserve_met: boolean | null
+ traffic: number | null
+ age_years: number | null
+ tld: string
+ affiliate_url: string
+}
+
+interface Opportunity {
+ auction: Auction
+ analysis: {
+ opportunity_score: number
+ urgency?: string
+ competition?: string
+ price_range?: string
+ recommendation: string
+ reasoning?: string
+ }
+}
+
+type TabType = 'all' | 'ending' | 'hot' | 'opportunities'
+type SortField = 'ending' | 'bid_asc' | 'bid_desc' | 'bids' | 'score'
+type FilterPreset = 'all' | 'no-trash' | 'short' | 'high-value' | 'low-competition'
+
+const PLATFORMS = [
+ { value: 'All', label: 'All Sources' },
+ { value: 'GoDaddy', label: 'GoDaddy' },
+ { value: 'Sedo', label: 'Sedo' },
+ { value: 'NameJet', label: 'NameJet' },
+ { value: 'DropCatch', label: 'DropCatch' },
+]
+
+const FILTER_PRESETS: { id: FilterPreset, label: string, icon: typeof Gavel, description: string, proOnly?: boolean }[] = [
+ { id: 'all', label: 'All', icon: Gavel, description: 'Show all auctions' },
+ { id: 'no-trash', label: 'No Trash', icon: Sparkles, description: 'Clean domains only (no spam)', proOnly: true },
+ { id: 'short', label: 'Short', icon: Zap, description: '4-letter domains or less' },
+ { id: 'high-value', label: 'High Value', icon: Crown, description: 'Premium TLDs with high activity', proOnly: true },
+ { id: 'low-competition', label: 'Low Competition', icon: Target, description: 'Under 5 bids' },
+]
+
+const PREMIUM_TLDS = ['com', 'io', 'ai', 'co', 'net', 'org', 'app', 'dev']
+
+// Pure functions (no hooks needed)
+function isCleanDomain(auction: Auction): boolean {
+ const name = auction.domain.split('.')[0]
+ if (name.includes('-')) return false
+ if (name.length > 4 && /\d/.test(name)) return false
+ if (name.length > 12) return false
+ if (!PREMIUM_TLDS.includes(auction.tld)) return false
+ return true
+}
+
+function calculateDealScore(auction: Auction): number {
+ let score = 50
+ const name = auction.domain.split('.')[0]
+ if (name.length <= 4) score += 25
+ else if (name.length <= 6) score += 15
+ else if (name.length <= 8) score += 5
+ if (['com', 'io', 'ai'].includes(auction.tld)) score += 15
+ else if (['co', 'net', 'org'].includes(auction.tld)) score += 5
+ if (auction.age_years && auction.age_years > 10) score += 15
+ else if (auction.age_years && auction.age_years > 5) score += 10
+ if (auction.num_bids >= 20) score += 10
+ else if (auction.num_bids >= 10) score += 5
+ if (isCleanDomain(auction)) score += 10
+ return Math.min(score, 100)
+}
+
+function getTimeColor(timeRemaining: string): string {
+ if (timeRemaining.includes('m') && !timeRemaining.includes('h') && !timeRemaining.includes('d')) return 'text-red-400'
+ if (timeRemaining.includes('h') && parseInt(timeRemaining) < 2) return 'text-amber-400'
+ return 'text-foreground-muted'
+}
+
+const formatCurrency = (value: number) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(value)
+}
+
+export default function AuctionsPage() {
+ const { isAuthenticated, subscription } = useStore()
+
+ const [allAuctions, setAllAuctions] = useState([])
+ const [endingSoon, setEndingSoon] = useState([])
+ const [hotAuctions, setHotAuctions] = useState([])
+ const [opportunities, setOpportunities] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+ const [activeTab, setActiveTab] = useState('all')
+ const [sortBy, setSortBy] = useState('ending')
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
+
+ // Filters
+ const [searchQuery, setSearchQuery] = useState('')
+ const [selectedPlatform, setSelectedPlatform] = useState('All')
+ const [maxBid, setMaxBid] = useState('')
+ const [filterPreset, setFilterPreset] = useState('all')
+ const [trackedDomains, setTrackedDomains] = useState>(new Set())
+ const [trackingInProgress, setTrackingInProgress] = useState(null)
+
+ const isPaidUser = subscription?.tier === 'trader' || subscription?.tier === 'tycoon'
+
+ // Data loading
+ const loadData = useCallback(async () => {
+ setLoading(true)
+ try {
+ const [auctionsData, hotData, endingData] = await Promise.all([
+ api.getAuctions(),
+ api.getHotAuctions(50),
+ api.getEndingSoonAuctions(24, 50),
+ ])
+
+ setAllAuctions(auctionsData.auctions || [])
+ setHotAuctions(hotData || [])
+ setEndingSoon(endingData || [])
+ } catch (error) {
+ console.error('Failed to load auction data:', error)
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ const loadOpportunities = useCallback(async () => {
+ try {
+ const oppData = await api.getAuctionOpportunities()
+ setOpportunities(oppData.opportunities || [])
+ } catch (e) {
+ console.error('Failed to load opportunities:', e)
+ }
+ }, [])
+
+ useEffect(() => {
+ loadData()
+ }, [loadData])
+
+ useEffect(() => {
+ if (isAuthenticated && opportunities.length === 0) {
+ loadOpportunities()
+ }
+ }, [isAuthenticated, opportunities.length, loadOpportunities])
+
+ const handleRefresh = useCallback(async () => {
+ setRefreshing(true)
+ await loadData()
+ if (isAuthenticated) await loadOpportunities()
+ setRefreshing(false)
+ }, [loadData, loadOpportunities, isAuthenticated])
+
+ const handleTrackDomain = useCallback(async (domain: string) => {
+ if (trackedDomains.has(domain)) return
+
+ setTrackingInProgress(domain)
+ try {
+ await api.addDomainToWatchlist({ domain })
+ setTrackedDomains(prev => new Set([...prev, domain]))
+ } catch (error) {
+ console.error('Failed to track domain:', error)
+ } finally {
+ setTrackingInProgress(null)
+ }
+ }, [trackedDomains])
+
+ const handleSort = useCallback((field: string) => {
+ const f = field as SortField
+ if (sortBy === f) {
+ setSortDirection(d => d === 'asc' ? 'desc' : 'asc')
+ } else {
+ setSortBy(f)
+ setSortDirection('asc')
+ }
+ }, [sortBy])
+
+ // Memoized tabs
+ const tabs = useMemo(() => [
+ { id: 'all', label: 'All', icon: Gavel, count: allAuctions.length },
+ { id: 'ending', label: 'Ending Soon', icon: Timer, count: endingSoon.length, color: 'warning' as const },
+ { id: 'hot', label: 'Hot', icon: Flame, count: hotAuctions.length },
+ { id: 'opportunities', label: 'Opportunities', icon: Target, count: opportunities.length },
+ ], [allAuctions.length, endingSoon.length, hotAuctions.length, opportunities.length])
+
+ // Filter and sort auctions
+ const sortedAuctions = useMemo(() => {
+ // Get base auctions for current tab
+ let auctions: Auction[] = []
+ switch (activeTab) {
+ case 'ending': auctions = [...endingSoon]; break
+ case 'hot': auctions = [...hotAuctions]; break
+ case 'opportunities': auctions = opportunities.map(o => o.auction); break
+ default: auctions = [...allAuctions]
+ }
+
+ // Apply preset filter
+ const baseFilter = filterPreset === 'all' && isPaidUser ? 'no-trash' : filterPreset
+ switch (baseFilter) {
+ case 'no-trash': auctions = auctions.filter(isCleanDomain); break
+ case 'short': auctions = auctions.filter(a => a.domain.split('.')[0].length <= 4); break
+ case 'high-value': auctions = auctions.filter(a =>
+ PREMIUM_TLDS.slice(0, 3).includes(a.tld) && a.num_bids >= 5 && calculateDealScore(a) >= 70
+ ); break
+ case 'low-competition': auctions = auctions.filter(a => a.num_bids < 5); break
+ }
+
+ // Apply search
+ if (searchQuery) {
+ const q = searchQuery.toLowerCase()
+ auctions = auctions.filter(a => a.domain.toLowerCase().includes(q))
+ }
+
+ // Apply platform filter
+ if (selectedPlatform !== 'All') {
+ auctions = auctions.filter(a => a.platform === selectedPlatform)
+ }
+
+ // Apply max bid
+ if (maxBid) {
+ const max = parseFloat(maxBid)
+ auctions = auctions.filter(a => a.current_bid <= max)
+ }
+
+ // Sort (skip for opportunities - already sorted by score)
+ if (activeTab !== 'opportunities') {
+ const mult = sortDirection === 'asc' ? 1 : -1
+ auctions.sort((a, b) => {
+ switch (sortBy) {
+ case 'ending': return mult * (new Date(a.end_time).getTime() - new Date(b.end_time).getTime())
+ case 'bid_asc':
+ case 'bid_desc': return mult * (a.current_bid - b.current_bid)
+ case 'bids': return mult * (b.num_bids - a.num_bids)
+ case 'score': return mult * (calculateDealScore(b) - calculateDealScore(a))
+ default: return 0
+ }
+ })
+ }
+
+ return auctions
+ }, [activeTab, allAuctions, endingSoon, hotAuctions, opportunities, filterPreset, isPaidUser, searchQuery, selectedPlatform, maxBid, sortBy, sortDirection])
+
+ // Subtitle
+ const subtitle = useMemo(() => {
+ if (loading) return 'Loading live auctions...'
+ const total = allAuctions.length
+ if (total === 0) return 'No active auctions found'
+ return `${sortedAuctions.length.toLocaleString()} auctions ${sortedAuctions.length < total ? `(filtered from ${total.toLocaleString()})` : 'across 4 platforms'}`
+ }, [loading, allAuctions.length, sortedAuctions.length])
+
+ // Get opportunity data helper
+ const getOpportunityData = useCallback((domain: string) => {
+ if (activeTab !== 'opportunities') return null
+ return opportunities.find(o => o.auction.domain === domain)?.analysis
+ }, [activeTab, opportunities])
+
+ // Table columns - memoized
+ const columns = useMemo(() => [
+ {
+ key: 'domain',
+ header: 'Domain',
+ sortable: true,
+ render: (a: Auction) => (
+
+ ),
+ },
+ {
+ key: 'platform',
+ header: 'Platform',
+ hideOnMobile: true,
+ render: (a: Auction) => (
+
+
+ {a.age_years && (
+
+ {a.age_years}y
+
+ )}
+
+ ),
+ },
+ {
+ key: 'bid_asc',
+ header: 'Bid',
+ sortable: true,
+ align: 'right' as const,
+ render: (a: Auction) => (
+
+
{formatCurrency(a.current_bid)}
+ {a.buy_now_price && (
+
Buy: {formatCurrency(a.buy_now_price)}
+ )}
+
+ ),
+ },
+ {
+ key: 'score',
+ header: 'Deal Score',
+ sortable: true,
+ align: 'center' as const,
+ hideOnMobile: true,
+ render: (a: Auction) => {
+ if (activeTab === 'opportunities') {
+ const oppData = getOpportunityData(a.domain)
+ if (oppData) {
+ return (
+
+ {oppData.opportunity_score}
+
+ )
+ }
+ }
+
+ if (!isPaidUser) {
+ return (
+
+
+
+ )
+ }
+
+ const score = calculateDealScore(a)
+ return (
+
+ = 75 ? "bg-accent/20 text-accent" :
+ score >= 50 ? "bg-amber-500/20 text-amber-400" :
+ "bg-foreground/10 text-foreground-muted"
+ )}>
+ {score}
+
+ {score >= 75 && Undervalued }
+
+ )
+ },
+ },
+ {
+ key: 'bids',
+ header: 'Bids',
+ sortable: true,
+ align: 'right' as const,
+ hideOnMobile: true,
+ render: (a: Auction) => (
+ = 20 ? "text-accent" : a.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted"
+ )}>
+ {a.num_bids}
+ {a.num_bids >= 20 && }
+
+ ),
+ },
+ {
+ key: 'ending',
+ header: 'Time Left',
+ sortable: true,
+ align: 'right' as const,
+ hideOnMobile: true,
+ render: (a: Auction) => (
+
+ {a.time_remaining}
+
+ ),
+ },
+ {
+ key: 'actions',
+ header: '',
+ align: 'right' as const,
+ render: (a: Auction) => (
+
+
{ e.preventDefault(); handleTrackDomain(a.domain) }}
+ disabled={trackedDomains.has(a.domain) || trackingInProgress === a.domain}
+ className={clsx(
+ "inline-flex items-center justify-center w-8 h-8 rounded-lg transition-all",
+ trackedDomains.has(a.domain)
+ ? "bg-accent/20 text-accent cursor-default"
+ : "bg-foreground/5 text-foreground-subtle hover:bg-accent/10 hover:text-accent"
+ )}
+ title={trackedDomains.has(a.domain) ? 'Already tracked' : 'Add to Watchlist'}
+ >
+ {trackingInProgress === a.domain ? (
+
+ ) : trackedDomains.has(a.domain) ? (
+
+ ) : (
+
+ )}
+
+
+ Bid
+
+
+ ),
+ },
+ ], [activeTab, isPaidUser, trackedDomains, trackingInProgress, handleTrackDomain, getOpportunityData])
+
+ return (
+
+ {refreshing ? '' : 'Refresh'}
+
+ }
+ >
+
+ {/* Stats */}
+
+
+
+
+
+
+
+ {/* Tabs */}
+ setActiveTab(id as TabType)} />
+
+ {/* Smart Filter Presets */}
+
+ {FILTER_PRESETS.map((preset) => {
+ const isDisabled = preset.proOnly && !isPaidUser
+ const isActive = filterPreset === preset.id
+ const Icon = preset.icon
+ return (
+ !isDisabled && setFilterPreset(preset.id)}
+ disabled={isDisabled}
+ title={isDisabled ? 'Upgrade to Trader to use this filter' : preset.description}
+ className={clsx(
+ "flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all",
+ isActive
+ ? "bg-accent text-background shadow-md"
+ : isDisabled
+ ? "text-foreground-subtle opacity-50 cursor-not-allowed"
+ : "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
+ )}
+ >
+
+ {preset.label}
+ {preset.proOnly && !isPaidUser && }
+
+ )
+ })}
+
+
+ {/* Tier notification for Scout users */}
+ {!isPaidUser && (
+
+
+
+
+
+
You're seeing the raw auction feed
+
+ Upgrade to Trader for spam-free listings, Deal Scores, and Smart Filters.
+
+
+
+ Upgrade
+
+
+ )}
+
+ {/* Filters */}
+
+
+
+
+
+ setMaxBid(e.target.value)}
+ className="w-28 h-10 pl-9 pr-3 bg-background-secondary/50 border border-border/40 rounded-xl
+ text-sm text-foreground placeholder:text-foreground-subtle
+ focus:outline-none focus:border-accent/50 transition-all"
+ />
+
+
+
+ {/* Table */}
+ `${a.domain}-${a.platform}`}
+ loading={loading}
+ sortBy={sortBy}
+ sortDirection={sortDirection}
+ onSort={handleSort}
+ emptyIcon={ }
+ emptyTitle={searchQuery ? `No auctions matching "${searchQuery}"` : "No auctions found"}
+ emptyDescription="Try adjusting your filters or check back later"
+ columns={columns}
+ />
+
+
+ )
+}
diff --git a/frontend/src/app/command/dashboard/page.tsx b/frontend/src/app/command/dashboard/page.tsx
new file mode 100755
index 0000000..8eb8e1f
--- /dev/null
+++ b/frontend/src/app/command/dashboard/page.tsx
@@ -0,0 +1,402 @@
+'use client'
+
+import { useEffect, useState, useMemo, useCallback } from 'react'
+import { useSearchParams } from 'next/navigation'
+import { useStore } from '@/lib/store'
+import { api } from '@/lib/api'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import { PremiumTable, StatCard, PageContainer, Badge, SectionHeader, SearchInput, ActionButton } from '@/components/PremiumTable'
+import { Toast, useToast } from '@/components/Toast'
+import {
+ Eye,
+ Briefcase,
+ TrendingUp,
+ Gavel,
+ Clock,
+ ExternalLink,
+ Sparkles,
+ ChevronRight,
+ Plus,
+ Zap,
+ Crown,
+ Activity,
+ Loader2,
+ Search,
+} from 'lucide-react'
+import clsx from 'clsx'
+import Link from 'next/link'
+
+interface HotAuction {
+ domain: string
+ current_bid: number
+ time_remaining: string
+ platform: string
+ affiliate_url?: string
+}
+
+interface TrendingTld {
+ tld: string
+ current_price: number
+ price_change: number
+ reason: string
+}
+
+export default function DashboardPage() {
+ const searchParams = useSearchParams()
+ const {
+ isAuthenticated,
+ isLoading,
+ user,
+ domains,
+ subscription
+ } = useStore()
+
+ const { toast, showToast, hideToast } = useToast()
+ const [hotAuctions, setHotAuctions] = useState([])
+ const [trendingTlds, setTrendingTlds] = useState([])
+ const [loadingAuctions, setLoadingAuctions] = useState(true)
+ const [loadingTlds, setLoadingTlds] = useState(true)
+ const [quickDomain, setQuickDomain] = useState('')
+ const [addingDomain, setAddingDomain] = useState(false)
+
+ // Check for upgrade success
+ useEffect(() => {
+ if (searchParams.get('upgraded') === 'true') {
+ showToast('Welcome to your upgraded plan! 🎉', 'success')
+ window.history.replaceState({}, '', '/command/dashboard')
+ }
+ }, [searchParams])
+
+ const loadDashboardData = useCallback(async () => {
+ try {
+ const [auctions, trending] = await Promise.all([
+ api.getEndingSoonAuctions(5).catch(() => []),
+ api.getTrendingTlds().catch(() => ({ trending: [] }))
+ ])
+ setHotAuctions(auctions.slice(0, 5))
+ setTrendingTlds(trending.trending?.slice(0, 4) || [])
+ } catch (error) {
+ console.error('Failed to load dashboard data:', error)
+ } finally {
+ setLoadingAuctions(false)
+ setLoadingTlds(false)
+ }
+ }, [])
+
+ // Load dashboard data
+ useEffect(() => {
+ if (isAuthenticated) {
+ loadDashboardData()
+ }
+ }, [isAuthenticated, loadDashboardData])
+
+ const handleQuickAdd = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!quickDomain.trim()) return
+
+ setAddingDomain(true)
+ try {
+ const store = useStore.getState()
+ await store.addDomain(quickDomain.trim())
+ setQuickDomain('')
+ showToast(`Added ${quickDomain.trim()} to watchlist`, 'success')
+ } catch (err: any) {
+ showToast(err.message || 'Failed to add domain', 'error')
+ } finally {
+ setAddingDomain(false)
+ }
+ }, [quickDomain, showToast])
+
+ // Memoized computed values
+ const { availableDomains, totalDomains, tierName, TierIcon, greeting, subtitle } = useMemo(() => {
+ const availableDomains = domains?.filter(d => d.is_available) || []
+ const totalDomains = domains?.length || 0
+ const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
+ const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
+
+ const hour = new Date().getHours()
+ const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening'
+
+ let subtitle = ''
+ if (availableDomains.length > 0) {
+ subtitle = `${availableDomains.length} domain${availableDomains.length !== 1 ? 's' : ''} ready to pounce!`
+ } else if (totalDomains > 0) {
+ subtitle = `Monitoring ${totalDomains} domain${totalDomains !== 1 ? 's' : ''} for you`
+ } else {
+ subtitle = 'Start tracking domains to find opportunities'
+ }
+
+ return { availableDomains, totalDomains, tierName, TierIcon, greeting, subtitle }
+ }, [domains, subscription])
+
+ if (isLoading || !isAuthenticated) {
+ return (
+
+ )
+ }
+
+ return (
+
+ {toast && }
+
+
+ {/* Quick Add */}
+
+
+
+
+
+
+
+ Quick Add to Watchlist
+
+
+
+
+
+ {/* Stats Overview */}
+
+
+
+
+
+ 0}
+ />
+
+
+
+
+
+
+
+ {/* Activity Feed + Market Pulse */}
+
+ {/* Activity Feed */}
+
+
+
+ View all →
+
+ }
+ />
+
+
+ {availableDomains.length > 0 ? (
+
+ {availableDomains.slice(0, 4).map((domain) => (
+
+
+
+
+
+
+
{domain.name}
+
Available for registration!
+
+
+ Register
+
+
+ ))}
+ {availableDomains.length > 4 && (
+
+ +{availableDomains.length - 4} more available
+
+ )}
+
+ ) : totalDomains > 0 ? (
+
+
+
All domains are still registered
+
+ We're monitoring {totalDomains} domains for you
+
+
+ ) : (
+
+
+
No domains tracked yet
+
+ Add a domain above to start monitoring
+
+
+ )}
+
+
+
+ {/* Market Pulse */}
+
+
+
+ View all →
+
+ }
+ />
+
+
+ {loadingAuctions ? (
+
+ {[...Array(4)].map((_, i) => (
+
+ ))}
+
+ ) : hotAuctions.length > 0 ? (
+
+ ) : (
+
+
+
No auctions ending soon
+
+ )}
+
+
+
+
+ {/* Trending TLDs */}
+
+
+
+ View all →
+
+ }
+ />
+
+
+ {loadingTlds ? (
+
+ {[...Array(4)].map((_, i) => (
+
+ ))}
+
+ ) : trendingTlds.length > 0 ? (
+
+ {trendingTlds.map((tld) => (
+
+
+
+
+ .{tld.tld}
+ 0
+ ? "text-orange-400 bg-orange-400/10 border-orange-400/20"
+ : "text-accent bg-accent/10 border-accent/20"
+ )}>
+ {(tld.price_change || 0) > 0 ? '+' : ''}{(tld.price_change || 0).toFixed(1)}%
+
+
+
{tld.reason}
+
+
+ ))}
+
+ ) : (
+
+
+
No trending TLDs available
+
+ )}
+
+
+
+
+ )
+}
diff --git a/frontend/src/app/command/listings/page.tsx b/frontend/src/app/command/listings/page.tsx
new file mode 100755
index 0000000..0eeb563
--- /dev/null
+++ b/frontend/src/app/command/listings/page.tsx
@@ -0,0 +1,582 @@
+'use client'
+
+import { useEffect, useState, useMemo, useCallback } from 'react'
+import { useSearchParams } from 'next/navigation'
+import { useStore } from '@/lib/store'
+import { api } from '@/lib/api'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import { PageContainer, StatCard, Badge, ActionButton } from '@/components/PremiumTable'
+import {
+ Plus,
+ Shield,
+ Eye,
+ MessageSquare,
+ ExternalLink,
+ Loader2,
+ Trash2,
+ CheckCircle,
+ AlertCircle,
+ Copy,
+ RefreshCw,
+ DollarSign,
+ X,
+ Tag,
+ Store,
+ Sparkles,
+} from 'lucide-react'
+import Link from 'next/link'
+import clsx from 'clsx'
+
+interface Listing {
+ id: number
+ domain: string
+ slug: string
+ title: string | null
+ description: string | null
+ asking_price: number | null
+ min_offer: number | null
+ currency: string
+ price_type: string
+ pounce_score: number | null
+ estimated_value: number | null
+ verification_status: string
+ is_verified: boolean
+ status: string
+ show_valuation: boolean
+ allow_offers: boolean
+ view_count: number
+ inquiry_count: number
+ public_url: string
+ created_at: string
+ published_at: string | null
+}
+
+interface VerificationInfo {
+ verification_code: string
+ dns_record_type: string
+ dns_record_name: string
+ dns_record_value: string
+ instructions: string
+ status: string
+}
+
+export default function MyListingsPage() {
+ const { subscription } = useStore()
+ const searchParams = useSearchParams()
+ const prefillDomain = searchParams.get('domain')
+
+ const [listings, setListings] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ // Modals - auto-open if domain is prefilled
+ const [showCreateModal, setShowCreateModal] = useState(false)
+ const [showVerifyModal, setShowVerifyModal] = useState(false)
+ const [selectedListing, setSelectedListing] = useState(null)
+ const [verificationInfo, setVerificationInfo] = useState(null)
+ const [verifying, setVerifying] = useState(false)
+ const [creating, setCreating] = useState(false)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(null)
+
+ // Create form
+ const [newListing, setNewListing] = useState({
+ domain: '',
+ title: '',
+ description: '',
+ asking_price: '',
+ price_type: 'negotiable',
+ allow_offers: true,
+ })
+
+ const loadListings = useCallback(async () => {
+ setLoading(true)
+ try {
+ const data = await api.request('/listings/my')
+ setListings(data)
+ } catch (err: any) {
+ console.error('Failed to load listings:', err)
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ loadListings()
+ }, [loadListings])
+
+ // Auto-open create modal if domain is prefilled from portfolio
+ useEffect(() => {
+ if (prefillDomain) {
+ setNewListing(prev => ({ ...prev, domain: prefillDomain }))
+ setShowCreateModal(true)
+ }
+ }, [prefillDomain])
+
+ const handleCreate = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setCreating(true)
+ setError(null)
+
+ try {
+ await api.request('/listings', {
+ method: 'POST',
+ body: JSON.stringify({
+ domain: newListing.domain,
+ title: newListing.title || null,
+ description: newListing.description || null,
+ asking_price: newListing.asking_price ? parseFloat(newListing.asking_price) : null,
+ price_type: newListing.price_type,
+ allow_offers: newListing.allow_offers,
+ }),
+ })
+ setSuccess('Listing created! Now verify ownership to publish.')
+ setShowCreateModal(false)
+ setNewListing({ domain: '', title: '', description: '', asking_price: '', price_type: 'negotiable', allow_offers: true })
+ loadListings()
+ } catch (err: any) {
+ setError(err.message)
+ } finally {
+ setCreating(false)
+ }
+ }
+
+ const handleStartVerification = async (listing: Listing) => {
+ setSelectedListing(listing)
+ setVerifying(true)
+
+ try {
+ const info = await api.request(`/listings/${listing.id}/verify-dns`, {
+ method: 'POST',
+ })
+ setVerificationInfo(info)
+ setShowVerifyModal(true)
+ } catch (err: any) {
+ setError(err.message)
+ } finally {
+ setVerifying(false)
+ }
+ }
+
+ const handleCheckVerification = async () => {
+ if (!selectedListing) return
+ setVerifying(true)
+
+ try {
+ const result = await api.request<{ verified: boolean; message: string }>(
+ `/listings/${selectedListing.id}/verify-dns/check`
+ )
+
+ if (result.verified) {
+ setSuccess('Domain verified! You can now publish your listing.')
+ setShowVerifyModal(false)
+ loadListings()
+ } else {
+ setError(result.message)
+ }
+ } catch (err: any) {
+ setError(err.message)
+ } finally {
+ setVerifying(false)
+ }
+ }
+
+ const handlePublish = async (listing: Listing) => {
+ try {
+ await api.request(`/listings/${listing.id}`, {
+ method: 'PUT',
+ body: JSON.stringify({ status: 'active' }),
+ })
+ setSuccess('Listing published!')
+ loadListings()
+ } catch (err: any) {
+ setError(err.message)
+ }
+ }
+
+ const handleDelete = async (listing: Listing) => {
+ if (!confirm(`Delete listing for ${listing.domain}?`)) return
+
+ try {
+ await api.request(`/listings/${listing.id}`, { method: 'DELETE' })
+ setSuccess('Listing deleted')
+ loadListings()
+ } catch (err: any) {
+ setError(err.message)
+ }
+ }
+
+ const copyToClipboard = (text: string) => {
+ navigator.clipboard.writeText(text)
+ setSuccess('Copied to clipboard!')
+ }
+
+ const formatPrice = (price: number | null, currency: string) => {
+ if (!price) return 'Make Offer'
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency,
+ minimumFractionDigits: 0,
+ }).format(price)
+ }
+
+ const getStatusBadge = (status: string, isVerified: boolean) => {
+ if (status === 'active') return Live
+ if (status === 'draft' && !isVerified) return Needs Verification
+ if (status === 'draft') return Draft
+ if (status === 'sold') return Sold
+ return {status}
+ }
+
+ const tier = subscription?.tier || 'scout'
+ const limits = { scout: 2, trader: 10, tycoon: 50 }
+ const maxListings = limits[tier as keyof typeof limits] || 2
+
+ return (
+
+
+
+ Browse Marketplace
+
+ setShowCreateModal(true)}
+ disabled={listings.length >= maxListings}
+ className="flex items-center gap-2 px-4 py-2 bg-accent text-background text-sm font-medium rounded-lg
+ hover:bg-accent-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+
+ List Domain
+
+
+ }
+ >
+
+ {/* Messages */}
+ {error && (
+
+
+
{error}
+
setError(null)}>
+
+ )}
+
+ {success && (
+
+
+
{success}
+
setSuccess(null)}>
+
+ )}
+
+ {/* Stats */}
+
+
+ l.status === 'active').length}
+ icon={CheckCircle}
+ accent
+ />
+ sum + l.view_count, 0)}
+ icon={Eye}
+ />
+ sum + l.inquiry_count, 0)}
+ icon={MessageSquare}
+ />
+
+
+ {/* Listings */}
+ {loading ? (
+
+
+
+ ) : listings.length === 0 ? (
+
+
+
No Listings Yet
+
+ Create your first listing to sell a domain on the Pounce marketplace.
+
+
setShowCreateModal(true)}
+ className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
+ >
+
+ Create Listing
+
+
+ ) : (
+
+ {listings.map((listing) => (
+
+
+ {/* Domain Info */}
+
+
+
{listing.domain}
+ {getStatusBadge(listing.status, listing.is_verified)}
+ {listing.is_verified && (
+
+
+
+ )}
+
+ {listing.title && (
+
{listing.title}
+ )}
+
+
+ {/* Price */}
+
+
+ {formatPrice(listing.asking_price, listing.currency)}
+
+ {listing.pounce_score && (
+
Score: {listing.pounce_score}
+ )}
+
+
+ {/* Stats */}
+
+
+ {listing.view_count}
+
+
+ {listing.inquiry_count}
+
+
+
+ {/* Actions */}
+
+ {!listing.is_verified && (
+ handleStartVerification(listing)}
+ disabled={verifying}
+ className="flex items-center gap-1.5 px-3 py-2 bg-amber-500/10 text-amber-400 text-sm font-medium rounded-lg hover:bg-amber-500/20 transition-all"
+ >
+
+ Verify
+
+ )}
+
+ {listing.is_verified && listing.status === 'draft' && (
+ handlePublish(listing)}
+ className="flex items-center gap-1.5 px-3 py-2 bg-accent text-background text-sm font-medium rounded-lg hover:bg-accent-hover transition-all"
+ >
+
+ Publish
+
+ )}
+
+ {listing.status === 'active' && (
+
+
+ View
+
+ )}
+
+ handleDelete(listing)}
+ className="p-2 text-foreground-subtle hover:text-red-400 transition-colors"
+ >
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Create Modal */}
+ {showCreateModal && (
+
+
+
List Domain for Sale
+
+
+
+
+ )}
+
+ {/* Verification Modal */}
+ {showVerifyModal && verificationInfo && selectedListing && (
+
+
+
Verify Domain Ownership
+
+ Add a DNS TXT record to prove you own {selectedListing.domain}
+
+
+
+
+
Record Type
+
{verificationInfo.dns_record_type}
+
+
+
+
Name / Host
+
+
{verificationInfo.dns_record_name}
+
copyToClipboard(verificationInfo.dns_record_name)}
+ className="p-2 text-foreground-subtle hover:text-accent transition-colors"
+ >
+
+
+
+
+
+
+
Value
+
+
{verificationInfo.dns_record_value}
+
copyToClipboard(verificationInfo.dns_record_value)}
+ className="p-2 text-foreground-subtle hover:text-accent transition-colors shrink-0"
+ >
+
+
+
+
+
+
+
+ {verificationInfo.instructions}
+
+
+
+
+
+ setShowVerifyModal(false)}
+ className="flex-1 px-4 py-3 border border-border text-foreground-muted rounded-xl hover:bg-foreground/5 transition-all"
+ >
+ Close
+
+
+ {verifying ? : }
+ {verifying ? 'Checking...' : 'Check Verification'}
+
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/app/command/marketplace/page.tsx b/frontend/src/app/command/marketplace/page.tsx
new file mode 100755
index 0000000..97af871
--- /dev/null
+++ b/frontend/src/app/command/marketplace/page.tsx
@@ -0,0 +1,302 @@
+'use client'
+
+import { useEffect, useState, useMemo, useCallback } from 'react'
+import { api } from '@/lib/api'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import {
+ PageContainer,
+ StatCard,
+ Badge,
+ SearchInput,
+ FilterBar,
+ SelectDropdown,
+ ActionButton,
+} from '@/components/PremiumTable'
+import {
+ Search,
+ Shield,
+ Loader2,
+ ExternalLink,
+ Store,
+ Tag,
+ DollarSign,
+ Filter,
+} from 'lucide-react'
+import Link from 'next/link'
+import clsx from 'clsx'
+
+interface Listing {
+ domain: string
+ slug: string
+ title: string | null
+ description: string | null
+ asking_price: number | null
+ currency: string
+ price_type: string
+ pounce_score: number | null
+ estimated_value: number | null
+ is_verified: boolean
+ allow_offers: boolean
+ public_url: string
+ seller_verified: boolean
+}
+
+type SortOption = 'newest' | 'price_asc' | 'price_desc' | 'score'
+
+export default function CommandMarketplacePage() {
+ const [listings, setListings] = useState
([])
+ const [loading, setLoading] = useState(true)
+ const [searchQuery, setSearchQuery] = useState('')
+ const [minPrice, setMinPrice] = useState('')
+ const [maxPrice, setMaxPrice] = useState('')
+ const [verifiedOnly, setVerifiedOnly] = useState(false)
+ const [sortBy, setSortBy] = useState('newest')
+ const [showFilters, setShowFilters] = useState(false)
+
+ const loadListings = useCallback(async () => {
+ setLoading(true)
+ try {
+ const params = new URLSearchParams()
+ params.set('limit', '100')
+ if (sortBy === 'price_asc') params.set('sort', 'price_asc')
+ if (sortBy === 'price_desc') params.set('sort', 'price_desc')
+ if (verifiedOnly) params.set('verified_only', 'true')
+
+ const data = await api.request(`/listings?${params.toString()}`)
+ setListings(data)
+ } catch (err) {
+ console.error('Failed to load listings:', err)
+ } finally {
+ setLoading(false)
+ }
+ }, [sortBy, verifiedOnly])
+
+ useEffect(() => {
+ loadListings()
+ }, [loadListings])
+
+ const formatPrice = (price: number | null, currency: string) => {
+ if (!price) return 'Make Offer'
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency,
+ minimumFractionDigits: 0,
+ }).format(price)
+ }
+
+ // Memoized filtered and sorted listings
+ const sortedListings = useMemo(() => {
+ let result = listings.filter(listing => {
+ if (searchQuery && !listing.domain.toLowerCase().includes(searchQuery.toLowerCase())) return false
+ if (minPrice && listing.asking_price && listing.asking_price < parseFloat(minPrice)) return false
+ if (maxPrice && listing.asking_price && listing.asking_price > parseFloat(maxPrice)) return false
+ return true
+ })
+
+ return result.sort((a, b) => {
+ switch (sortBy) {
+ case 'price_asc': return (a.asking_price || 0) - (b.asking_price || 0)
+ case 'price_desc': return (b.asking_price || 0) - (a.asking_price || 0)
+ case 'score': return (b.pounce_score || 0) - (a.pounce_score || 0)
+ default: return 0
+ }
+ })
+ }, [listings, searchQuery, minPrice, maxPrice, sortBy])
+
+ // Memoized stats
+ const stats = useMemo(() => {
+ const verifiedCount = listings.filter(l => l.is_verified).length
+ const pricesWithValue = listings.filter(l => l.asking_price)
+ const avgPrice = pricesWithValue.length > 0
+ ? pricesWithValue.reduce((sum, l) => sum + (l.asking_price || 0), 0) / pricesWithValue.length
+ : 0
+ return { verifiedCount, avgPrice }
+ }, [listings])
+
+ return (
+
+ My Listings
+
+ }
+ >
+
+ {/* Stats */}
+
+
+
+ 0 ? `$${Math.round(stats.avgPrice).toLocaleString()}` : '—'}
+ icon={DollarSign}
+ />
+
+
+
+ {/* Search & Filters */}
+
+
+ {/* Search */}
+
+
+ setSearchQuery(e.target.value)}
+ className="w-full pl-12 pr-4 py-3 bg-background border border-border rounded-xl
+ text-foreground placeholder:text-foreground-subtle
+ focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent"
+ />
+
+
+ {/* Sort */}
+
setSortBy(e.target.value as SortOption)}
+ className="px-4 py-3 bg-background border border-border rounded-xl text-foreground
+ focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent"
+ >
+ Newest First
+ Price: Low to High
+ Price: High to Low
+ Pounce Score
+
+
+ {/* Filter Toggle */}
+
setShowFilters(!showFilters)}
+ className={clsx(
+ "flex items-center gap-2 px-4 py-3 border rounded-xl transition-all",
+ showFilters
+ ? "bg-accent/10 border-accent/30 text-accent"
+ : "bg-background border-border text-foreground-muted hover:text-foreground"
+ )}
+ >
+
+ Filters
+
+
+
+ {/* Expanded Filters */}
+ {showFilters && (
+
+ )}
+
+
+ {/* Listings Grid */}
+ {loading ? (
+
+
+
+ ) : sortedListings.length === 0 ? (
+
+
+
No Domains Found
+
+ {searchQuery || minPrice || maxPrice
+ ? 'Try adjusting your filters'
+ : 'No domains are currently listed for sale'}
+
+
+
+ List Your Domain
+
+
+ ) : (
+
+ {sortedListings.map((listing) => (
+
+
+
+
+ {listing.domain}
+
+ {listing.title && (
+
{listing.title}
+ )}
+
+ {listing.is_verified && (
+
+
+
+ )}
+
+
+ {listing.description && (
+
+ {listing.description}
+
+ )}
+
+
+
+ {listing.pounce_score && (
+
+ {listing.pounce_score}
+
+ )}
+ {listing.allow_offers && (
+
Offers
+ )}
+
+
+
+ {formatPrice(listing.asking_price, listing.currency)}
+
+ {listing.price_type === 'negotiable' && (
+
Negotiable
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+ )
+}
+
diff --git a/frontend/src/app/command/page.tsx b/frontend/src/app/command/page.tsx
new file mode 100755
index 0000000..a4b01b0
--- /dev/null
+++ b/frontend/src/app/command/page.tsx
@@ -0,0 +1,19 @@
+'use client'
+
+import { useEffect } from 'react'
+import { useRouter } from 'next/navigation'
+
+export default function CommandPage() {
+ const router = useRouter()
+
+ useEffect(() => {
+ router.replace('/command/dashboard')
+ }, [router])
+
+ return (
+
+ )
+}
+
diff --git a/frontend/src/app/command/portfolio/page.tsx b/frontend/src/app/command/portfolio/page.tsx
new file mode 100755
index 0000000..8b6db85
--- /dev/null
+++ b/frontend/src/app/command/portfolio/page.tsx
@@ -0,0 +1,955 @@
+'use client'
+
+import { useEffect, useState, useMemo, useCallback, memo } from 'react'
+import { useStore } from '@/lib/store'
+import { api, PortfolioDomain, PortfolioSummary, DomainValuation, DomainHealthReport, HealthStatus } from '@/lib/api'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import { PremiumTable, StatCard, PageContainer, ActionButton } from '@/components/PremiumTable'
+import { Toast, useToast } from '@/components/Toast'
+import {
+ Plus,
+ Trash2,
+ Edit2,
+ DollarSign,
+ Calendar,
+ Building,
+ Loader2,
+ ArrowUpRight,
+ X,
+ Briefcase,
+ ShoppingCart,
+ Activity,
+ Shield,
+ AlertTriangle,
+ Tag,
+ MoreVertical,
+ ExternalLink,
+} from 'lucide-react'
+import clsx from 'clsx'
+import Link from 'next/link'
+
+// Health status configuration
+const healthStatusConfig: Record = {
+ healthy: { label: 'Healthy', color: 'text-accent', bgColor: 'bg-accent/10', icon: Activity },
+ weakening: { label: 'Weak', color: 'text-amber-400', bgColor: 'bg-amber-400/10', icon: AlertTriangle },
+ parked: { label: 'Parked', color: 'text-orange-400', bgColor: 'bg-orange-400/10', icon: ShoppingCart },
+ critical: { label: 'Critical', color: 'text-red-400', bgColor: 'bg-red-400/10', icon: AlertTriangle },
+ unknown: { label: 'Unknown', color: 'text-foreground-muted', bgColor: 'bg-foreground/5', icon: Activity },
+}
+
+export default function PortfolioPage() {
+ const { subscription } = useStore()
+ const { toast, showToast, hideToast } = useToast()
+
+ const [portfolio, setPortfolio] = useState([])
+ const [summary, setSummary] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [showAddModal, setShowAddModal] = useState(false)
+ const [showEditModal, setShowEditModal] = useState(false)
+ const [showSellModal, setShowSellModal] = useState(false)
+ const [showValuationModal, setShowValuationModal] = useState(false)
+ const [selectedDomain, setSelectedDomain] = useState(null)
+ const [valuation, setValuation] = useState(null)
+ const [valuatingDomain, setValuatingDomain] = useState('')
+ const [addingDomain, setAddingDomain] = useState(false)
+ const [savingEdit, setSavingEdit] = useState(false)
+ const [processingSale, setProcessingSale] = useState(false)
+ const [refreshingId, setRefreshingId] = useState(null)
+
+ // Health monitoring state
+ const [healthReports, setHealthReports] = useState>({})
+ const [loadingHealth, setLoadingHealth] = useState>({})
+ const [selectedHealthDomain, setSelectedHealthDomain] = useState(null)
+
+ // Dropdown menu state
+ const [openMenuId, setOpenMenuId] = useState(null)
+
+ const [addForm, setAddForm] = useState({
+ domain: '',
+ purchase_price: '',
+ purchase_date: '',
+ registrar: '',
+ renewal_date: '',
+ renewal_cost: '',
+ notes: '',
+ })
+
+ const [editForm, setEditForm] = useState({
+ purchase_price: '',
+ purchase_date: '',
+ registrar: '',
+ renewal_date: '',
+ renewal_cost: '',
+ notes: '',
+ })
+
+ const [sellForm, setSellForm] = useState({
+ sale_date: new Date().toISOString().split('T')[0],
+ sale_price: '',
+ })
+
+ const loadPortfolio = useCallback(async () => {
+ setLoading(true)
+ try {
+ const [portfolioData, summaryData] = await Promise.all([
+ api.getPortfolio(),
+ api.getPortfolioSummary(),
+ ])
+ setPortfolio(portfolioData)
+ setSummary(summaryData)
+ } catch (error) {
+ console.error('Failed to load portfolio:', error)
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ loadPortfolio()
+ }, [loadPortfolio])
+
+ const handleAddDomain = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!addForm.domain.trim()) return
+
+ setAddingDomain(true)
+ try {
+ await api.addPortfolioDomain({
+ domain: addForm.domain.trim(),
+ purchase_price: addForm.purchase_price ? parseFloat(addForm.purchase_price) : undefined,
+ purchase_date: addForm.purchase_date || undefined,
+ registrar: addForm.registrar || undefined,
+ renewal_date: addForm.renewal_date || undefined,
+ renewal_cost: addForm.renewal_cost ? parseFloat(addForm.renewal_cost) : undefined,
+ notes: addForm.notes || undefined,
+ })
+ showToast(`Added ${addForm.domain} to portfolio`, 'success')
+ setAddForm({ domain: '', purchase_price: '', purchase_date: '', registrar: '', renewal_date: '', renewal_cost: '', notes: '' })
+ setShowAddModal(false)
+ loadPortfolio()
+ } catch (err: any) {
+ showToast(err.message || 'Failed to add domain', 'error')
+ } finally {
+ setAddingDomain(false)
+ }
+ }, [addForm, loadPortfolio, showToast])
+
+ const handleEditDomain = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!selectedDomain) return
+
+ setSavingEdit(true)
+ try {
+ await api.updatePortfolioDomain(selectedDomain.id, {
+ purchase_price: editForm.purchase_price ? parseFloat(editForm.purchase_price) : undefined,
+ purchase_date: editForm.purchase_date || undefined,
+ registrar: editForm.registrar || undefined,
+ renewal_date: editForm.renewal_date || undefined,
+ renewal_cost: editForm.renewal_cost ? parseFloat(editForm.renewal_cost) : undefined,
+ notes: editForm.notes || undefined,
+ })
+ showToast('Domain updated', 'success')
+ setShowEditModal(false)
+ loadPortfolio()
+ } catch (err: any) {
+ showToast(err.message || 'Failed to update', 'error')
+ } finally {
+ setSavingEdit(false)
+ }
+ }, [selectedDomain, editForm, loadPortfolio, showToast])
+
+ const handleSellDomain = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!selectedDomain || !sellForm.sale_price) return
+
+ setProcessingSale(true)
+ try {
+ await api.markDomainSold(selectedDomain.id, sellForm.sale_date, parseFloat(sellForm.sale_price))
+ showToast(`Marked ${selectedDomain.domain} as sold`, 'success')
+ setShowSellModal(false)
+ loadPortfolio()
+ } catch (err: any) {
+ showToast(err.message || 'Failed to process sale', 'error')
+ } finally {
+ setProcessingSale(false)
+ }
+ }, [selectedDomain, sellForm, loadPortfolio, showToast])
+
+ const handleValuate = useCallback(async (domain: PortfolioDomain) => {
+ setValuatingDomain(domain.domain)
+ setShowValuationModal(true)
+ try {
+ const result = await api.getDomainValuation(domain.domain)
+ setValuation(result)
+ } catch (err: any) {
+ showToast(err.message || 'Failed to get valuation', 'error')
+ setShowValuationModal(false)
+ } finally {
+ setValuatingDomain('')
+ }
+ }, [showToast])
+
+ const handleRefresh = useCallback(async (domain: PortfolioDomain) => {
+ setRefreshingId(domain.id)
+ try {
+ await api.refreshDomainValue(domain.id)
+ showToast('Valuation refreshed', 'success')
+ loadPortfolio()
+ } catch (err: any) {
+ showToast(err.message || 'Failed to refresh', 'error')
+ } finally {
+ setRefreshingId(null)
+ }
+ }, [loadPortfolio, showToast])
+
+ const handleHealthCheck = useCallback(async (domainName: string) => {
+ if (loadingHealth[domainName]) return
+
+ setLoadingHealth(prev => ({ ...prev, [domainName]: true }))
+ try {
+ const report = await api.quickHealthCheck(domainName)
+ setHealthReports(prev => ({ ...prev, [domainName]: report }))
+ setSelectedHealthDomain(domainName)
+ } catch (err: any) {
+ showToast(err.message || 'Health check failed', 'error')
+ } finally {
+ setLoadingHealth(prev => ({ ...prev, [domainName]: false }))
+ }
+ }, [loadingHealth, showToast])
+
+ const handleDelete = useCallback(async (domain: PortfolioDomain) => {
+ if (!confirm(`Remove ${domain.domain} from your portfolio?`)) return
+
+ try {
+ await api.deletePortfolioDomain(domain.id)
+ showToast(`Removed ${domain.domain}`, 'success')
+ loadPortfolio()
+ } catch (err: any) {
+ showToast(err.message || 'Failed to remove', 'error')
+ }
+ }, [loadPortfolio, showToast])
+
+ const openEditModal = useCallback((domain: PortfolioDomain) => {
+ setSelectedDomain(domain)
+ setEditForm({
+ purchase_price: domain.purchase_price?.toString() || '',
+ purchase_date: domain.purchase_date || '',
+ registrar: domain.registrar || '',
+ renewal_date: domain.renewal_date || '',
+ renewal_cost: domain.renewal_cost?.toString() || '',
+ notes: domain.notes || '',
+ })
+ setShowEditModal(true)
+ }, [])
+
+ const openSellModal = useCallback((domain: PortfolioDomain) => {
+ setSelectedDomain(domain)
+ setSellForm({
+ sale_date: new Date().toISOString().split('T')[0],
+ sale_price: '',
+ })
+ setShowSellModal(true)
+ }, [])
+
+ const portfolioLimit = subscription?.portfolio_limit || 0
+ const canAddMore = portfolioLimit === -1 || portfolio.length < portfolioLimit
+
+ // Memoized stats and subtitle
+ const { expiringSoonCount, subtitle } = useMemo(() => {
+ const expiring = portfolio.filter(d => {
+ if (!d.renewal_date) return false
+ const days = Math.ceil((new Date(d.renewal_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
+ return days <= 30 && days > 0
+ }).length
+
+ let sub = ''
+ if (loading) sub = 'Loading your portfolio...'
+ else if (portfolio.length === 0) sub = 'Start tracking your domains'
+ else if (expiring > 0) sub = `${portfolio.length} domain${portfolio.length !== 1 ? 's' : ''} • ${expiring} expiring soon`
+ else sub = `Managing ${portfolio.length} domain${portfolio.length !== 1 ? 's' : ''}`
+
+ return { expiringSoonCount: expiring, subtitle: sub }
+ }, [portfolio, loading])
+
+ return (
+ setShowAddModal(true)} disabled={!canAddMore} icon={Plus}>
+ Add Domain
+
+ }
+ >
+ {toast && }
+
+
+ {/* Summary Stats - Only reliable data */}
+
+
+
+ r.status !== 'healthy').length}
+ icon={AlertTriangle}
+ />
+
+
+
+ {!canAddMore && (
+
+
+ You've reached your portfolio limit. Upgrade to add more.
+
+
+ Upgrade
+
+
+ )}
+
+ {/* Portfolio Table */}
+ d.id}
+ loading={loading}
+ emptyIcon={ }
+ emptyTitle="Your portfolio is empty"
+ emptyDescription="Add your first domain to start tracking investments"
+ columns={[
+ {
+ key: 'domain',
+ header: 'Domain',
+ render: (domain) => (
+
+
{domain.domain}
+ {domain.registrar && (
+
+ {domain.registrar}
+
+ )}
+
+ ),
+ },
+ {
+ key: 'added',
+ header: 'Added',
+ hideOnMobile: true,
+ hideOnTablet: true,
+ render: (domain) => (
+
+ {domain.purchase_date
+ ? new Date(domain.purchase_date).toLocaleDateString()
+ : new Date(domain.created_at).toLocaleDateString()
+ }
+
+ ),
+ },
+ {
+ key: 'renewal',
+ header: 'Expires',
+ hideOnMobile: true,
+ render: (domain) => {
+ if (!domain.renewal_date) {
+ return —
+ }
+ const days = Math.ceil((new Date(domain.renewal_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
+ const isExpiringSoon = days <= 30 && days > 0
+ const isExpired = days <= 0
+ return (
+
+
+ {new Date(domain.renewal_date).toLocaleDateString()}
+
+ {isExpiringSoon && (
+
+ {days}d
+
+ )}
+ {isExpired && (
+
+ EXPIRED
+
+ )}
+
+ )
+ },
+ },
+ {
+ key: 'health',
+ header: 'Health',
+ hideOnMobile: true,
+ render: (domain) => {
+ const report = healthReports[domain.domain]
+ if (loadingHealth[domain.domain]) {
+ return
+ }
+ if (report) {
+ const config = healthStatusConfig[report.status]
+ const Icon = config.icon
+ return (
+ setSelectedHealthDomain(domain.domain)}
+ className={clsx(
+ "inline-flex items-center gap-1.5 px-2 py-1 rounded-lg text-xs font-medium",
+ config.bgColor, config.color
+ )}
+ >
+
+ {config.label}
+
+ )
+ }
+ return (
+ handleHealthCheck(domain.domain)}
+ className="text-xs text-foreground-muted hover:text-accent transition-colors flex items-center gap-1"
+ >
+
+ Check
+
+ )
+ },
+ },
+ {
+ key: 'actions',
+ header: '',
+ align: 'right',
+ render: (domain) => (
+
+
{
+ e.stopPropagation()
+ setOpenMenuId(openMenuId === domain.id ? null : domain.id)
+ }}
+ className="p-2 text-foreground-muted hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
+ >
+
+
+
+ {openMenuId === domain.id && (
+ <>
+ {/* Backdrop */}
+
setOpenMenuId(null)}
+ />
+ {/* Menu - opens downward */}
+
+
{ handleHealthCheck(domain.domain); setOpenMenuId(null) }}
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 transition-colors"
+ >
+
+ Health Check
+
+
{ openEditModal(domain); setOpenMenuId(null) }}
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 transition-colors"
+ >
+
+ Edit Details
+
+
+
setOpenMenuId(null)}
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-accent hover:bg-accent/5 transition-colors"
+ >
+
+ List for Sale
+
+
setOpenMenuId(null)}
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 transition-colors"
+ >
+
+ Visit Website
+
+
+
{ openSellModal(domain); setOpenMenuId(null) }}
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 transition-colors"
+ >
+
+ Record Sale
+
+
{ handleDelete(domain); setOpenMenuId(null) }}
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-red-400 hover:bg-red-400/5 transition-colors"
+ >
+
+ Remove
+
+
+ >
+ )}
+
+ ),
+ },
+ ]}
+ />
+
+
+ {/* Add Modal */}
+ {showAddModal && (
+
setShowAddModal(false)}>
+
+
+ Domain *
+ setAddForm({ ...addForm, domain: e.target.value })}
+ placeholder="example.com"
+ className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
+ focus:outline-none focus:border-accent/50 transition-all"
+ required
+ />
+
+
+
+ Registrar
+ setAddForm({ ...addForm, registrar: e.target.value })}
+ placeholder="Namecheap"
+ className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
+ focus:outline-none focus:border-accent/50"
+ />
+
+
+ setShowAddModal(false)}
+ className="px-4 py-2.5 text-foreground-muted hover:text-foreground transition-colors"
+ >
+ Cancel
+
+
+ {addingDomain && }
+ Add Domain
+
+
+
+
+ )}
+
+ {/* Edit Modal */}
+ {showEditModal && selectedDomain && (
+
setShowEditModal(false)}>
+
+
+
+ setShowEditModal(false)}
+ className="px-4 py-2.5 text-foreground-muted hover:text-foreground transition-colors"
+ >
+ Cancel
+
+
+ {savingEdit && }
+ Save Changes
+
+
+
+
+ )}
+
+ {/* Record Sale Modal - for tracking completed sales */}
+ {showSellModal && selectedDomain && (
+
setShowSellModal(false)}>
+
+
+
Record a completed sale to track your profit/loss. This will mark the domain as sold in your portfolio.
+
Want to list it for sale instead? Use the "List" button.
+
+
+
+ setShowSellModal(false)}
+ className="px-4 py-2.5 text-foreground-muted hover:text-foreground transition-colors"
+ >
+ Cancel
+
+
+ {processingSale && }
+ Mark as Sold
+
+
+
+
+ )}
+
+ {/* Valuation Modal */}
+ {showValuationModal && (
+
{ setShowValuationModal(false); setValuation(null); }}>
+ {valuatingDomain ? (
+
+
+
+ ) : valuation ? (
+
+
+
${valuation.estimated_value.toLocaleString()}
+
Pounce Score Estimate
+
+
+
+ Confidence Level
+
+ {valuation.confidence}
+
+
+
+
Valuation Formula
+
{valuation.valuation_formula}
+
+
+
This is an algorithmic estimate based on domain length, TLD, and market patterns. Actual market value may vary.
+
+
+
+ ) : null}
+
+ )}
+
+ {/* Health Report Modal */}
+ {selectedHealthDomain && healthReports[selectedHealthDomain] && (
+
setSelectedHealthDomain(null)}
+ />
+ )}
+
+ )
+}
+
+// Health Report Modal Component
+function HealthReportModal({ report, onClose }: { report: DomainHealthReport; onClose: () => void }) {
+ const config = healthStatusConfig[report.status]
+ const Icon = config.icon
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+
+
+
+
+
{report.domain}
+
{config.label}
+
+
+
+
+
+
+
+ {/* Score */}
+
+
+
Health Score
+
+
+
= 70 ? "bg-accent" :
+ report.score >= 40 ? "bg-amber-400" : "bg-red-400"
+ )}
+ style={{ width: `${report.score}%` }}
+ />
+
+
= 70 ? "text-accent" :
+ report.score >= 40 ? "text-amber-400" : "text-red-400"
+ )}>
+ {report.score}/100
+
+
+
+
+
+ {/* Check Results */}
+
+ {/* DNS */}
+ {report.dns && (
+
+
+
+ DNS Infrastructure
+
+
+
+
+ {report.dns.has_ns ? '✓' : '✗'}
+
+ Nameservers
+
+
+
+ {report.dns.has_a ? '✓' : '✗'}
+
+ A Record
+
+
+
+ {report.dns.has_mx ? '✓' : '—'}
+
+ MX Record
+
+
+ {report.dns.is_parked && (
+
⚠ Parked at {report.dns.parking_provider || 'unknown provider'}
+ )}
+
+ )}
+
+ {/* HTTP */}
+ {report.http && (
+
+
+
+ Website Status
+
+
+
+ {report.http.is_reachable ? 'Reachable' : 'Unreachable'}
+
+ {report.http.status_code && (
+
+ HTTP {report.http.status_code}
+
+ )}
+
+ {report.http.is_parked && (
+
⚠ Parking page detected
+ )}
+
+ )}
+
+ {/* SSL */}
+ {report.ssl && (
+
+
+
+ SSL Certificate
+
+
+ {report.ssl.has_certificate ? (
+
+
+ {report.ssl.is_valid ? '✓ Valid certificate' : '✗ Certificate invalid/expired'}
+
+ {report.ssl.days_until_expiry !== undefined && (
+
30 ? "text-foreground-muted" :
+ report.ssl.days_until_expiry > 7 ? "text-amber-400" : "text-red-400"
+ )}>
+ Expires in {report.ssl.days_until_expiry} days
+
+ )}
+
+ ) : (
+
No SSL certificate
+ )}
+
+
+ )}
+
+ {/* Signals & Recommendations */}
+ {((report.signals?.length || 0) > 0 || (report.recommendations?.length || 0) > 0) && (
+
+ {(report.signals?.length || 0) > 0 && (
+
+
Signals
+
+ {report.signals?.map((signal, i) => (
+
+ •
+ {signal}
+
+ ))}
+
+
+ )}
+ {(report.recommendations?.length || 0) > 0 && (
+
+
Recommendations
+
+ {report.recommendations?.map((rec, i) => (
+
+ →
+ {rec}
+
+ ))}
+
+
+ )}
+
+ )}
+
+
+ {/* Footer */}
+
+
+ Checked at {new Date(report.checked_at).toLocaleString()}
+
+
+
+
+ )
+}
+
+// Modal Component
+function Modal({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) {
+ return (
+
+
e.stopPropagation()}
+ >
+
+
{title}
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/frontend/src/app/command/pricing/[tld]/page.tsx b/frontend/src/app/command/pricing/[tld]/page.tsx
new file mode 100755
index 0000000..2c1104e
--- /dev/null
+++ b/frontend/src/app/command/pricing/[tld]/page.tsx
@@ -0,0 +1,722 @@
+'use client'
+
+import { useEffect, useState, useMemo, useRef } from 'react'
+import { useParams } from 'next/navigation'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import { PageContainer, StatCard } from '@/components/PremiumTable'
+import { useStore } from '@/lib/store'
+import { api } from '@/lib/api'
+import {
+ ArrowLeft,
+ TrendingUp,
+ TrendingDown,
+ Minus,
+ Calendar,
+ Globe,
+ Building,
+ ExternalLink,
+ Search,
+ ChevronRight,
+ Check,
+ X,
+ RefreshCw,
+ AlertTriangle,
+ DollarSign,
+ BarChart3,
+ Shield,
+} from 'lucide-react'
+import Link from 'next/link'
+import clsx from 'clsx'
+
+interface TldDetails {
+ tld: string
+ type: string
+ description: string
+ registry: string
+ introduced: number
+ trend: string
+ trend_reason: string
+ pricing: {
+ avg: number
+ min: number
+ max: number
+ }
+ registrars: Array<{
+ name: string
+ registration_price: number
+ renewal_price: number
+ transfer_price: number
+ }>
+ cheapest_registrar: string
+ // New fields from table
+ min_renewal_price: number
+ price_change_1y: number
+ price_change_3y: number
+ risk_level: 'low' | 'medium' | 'high'
+ risk_reason: string
+}
+
+interface TldHistory {
+ tld: string
+ current_price: number
+ price_change_7d: number
+ price_change_30d: number
+ price_change_90d: number
+ trend: string
+ trend_reason: string
+ history: Array<{
+ date: string
+ price: number
+ }>
+}
+
+interface DomainCheckResult {
+ domain: string
+ is_available: boolean
+ status: string
+ registrar?: string | null
+ creation_date?: string | null
+ expiration_date?: string | null
+}
+
+// Registrar URLs
+const REGISTRAR_URLS: Record
= {
+ 'Namecheap': 'https://www.namecheap.com/domains/registration/results/?domain=',
+ 'Porkbun': 'https://porkbun.com/checkout/search?q=',
+ 'Cloudflare': 'https://www.cloudflare.com/products/registrar/',
+ 'Google Domains': 'https://domains.google.com/registrar/search?searchTerm=',
+ 'GoDaddy': 'https://www.godaddy.com/domainsearch/find?domainToCheck=',
+ 'porkbun': 'https://porkbun.com/checkout/search?q=',
+ 'Dynadot': 'https://www.dynadot.com/domain/search?domain=',
+}
+
+type ChartPeriod = '1M' | '3M' | '1Y' | 'ALL'
+
+// Premium Chart Component with real data
+function PriceChart({
+ data,
+ chartStats,
+}: {
+ data: Array<{ date: string; price: number }>
+ chartStats: { high: number; low: number; avg: number }
+}) {
+ const [hoveredIndex, setHoveredIndex] = useState(null)
+ const containerRef = useRef(null)
+
+ if (data.length === 0) {
+ return (
+
+ No price history available
+
+ )
+ }
+
+ const minPrice = Math.min(...data.map(d => d.price))
+ const maxPrice = Math.max(...data.map(d => d.price))
+ const priceRange = maxPrice - minPrice || 1
+
+ const points = data.map((d, i) => ({
+ x: (i / (data.length - 1)) * 100,
+ y: 100 - ((d.price - minPrice) / priceRange) * 80 - 10,
+ ...d,
+ }))
+
+ const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')
+ const areaPath = linePath + ` L${points[points.length - 1].x},100 L${points[0].x},100 Z`
+
+ const isRising = data[data.length - 1].price > data[0].price
+ const strokeColor = isRising ? '#f97316' : '#00d4aa'
+
+ return (
+ setHoveredIndex(null)}
+ >
+
{
+ if (!containerRef.current) return
+ const rect = containerRef.current.getBoundingClientRect()
+ const x = ((e.clientX - rect.left) / rect.width) * 100
+ const idx = Math.round((x / 100) * (points.length - 1))
+ setHoveredIndex(Math.max(0, Math.min(idx, points.length - 1)))
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ {hoveredIndex !== null && points[hoveredIndex] && (
+
+ )}
+
+
+ {/* Tooltip */}
+ {hoveredIndex !== null && points[hoveredIndex] && (
+
+
${points[hoveredIndex].price.toFixed(2)}
+
{new Date(points[hoveredIndex].date).toLocaleDateString()}
+
+ )}
+
+ {/* Y-axis labels */}
+
+ ${maxPrice.toFixed(2)}
+ ${((maxPrice + minPrice) / 2).toFixed(2)}
+ ${minPrice.toFixed(2)}
+
+
+ )
+}
+
+export default function CommandTldDetailPage() {
+ const params = useParams()
+ const { fetchSubscription } = useStore()
+ const tld = params.tld as string
+
+ const [details, setDetails] = useState(null)
+ const [history, setHistory] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [chartPeriod, setChartPeriod] = useState('1Y')
+ const [domainSearch, setDomainSearch] = useState('')
+ const [checkingDomain, setCheckingDomain] = useState(false)
+ const [domainResult, setDomainResult] = useState(null)
+
+ useEffect(() => {
+ fetchSubscription()
+ if (tld) {
+ loadData()
+ }
+ }, [tld, fetchSubscription])
+
+ const loadData = async () => {
+ try {
+ const [historyData, compareData, overviewData] = await Promise.all([
+ api.getTldHistory(tld, 365),
+ api.getTldCompare(tld),
+ api.getTldOverview(1, 0, 'popularity', tld), // Get the specific TLD data
+ ])
+
+ if (historyData && compareData) {
+ const sortedRegistrars = [...(compareData.registrars || [])].sort((a, b) =>
+ a.registration_price - b.registration_price
+ )
+
+ // Get additional data from overview API (1y, 3y change, risk)
+ const tldFromOverview = overviewData?.tlds?.[0]
+
+ setDetails({
+ tld: compareData.tld || tld,
+ type: compareData.type || 'generic',
+ description: compareData.description || `Domain extension .${tld}`,
+ registry: compareData.registry || 'Various',
+ introduced: compareData.introduced || 0,
+ trend: historyData.trend || 'stable',
+ trend_reason: historyData.trend_reason || 'Price tracking available',
+ pricing: {
+ avg: compareData.price_range?.avg || historyData.current_price || 0,
+ min: compareData.price_range?.min || historyData.current_price || 0,
+ max: compareData.price_range?.max || historyData.current_price || 0,
+ },
+ registrars: sortedRegistrars,
+ cheapest_registrar: compareData.cheapest_registrar || sortedRegistrars[0]?.name || 'N/A',
+ // New fields from overview
+ min_renewal_price: tldFromOverview?.min_renewal_price || sortedRegistrars[0]?.renewal_price || 0,
+ price_change_1y: tldFromOverview?.price_change_1y || 0,
+ price_change_3y: tldFromOverview?.price_change_3y || 0,
+ risk_level: tldFromOverview?.risk_level || 'low',
+ risk_reason: tldFromOverview?.risk_reason || 'Stable',
+ })
+ setHistory(historyData)
+ } else {
+ setError('Failed to load TLD data')
+ }
+ } catch (err) {
+ console.error('Error loading TLD data:', err)
+ setError('Failed to load TLD data')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const filteredHistory = useMemo(() => {
+ if (!history?.history) return []
+
+ const now = new Date()
+ let cutoffDays = 365
+
+ switch (chartPeriod) {
+ case '1M': cutoffDays = 30; break
+ case '3M': cutoffDays = 90; break
+ case '1Y': cutoffDays = 365; break
+ case 'ALL': cutoffDays = 9999; break
+ }
+
+ const cutoff = new Date(now.getTime() - cutoffDays * 24 * 60 * 60 * 1000)
+ return history.history.filter(h => new Date(h.date) >= cutoff)
+ }, [history, chartPeriod])
+
+ const chartStats = useMemo(() => {
+ if (filteredHistory.length === 0) return { high: 0, low: 0, avg: 0 }
+ const prices = filteredHistory.map(h => h.price)
+ return {
+ high: Math.max(...prices),
+ low: Math.min(...prices),
+ avg: prices.reduce((a, b) => a + b, 0) / prices.length,
+ }
+ }, [filteredHistory])
+
+ const handleDomainCheck = async () => {
+ if (!domainSearch.trim()) return
+
+ setCheckingDomain(true)
+ setDomainResult(null)
+
+ try {
+ const domain = domainSearch.includes('.') ? domainSearch : `${domainSearch}.${tld}`
+ const result = await api.checkDomain(domain, false)
+ setDomainResult({
+ domain,
+ is_available: result.is_available,
+ status: result.status,
+ registrar: result.registrar,
+ creation_date: result.creation_date,
+ expiration_date: result.expiration_date,
+ })
+ } catch (err) {
+ console.error('Domain check failed:', err)
+ } finally {
+ setCheckingDomain(false)
+ }
+ }
+
+ const getRegistrarUrl = (registrarName: string, domain?: string) => {
+ const baseUrl = REGISTRAR_URLS[registrarName]
+ if (!baseUrl) return '#'
+ if (domain) return `${baseUrl}${domain}`
+ return baseUrl
+ }
+
+ // Calculate renewal trap info
+ const getRenewalInfo = () => {
+ if (!details?.registrars?.length) return null
+ const cheapest = details.registrars[0]
+ const ratio = cheapest.renewal_price / cheapest.registration_price
+ return {
+ registration: cheapest.registration_price,
+ renewal: cheapest.renewal_price,
+ ratio,
+ isTrap: ratio > 2,
+ }
+ }
+
+ const renewalInfo = getRenewalInfo()
+
+ // Risk badge component
+ const getRiskBadge = () => {
+ if (!details) return null
+ const level = details.risk_level
+ const reason = details.risk_reason
+ return (
+
+
+ {reason}
+
+ )
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+ )
+ }
+
+ if (error || !details) {
+ return (
+
+
+
+
+
+
+
TLD Not Found
+
{error || `The TLD .${tld} could not be found.`}
+
+
+ Back to TLD Pricing
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ {/* Breadcrumb */}
+
+
+ TLD Pricing
+
+
+ .{details.tld}
+
+
+ {/* Stats Grid - All info from table */}
+
+
+
+
+
+
+
+
+ 0 ? '+' : ''}${details.price_change_1y.toFixed(0)}%`}
+ icon={details.price_change_1y > 0 ? TrendingUp : details.price_change_1y < 0 ? TrendingDown : Minus}
+ />
+
+
+ 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%`}
+ icon={BarChart3}
+ />
+
+
+
+ {/* Risk Level */}
+
+
+
+
Risk Assessment
+
Based on renewal ratio, price volatility, and market trends
+
+ {getRiskBadge()}
+
+
+ {/* Renewal Trap Warning */}
+ {renewalInfo?.isTrap && (
+
+
+
+
Renewal Trap Detected
+
+ The renewal price (${renewalInfo.renewal.toFixed(2)}) is {renewalInfo.ratio.toFixed(1)}x higher than the registration price (${renewalInfo.registration.toFixed(2)}).
+ Consider the total cost of ownership before registering.
+
+
+
+ )}
+
+ {/* Price Chart */}
+
+
+
Price History
+
+ {(['1M', '3M', '1Y', 'ALL'] as ChartPeriod[]).map((period) => (
+ setChartPeriod(period)}
+ className={clsx(
+ "px-3 py-1.5 text-xs font-medium rounded-md transition-all",
+ chartPeriod === period
+ ? "bg-accent text-background"
+ : "text-foreground-muted hover:text-foreground"
+ )}
+ >
+ {period}
+
+ ))}
+
+
+
+
+
+ {/* Chart Stats */}
+
+
+
Period High
+
${chartStats.high.toFixed(2)}
+
+
+
Average
+
${chartStats.avg.toFixed(2)}
+
+
+
Period Low
+
${chartStats.low.toFixed(2)}
+
+
+
+
+ {/* Registrar Comparison */}
+
+
Registrar Comparison
+
+
+
+
+
+ Registrar
+ Register
+ Renew
+ Transfer
+
+
+
+
+ {details.registrars.map((registrar, idx) => {
+ const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5
+ const isBestValue = idx === 0 && !hasRenewalTrap
+
+ return (
+
+
+
+ {registrar.name}
+ {isBestValue && (
+
+ Best
+
+ )}
+ {idx === 0 && hasRenewalTrap && (
+
+ Cheap Start
+
+ )}
+
+
+
+
+ ${registrar.registration_price.toFixed(2)}
+
+
+
+
+
+ ${registrar.renewal_price.toFixed(2)}
+
+ {hasRenewalTrap && (
+
+ )}
+
+
+
+
+ ${registrar.transfer_price.toFixed(2)}
+
+
+
+
+ Visit
+
+
+
+
+ )
+ })}
+
+
+
+
+
+ {/* Quick Domain Check */}
+
+
Quick Domain Check
+
+ Check if a domain is available with .{tld}
+
+
+
+
+ setDomainSearch(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && handleDomainCheck()}
+ placeholder={`example or example.${tld}`}
+ className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl
+ text-sm text-foreground placeholder:text-foreground-subtle
+ focus:outline-none focus:border-accent/50 transition-all"
+ />
+
+
+ {checkingDomain ? (
+
+ ) : (
+ 'Check'
+ )}
+
+
+
+ {/* Result */}
+ {domainResult && (
+
+
+ {domainResult.is_available ? (
+
+ ) : (
+
+ )}
+
+
{domainResult.domain}
+
+ {domainResult.is_available ? 'Available for registration!' : 'Already registered'}
+
+
+
+
+ {domainResult.is_available && (
+
+ Register at {details.cheapest_registrar}
+
+
+ )}
+
+ )}
+
+
+ {/* TLD Info */}
+
+
TLD Information
+
+
+
+
+
+ Type
+
+
{details.type}
+
+
+
+
+ Registry
+
+
{details.registry}
+
+
+
+
+ Introduced
+
+
{details.introduced || 'Unknown'}
+
+
+
+
+ Registrars
+
+
{details.registrars.length} tracked
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/app/command/pricing/page.tsx b/frontend/src/app/command/pricing/page.tsx
new file mode 100755
index 0000000..88476d6
--- /dev/null
+++ b/frontend/src/app/command/pricing/page.tsx
@@ -0,0 +1,387 @@
+'use client'
+
+import { useEffect, useState, useMemo, useCallback, memo } from 'react'
+import { useStore } from '@/lib/store'
+import { api } from '@/lib/api'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import {
+ PremiumTable,
+ StatCard,
+ PageContainer,
+ SearchInput,
+ TabBar,
+ FilterBar,
+ SelectDropdown,
+ ActionButton,
+} from '@/components/PremiumTable'
+import {
+ TrendingUp,
+ ChevronRight,
+ Globe,
+ DollarSign,
+ RefreshCw,
+ AlertTriangle,
+ Cpu,
+ MapPin,
+ Coins,
+ Crown,
+ Info,
+ Loader2,
+} from 'lucide-react'
+import clsx from 'clsx'
+
+interface TLDData {
+ tld: string
+ min_price: number
+ avg_price: number
+ max_price: number
+ min_renewal_price: number
+ avg_renewal_price: number
+ cheapest_registrar?: string
+ cheapest_registrar_url?: string
+ price_change_7d: number
+ price_change_1y: number
+ price_change_3y: number
+ risk_level: 'low' | 'medium' | 'high'
+ risk_reason: string
+ popularity_rank?: number
+ type?: string
+}
+
+// Category definitions
+const CATEGORIES = [
+ { id: 'all', label: 'All', icon: Globe },
+ { id: 'tech', label: 'Tech', icon: Cpu },
+ { id: 'geo', label: 'Geo', icon: MapPin },
+ { id: 'budget', label: 'Budget', icon: Coins },
+ { id: 'premium', label: 'Premium', icon: Crown },
+]
+
+const CATEGORY_FILTERS: Record boolean> = {
+ all: () => true,
+ tech: (tld) => ['ai', 'io', 'app', 'dev', 'tech', 'code', 'cloud', 'data', 'api', 'software'].includes(tld.tld),
+ geo: (tld) => ['ch', 'de', 'uk', 'us', 'fr', 'it', 'es', 'nl', 'at', 'eu', 'co', 'ca', 'au', 'nz', 'jp', 'cn', 'in', 'br', 'mx', 'nyc', 'london', 'paris', 'berlin', 'tokyo', 'swiss'].includes(tld.tld),
+ budget: (tld) => tld.min_price < 5,
+ premium: (tld) => tld.min_price >= 50,
+}
+
+const SORT_OPTIONS = [
+ { value: 'popularity', label: 'By Popularity' },
+ { value: 'price_asc', label: 'Price: Low → High' },
+ { value: 'price_desc', label: 'Price: High → Low' },
+ { value: 'change', label: 'By Price Change' },
+ { value: 'risk', label: 'By Risk Level' },
+]
+
+// Memoized Sparkline
+const Sparkline = memo(function Sparkline({ trend }: { trend: number }) {
+ const isPositive = trend > 0
+ const isNeutral = trend === 0
+
+ return (
+
+ {isNeutral ? (
+
+ ) : isPositive ? (
+
+ ) : (
+
+ )}
+
+ )
+})
+
+export default function TLDPricingPage() {
+ const { subscription } = useStore()
+
+ const [tldData, setTldData] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+ const [searchQuery, setSearchQuery] = useState('')
+ const [sortBy, setSortBy] = useState('popularity')
+ const [category, setCategory] = useState('all')
+ const [page, setPage] = useState(0)
+ const [total, setTotal] = useState(0)
+
+ const loadTLDData = useCallback(async () => {
+ setLoading(true)
+ try {
+ const response = await api.getTldOverview(
+ 50,
+ page * 50,
+ sortBy === 'risk' || sortBy === 'change' ? 'popularity' : sortBy as any,
+ )
+ const mapped: TLDData[] = (response.tlds || []).map((tld) => ({
+ tld: tld.tld,
+ min_price: tld.min_registration_price,
+ avg_price: tld.avg_registration_price,
+ max_price: tld.max_registration_price,
+ min_renewal_price: tld.min_renewal_price,
+ avg_renewal_price: tld.avg_renewal_price,
+ price_change_7d: tld.price_change_7d,
+ price_change_1y: tld.price_change_1y,
+ price_change_3y: tld.price_change_3y,
+ risk_level: tld.risk_level,
+ risk_reason: tld.risk_reason,
+ popularity_rank: tld.popularity_rank,
+ type: tld.type,
+ }))
+ setTldData(mapped)
+ setTotal(response.total || 0)
+ } catch (error) {
+ console.error('Failed to load TLD data:', error)
+ } finally {
+ setLoading(false)
+ }
+ }, [page, sortBy])
+
+ useEffect(() => {
+ loadTLDData()
+ }, [loadTLDData])
+
+ const handleRefresh = useCallback(async () => {
+ setRefreshing(true)
+ await loadTLDData()
+ setRefreshing(false)
+ }, [loadTLDData])
+
+ // Memoized filtered and sorted data
+ const sortedData = useMemo(() => {
+ let data = tldData.filter(CATEGORY_FILTERS[category] || (() => true))
+
+ if (searchQuery) {
+ const q = searchQuery.toLowerCase()
+ data = data.filter(tld => tld.tld.toLowerCase().includes(q))
+ }
+
+ if (sortBy === 'risk') {
+ const riskOrder = { high: 0, medium: 1, low: 2 }
+ data = [...data].sort((a, b) => riskOrder[a.risk_level] - riskOrder[b.risk_level])
+ }
+
+ return data
+ }, [tldData, category, searchQuery, sortBy])
+
+ // Memoized stats
+ const stats = useMemo(() => {
+ const lowestPrice = tldData.length > 0
+ ? tldData.reduce((min, tld) => Math.min(min, tld.min_price), Infinity)
+ : 0.99
+ const hottestTld = tldData.find(tld => (tld.price_change_7d || 0) > 5)?.tld || 'ai'
+ const trapCount = tldData.filter(tld => tld.risk_level === 'high' || tld.risk_level === 'medium').length
+ return { lowestPrice, hottestTld, trapCount }
+ }, [tldData])
+
+ const subtitle = useMemo(() => {
+ if (loading && total === 0) return 'Loading TLD pricing data...'
+ if (total === 0) return 'No TLD data available'
+ return `Tracking ${total.toLocaleString()} TLDs • Updated daily`
+ }, [loading, total])
+
+ // Memoized columns
+ const columns = useMemo(() => [
+ {
+ key: 'tld',
+ header: 'TLD',
+ width: '100px',
+ render: (tld: TLDData) => (
+
+ .{tld.tld}
+
+ ),
+ },
+ {
+ key: 'trend',
+ header: 'Trend',
+ width: '80px',
+ hideOnMobile: true,
+ render: (tld: TLDData) => ,
+ },
+ {
+ key: 'buy_price',
+ header: 'Buy (1y)',
+ align: 'right' as const,
+ width: '100px',
+ render: (tld: TLDData) => (
+ ${tld.min_price.toFixed(2)}
+ ),
+ },
+ {
+ key: 'renew_price',
+ header: 'Renew (1y)',
+ align: 'right' as const,
+ width: '120px',
+ render: (tld: TLDData) => {
+ const ratio = tld.min_renewal_price / tld.min_price
+ return (
+
+
${tld.min_renewal_price.toFixed(2)}
+ {ratio > 2 && (
+
+
+
+ )}
+
+ )
+ },
+ },
+ {
+ key: 'change_1y',
+ header: '1y',
+ align: 'right' as const,
+ width: '80px',
+ hideOnMobile: true,
+ render: (tld: TLDData) => {
+ const change = tld.price_change_1y || 0
+ return (
+ 0 ? "text-orange-400" : change < 0 ? "text-accent" : "text-foreground-muted")}>
+ {change > 0 ? '+' : ''}{change.toFixed(0)}%
+
+ )
+ },
+ },
+ {
+ key: 'change_3y',
+ header: '3y',
+ align: 'right' as const,
+ width: '80px',
+ hideOnMobile: true,
+ render: (tld: TLDData) => {
+ const change = tld.price_change_3y || 0
+ return (
+ 0 ? "text-orange-400" : change < 0 ? "text-accent" : "text-foreground-muted")}>
+ {change > 0 ? '+' : ''}{change.toFixed(0)}%
+
+ )
+ },
+ },
+ {
+ key: 'risk',
+ header: 'Risk',
+ align: 'center' as const,
+ width: '120px',
+ render: (tld: TLDData) => (
+
+
+ {tld.risk_reason}
+
+ ),
+ },
+ {
+ key: 'actions',
+ header: '',
+ align: 'right' as const,
+ width: '50px',
+ render: () => ,
+ },
+ ], [])
+
+ return (
+
+ {refreshing ? '' : 'Refresh'}
+
+ }
+ >
+
+ {/* Stats Overview */}
+
+ 0 ? total.toLocaleString() : '—'} subtitle="updated daily" icon={Globe} />
+ 0 ? `$${stats.lowestPrice.toFixed(2)}` : '—'} icon={DollarSign} />
+ 0 ? `.${stats.hottestTld}` : '—'} subtitle="rising prices" icon={TrendingUp} />
+
+
+
+ {/* Category Tabs */}
+ ({ id: c.id, label: c.label, icon: c.icon }))}
+ activeTab={category}
+ onChange={setCategory}
+ />
+
+ {/* Filters */}
+
+
+
+
+
+ {/* Legend */}
+
+
+
+ Tip: Renewal traps show ⚠️ when renewal price is >2x registration
+
+
+
+ {/* TLD Table */}
+ tld.tld}
+ loading={loading}
+ onRowClick={(tld) => window.location.href = `/command/pricing/${tld.tld}`}
+ emptyIcon={ }
+ emptyTitle="No TLDs found"
+ emptyDescription={searchQuery ? `No TLDs matching "${searchQuery}"` : "Check back later for TLD data"}
+ columns={columns}
+ />
+
+ {/* Pagination */}
+ {total > 50 && (
+
+ setPage(Math.max(0, page - 1))}
+ disabled={page === 0}
+ className="px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground bg-foreground/5 hover:bg-foreground/10 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ Previous
+
+
+ Page {page + 1} of {Math.ceil(total / 50)}
+
+ setPage(page + 1)}
+ disabled={(page + 1) * 50 >= total}
+ className="px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground bg-foreground/5 hover:bg-foreground/10 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ Next
+
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/app/command/seo/page.tsx b/frontend/src/app/command/seo/page.tsx
new file mode 100755
index 0000000..ba4161e
--- /dev/null
+++ b/frontend/src/app/command/seo/page.tsx
@@ -0,0 +1,508 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useStore } from '@/lib/store'
+import { api } from '@/lib/api'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import { PageContainer, StatCard, Badge } from '@/components/PremiumTable'
+import {
+ Search,
+ Link2,
+ Globe,
+ Shield,
+ TrendingUp,
+ Loader2,
+ AlertCircle,
+ X,
+ ExternalLink,
+ Crown,
+ CheckCircle,
+ Sparkles,
+ BookOpen,
+ Building,
+ GraduationCap,
+ Newspaper,
+ Lock,
+ Star,
+} from 'lucide-react'
+import Link from 'next/link'
+import clsx from 'clsx'
+
+interface SEOData {
+ domain: string
+ seo_score: number
+ value_category: string
+ metrics: {
+ domain_authority: number | null
+ page_authority: number | null
+ spam_score: number | null
+ total_backlinks: number | null
+ referring_domains: number | null
+ }
+ notable_links: {
+ has_wikipedia: boolean
+ has_gov: boolean
+ has_edu: boolean
+ has_news: boolean
+ notable_domains: string[]
+ }
+ top_backlinks: Array<{
+ domain: string
+ authority: number
+ page: string
+ }>
+ estimated_value: number | null
+ data_source: string
+ last_updated: string | null
+ is_estimated: boolean
+}
+
+export default function SEOPage() {
+ const { subscription } = useStore()
+
+ const [domain, setDomain] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [seoData, setSeoData] = useState(null)
+ const [error, setError] = useState(null)
+ const [recentSearches, setRecentSearches] = useState([])
+
+ const tier = subscription?.tier?.toLowerCase() || 'scout'
+ const isTycoon = tier === 'tycoon'
+
+ useEffect(() => {
+ // Load recent searches from localStorage
+ const saved = localStorage.getItem('seo-recent-searches')
+ if (saved) {
+ setRecentSearches(JSON.parse(saved))
+ }
+ }, [])
+
+ const saveRecentSearch = (domain: string) => {
+ const updated = [domain, ...recentSearches.filter(d => d !== domain)].slice(0, 5)
+ setRecentSearches(updated)
+ localStorage.setItem('seo-recent-searches', JSON.stringify(updated))
+ }
+
+ const cleanDomain = (d: string): string => {
+ // Remove whitespace, protocol, www, and trailing slashes
+ return d.trim()
+ .toLowerCase()
+ .replace(/\s+/g, '')
+ .replace(/^https?:\/\//, '')
+ .replace(/^www\./, '')
+ .replace(/\/.*$/, '')
+ }
+
+ const handleSearch = async (e: React.FormEvent) => {
+ e.preventDefault()
+ const cleanedDomain = cleanDomain(domain)
+ if (!cleanedDomain) return
+
+ setLoading(true)
+ setError(null)
+ setSeoData(null)
+
+ try {
+ const data = await api.request(`/seo/${encodeURIComponent(cleanedDomain)}`)
+ setSeoData(data)
+ saveRecentSearch(cleanedDomain)
+ } catch (err: any) {
+ setError(err.message || 'Failed to analyze domain')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleQuickSearch = async (searchDomain: string) => {
+ const cleanedDomain = cleanDomain(searchDomain)
+ setDomain(cleanedDomain)
+ setLoading(true)
+ setError(null)
+ setSeoData(null)
+
+ try {
+ const data = await api.request(`/seo/${encodeURIComponent(cleanedDomain)}`)
+ setSeoData(data)
+ } catch (err: any) {
+ setError(err.message || 'Failed to analyze domain')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const getScoreColor = (score: number) => {
+ if (score >= 60) return 'text-accent'
+ if (score >= 40) return 'text-amber-400'
+ if (score >= 20) return 'text-orange-400'
+ return 'text-foreground-muted'
+ }
+
+ const getScoreBg = (score: number) => {
+ if (score >= 60) return 'bg-accent/10 border-accent/30'
+ if (score >= 40) return 'bg-amber-500/10 border-amber-500/30'
+ if (score >= 20) return 'bg-orange-500/10 border-orange-500/30'
+ return 'bg-foreground/5 border-border'
+ }
+
+ const formatNumber = (num: number | null) => {
+ if (num === null) return '-'
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
+ if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
+ return num.toString()
+ }
+
+ // Show upgrade prompt for non-Tycoon users
+ if (!isTycoon) {
+ return (
+
+
+
+
+
+
+
Tycoon Feature
+
+ SEO Juice Detector is a premium feature for serious domain investors.
+ Analyze backlinks, domain authority, and find hidden gems that SEO agencies pay
+ $100-$500 for — even if the name is "ugly".
+
+
+
+
+
+
Backlink Analysis
+
Top referring domains
+
+
+
+
Domain Authority
+
Moz DA/PA scores
+
+
+
+
Notable Links
+
Wikipedia, .gov, .edu
+
+
+
+
+
+ Upgrade to Tycoon
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ {/* Error Message */}
+ {error && (
+
+
+
{error}
+
setError(null)}>
+
+ )}
+
+ {/* Search Form */}
+
+
+
+
+ setDomain(e.target.value)}
+ placeholder="Enter domain to analyze (e.g., example.com)"
+ className="w-full pl-12 pr-4 py-3 bg-background border border-border rounded-xl
+ text-foreground placeholder:text-foreground-subtle
+ focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent"
+ />
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+ Analyze
+
+
+
+ {/* Recent Searches */}
+ {recentSearches.length > 0 && !seoData && (
+
+ Recent:
+ {recentSearches.map((d) => (
+ handleQuickSearch(d)}
+ className="px-3 py-1 text-xs bg-foreground/5 text-foreground-muted rounded-full hover:bg-foreground/10 transition-colors"
+ >
+ {d}
+
+ ))}
+
+ )}
+
+
+ {/* Loading State */}
+ {loading && (
+
+
+
Analyzing backlinks & authority...
+
+ )}
+
+ {/* Results */}
+ {seoData && !loading && (
+
+ {/* Header with Score */}
+
+
+
+
+ {seoData.domain}
+
+
+
+ {seoData.data_source === 'moz' ? 'Moz Data' : 'Estimated'}
+
+ {seoData.value_category}
+
+
+
+
+ {seoData.seo_score}
+
+ SEO Score
+
+
+
+ {/* Estimated Value */}
+ {seoData.estimated_value && (
+
+
Estimated SEO Value
+
+ ${seoData.estimated_value.toLocaleString()}
+
+
+ Based on domain authority & backlink profile
+
+
+ )}
+
+
+ {/* Metrics Grid */}
+
+
+
+
+
+ 30 ? '⚠️ High' : '✓ Low'}
+ />
+
+
+ {/* Notable Links */}
+
+
Notable Backlinks
+
+
+
+
+
Wikipedia
+
+ {seoData.notable_links.has_wikipedia ? '✓ Found' : 'Not found'}
+
+
+
+
+
+
+
+
.gov Links
+
+ {seoData.notable_links.has_gov ? '✓ Found' : 'Not found'}
+
+
+
+
+
+
+
+
.edu Links
+
+ {seoData.notable_links.has_edu ? '✓ Found' : 'Not found'}
+
+
+
+
+
+
+
+
News Sites
+
+ {seoData.notable_links.has_news ? '✓ Found' : 'Not found'}
+
+
+
+
+
+ {/* Notable Domains List */}
+ {seoData.notable_links.notable_domains.length > 0 && (
+
+
High-authority referring domains:
+
+ {seoData.notable_links.notable_domains.map((d) => (
+
+ {d}
+
+ ))}
+
+
+ )}
+
+
+ {/* Top Backlinks */}
+ {seoData.top_backlinks.length > 0 && (
+
+
Top Backlinks
+
+ {seoData.top_backlinks.map((link, idx) => (
+
+
+
= 60 ? "bg-accent/10 text-accent" :
+ link.authority >= 40 ? "bg-amber-500/10 text-amber-400" :
+ "bg-foreground/5 text-foreground-muted"
+ )}>
+ {link.authority}
+
+
+
{link.domain}
+ {link.page && (
+
{link.page}
+ )}
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Data Source Note */}
+ {seoData.is_estimated && (
+
+
+
+ This data is estimated based on domain characteristics.
+ For live Moz data, configure MOZ_ACCESS_ID and MOZ_SECRET_KEY in the backend.
+
+
+ )}
+
+ )}
+
+ {/* Empty State */}
+ {!seoData && !loading && !error && (
+
+
+
SEO Juice Detector
+
+ Enter a domain above to analyze its backlink profile, domain authority,
+ and find hidden SEO value that others miss.
+
+
+ )}
+
+
+ )
+}
+
diff --git a/frontend/src/app/command/settings/page.tsx b/frontend/src/app/command/settings/page.tsx
new file mode 100755
index 0000000..00d7818
--- /dev/null
+++ b/frontend/src/app/command/settings/page.tsx
@@ -0,0 +1,563 @@
+'use client'
+
+import { useEffect, useState, useCallback, useMemo } from 'react'
+import { useRouter } from 'next/navigation'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import { PageContainer, TabBar } from '@/components/PremiumTable'
+import { useStore } from '@/lib/store'
+import { api, PriceAlert } from '@/lib/api'
+import {
+ User,
+ Bell,
+ CreditCard,
+ Shield,
+ ChevronRight,
+ Loader2,
+ Check,
+ AlertCircle,
+ Trash2,
+ ExternalLink,
+ Crown,
+ Zap,
+ Key,
+ TrendingUp,
+} from 'lucide-react'
+import Link from 'next/link'
+import clsx from 'clsx'
+
+type SettingsTab = 'profile' | 'notifications' | 'billing' | 'security'
+
+export default function SettingsPage() {
+ const router = useRouter()
+ const { user, isAuthenticated, isLoading, checkAuth, subscription } = useStore()
+
+ const [activeTab, setActiveTab] = useState('profile')
+ const [saving, setSaving] = useState(false)
+ const [success, setSuccess] = useState(null)
+ const [error, setError] = useState(null)
+
+ // Profile form
+ const [profileForm, setProfileForm] = useState({
+ name: '',
+ email: '',
+ })
+
+ // Notification preferences
+ const [notificationPrefs, setNotificationPrefs] = useState({
+ domain_availability: true,
+ price_alerts: true,
+ weekly_digest: false,
+ })
+ const [savingNotifications, setSavingNotifications] = useState(false)
+
+ // Price alerts
+ const [priceAlerts, setPriceAlerts] = useState([])
+ const [loadingAlerts, setLoadingAlerts] = useState(false)
+ const [deletingAlertId, setDeletingAlertId] = useState(null)
+
+ useEffect(() => {
+ checkAuth()
+ }, [checkAuth])
+
+ useEffect(() => {
+ if (!isLoading && !isAuthenticated) {
+ router.push('/login')
+ }
+ }, [isLoading, isAuthenticated, router])
+
+ useEffect(() => {
+ if (user) {
+ setProfileForm({
+ name: user.name || '',
+ email: user.email || '',
+ })
+ }
+ }, [user])
+
+ useEffect(() => {
+ if (isAuthenticated && activeTab === 'notifications') {
+ loadPriceAlerts()
+ }
+ }, [isAuthenticated, activeTab])
+
+ const loadPriceAlerts = async () => {
+ setLoadingAlerts(true)
+ try {
+ const alerts = await api.getPriceAlerts()
+ setPriceAlerts(alerts)
+ } catch (err) {
+ console.error('Failed to load alerts:', err)
+ } finally {
+ setLoadingAlerts(false)
+ }
+ }
+
+ const handleSaveProfile = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setSaving(true)
+ setError(null)
+ setSuccess(null)
+
+ try {
+ await api.updateMe({ name: profileForm.name || undefined })
+ const { checkAuth } = useStore.getState()
+ await checkAuth()
+ setSuccess('Profile updated successfully')
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to update profile')
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const handleSaveNotifications = async () => {
+ setSavingNotifications(true)
+ setError(null)
+ setSuccess(null)
+
+ try {
+ localStorage.setItem('notification_prefs', JSON.stringify(notificationPrefs))
+ setSuccess('Notification preferences saved')
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to save preferences')
+ } finally {
+ setSavingNotifications(false)
+ }
+ }
+
+ useEffect(() => {
+ const saved = localStorage.getItem('notification_prefs')
+ if (saved) {
+ try {
+ setNotificationPrefs(JSON.parse(saved))
+ } catch {}
+ }
+ }, [])
+
+ const handleDeletePriceAlert = async (tld: string, alertId: number) => {
+ setDeletingAlertId(alertId)
+ try {
+ await api.deletePriceAlert(tld)
+ setPriceAlerts(prev => prev.filter(a => a.id !== alertId))
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to delete alert')
+ } finally {
+ setDeletingAlertId(null)
+ }
+ }
+
+ const handleOpenBillingPortal = async () => {
+ try {
+ const { portal_url } = await api.createPortalSession()
+ window.location.href = portal_url
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to open billing portal')
+ }
+ }
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (!isAuthenticated || !user) {
+ return null
+ }
+
+ const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
+ const isProOrHigher = ['Trader', 'Tycoon', 'Professional', 'Enterprise'].includes(tierName)
+
+ const tabs = [
+ { id: 'profile' as const, label: 'Profile', icon: User },
+ { id: 'notifications' as const, label: 'Notifications', icon: Bell },
+ { id: 'billing' as const, label: 'Billing', icon: CreditCard },
+ { id: 'security' as const, label: 'Security', icon: Shield },
+ ]
+
+ return (
+
+
+ {/* Messages */}
+ {error && (
+
+
+
{error}
+
setError(null)} className="text-red-400 hover:text-red-300">
+
+
+
+ )}
+
+ {success && (
+
+
+
{success}
+
setSuccess(null)} className="text-accent hover:text-accent/80">
+
+
+
+ )}
+
+
+ {/* Sidebar */}
+
+ {/* Mobile: Horizontal scroll tabs */}
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id)}
+ className={clsx(
+ "flex items-center gap-2.5 px-5 py-3 text-sm font-medium rounded-xl whitespace-nowrap transition-all",
+ activeTab === tab.id
+ ? "bg-gradient-to-r from-accent to-accent/80 text-background shadow-lg shadow-accent/20"
+ : "bg-background-secondary/50 text-foreground-muted hover:text-foreground border border-border/50"
+ )}
+ >
+
+ {tab.label}
+
+ ))}
+
+
+ {/* Desktop: Vertical tabs */}
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id)}
+ className={clsx(
+ "w-full flex items-center gap-3 px-5 py-3.5 text-sm font-medium rounded-xl transition-all",
+ activeTab === tab.id
+ ? "bg-gradient-to-r from-accent to-accent/80 text-background shadow-lg shadow-accent/20"
+ : "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
+ )}
+ >
+
+ {tab.label}
+
+ ))}
+
+
+ {/* Plan info */}
+
+
+ {isProOrHigher ? : }
+ {tierName} Plan
+
+
+ {subscription?.domains_used || 0} / {subscription?.domain_limit || 5} domains tracked
+
+ {!isProOrHigher && (
+
+ Upgrade
+
+
+ )}
+
+
+
+ {/* Content */}
+
+ {/* Profile Tab */}
+ {activeTab === 'profile' && (
+
+
Profile Information
+
+
+
+ Name
+ setProfileForm({ ...profileForm, name: e.target.value })}
+ placeholder="Your name"
+ className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
+ placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all"
+ />
+
+
+
+
Email
+
+
Email cannot be changed
+
+
+
+ {saving ? : }
+ Save Changes
+
+
+
+ )}
+
+ {/* Notifications Tab */}
+ {activeTab === 'notifications' && (
+
+
+
Email Preferences
+
+
+ {[
+ { key: 'domain_availability', label: 'Domain Availability', desc: 'Get notified when watched domains become available' },
+ { key: 'price_alerts', label: 'Price Alerts', desc: 'Get notified when TLD prices change' },
+ { key: 'weekly_digest', label: 'Weekly Digest', desc: 'Receive a weekly summary of your portfolio' },
+ ].map((item) => (
+
+
+
{item.label}
+
{item.desc}
+
+ setNotificationPrefs({ ...notificationPrefs, [item.key]: e.target.checked })}
+ className="w-5 h-5 accent-accent cursor-pointer"
+ />
+
+ ))}
+
+
+
+ {savingNotifications ? : }
+ Save Preferences
+
+
+
+ {/* Active Price Alerts */}
+
+
Active Price Alerts
+
+ {loadingAlerts ? (
+
+
+
+ ) : priceAlerts.length === 0 ? (
+
+
+
No price alerts set
+
+ Browse TLD prices →
+
+
+ ) : (
+
+ {priceAlerts.map((alert) => (
+
+
+
+
+ {alert.is_active && (
+
+ )}
+
+
+
+ .{alert.tld}
+
+
+ Alert on {alert.threshold_percent}% change
+ {alert.target_price && ` or below $${alert.target_price}`}
+
+
+
+
handleDeletePriceAlert(alert.tld, alert.id)}
+ disabled={deletingAlertId === alert.id}
+ className="p-2 text-foreground-subtle hover:text-red-400 hover:bg-red-400/10 rounded-lg transition-all"
+ >
+ {deletingAlertId === alert.id ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* Billing Tab */}
+ {activeTab === 'billing' && (
+
+ {/* Current Plan */}
+
+
Your Current Plan
+
+
+
+
+ {tierName === 'Tycoon' ?
: tierName === 'Trader' ?
:
}
+
+
{tierName}
+
+ {tierName === 'Scout' ? 'Free forever' : tierName === 'Trader' ? '$9/month' : '$29/month'}
+
+
+
+
+ {isProOrHigher ? 'Active' : 'Free'}
+
+
+
+ {/* Plan Stats */}
+
+
+
{subscription?.domain_limit || 5}
+
Domains
+
+
+
+ {subscription?.check_frequency === 'realtime' ? '10m' :
+ subscription?.check_frequency === 'hourly' ? '1h' : '24h'}
+
+
Check Interval
+
+
+
+ {subscription?.portfolio_limit === -1 ? '∞' : subscription?.portfolio_limit || 0}
+
+
Portfolio
+
+
+
+ {isProOrHigher ? (
+
+
+ Manage Subscription
+
+ ) : (
+
+
+ Upgrade Plan
+
+ )}
+
+
+ {/* Plan Features */}
+
Your Plan Includes
+
+ {[
+ `${subscription?.domain_limit || 5} Watchlist Domains`,
+ `${subscription?.check_frequency === 'realtime' ? '10-minute' : subscription?.check_frequency === 'hourly' ? 'Hourly' : 'Daily'} Scans`,
+ 'Email Alerts',
+ 'TLD Price Data',
+ ].map((feature) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+ )}
+
+ {/* Security Tab */}
+ {activeTab === 'security' && (
+
+
+
Password
+
+ Change your password or reset it if you've forgotten it.
+
+
+
+ Change Password
+
+
+
+
+
Account Security
+
+
+
+
+
Email Verified
+
Your email address has been verified
+
+
+
+
+
+
+
+
+
Two-Factor Authentication
+
Coming soon
+
+
Soon
+
+
+
+
+
+
Danger Zone
+
+ Permanently delete your account and all associated data.
+
+
+ Delete Account
+
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/frontend/src/app/command/watchlist/page.tsx b/frontend/src/app/command/watchlist/page.tsx
new file mode 100755
index 0000000..d925131
--- /dev/null
+++ b/frontend/src/app/command/watchlist/page.tsx
@@ -0,0 +1,620 @@
+'use client'
+
+import { useEffect, useState, useMemo, useCallback, memo } from 'react'
+import { useStore } from '@/lib/store'
+import { api, DomainHealthReport, HealthStatus } from '@/lib/api'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import {
+ PremiumTable,
+ Badge,
+ StatCard,
+ PageContainer,
+ TableActionButton,
+ SearchInput,
+ TabBar,
+ FilterBar,
+ ActionButton,
+} from '@/components/PremiumTable'
+import { Toast, useToast } from '@/components/Toast'
+import {
+ Plus,
+ Trash2,
+ RefreshCw,
+ Loader2,
+ Bell,
+ BellOff,
+ ExternalLink,
+ Eye,
+ Sparkles,
+ ArrowUpRight,
+ X,
+ Activity,
+ Shield,
+ AlertTriangle,
+ ShoppingCart,
+ HelpCircle,
+} from 'lucide-react'
+import clsx from 'clsx'
+import Link from 'next/link'
+
+// Health status badge colors and icons
+const healthStatusConfig: Record = {
+ healthy: {
+ label: 'Healthy',
+ color: 'text-accent',
+ bgColor: 'bg-accent/10 border-accent/20',
+ icon: Activity,
+ description: 'Domain is active and well-maintained'
+ },
+ weakening: {
+ label: 'Weakening',
+ color: 'text-amber-400',
+ bgColor: 'bg-amber-400/10 border-amber-400/20',
+ icon: AlertTriangle,
+ description: 'Warning signs detected - owner may be losing interest'
+ },
+ parked: {
+ label: 'For Sale',
+ color: 'text-orange-400',
+ bgColor: 'bg-orange-400/10 border-orange-400/20',
+ icon: ShoppingCart,
+ description: 'Domain is parked and likely for sale'
+ },
+ critical: {
+ label: 'Critical',
+ color: 'text-red-400',
+ bgColor: 'bg-red-400/10 border-red-400/20',
+ icon: AlertTriangle,
+ description: 'Domain drop is imminent!'
+ },
+ unknown: {
+ label: 'Unknown',
+ color: 'text-foreground-muted',
+ bgColor: 'bg-foreground/5 border-border/30',
+ icon: HelpCircle,
+ description: 'Could not determine status'
+ },
+}
+
+type FilterStatus = 'all' | 'available' | 'watching'
+
+export default function WatchlistPage() {
+ const { domains, addDomain, deleteDomain, refreshDomain, subscription } = useStore()
+ const { toast, showToast, hideToast } = useToast()
+
+ const [newDomain, setNewDomain] = useState('')
+ const [adding, setAdding] = useState(false)
+ const [refreshingId, setRefreshingId] = useState(null)
+ const [deletingId, setDeletingId] = useState(null)
+ const [togglingNotifyId, setTogglingNotifyId] = useState(null)
+ const [filterStatus, setFilterStatus] = useState('all')
+ const [searchQuery, setSearchQuery] = useState('')
+
+ // Health check state
+ const [healthReports, setHealthReports] = useState>({})
+ const [loadingHealth, setLoadingHealth] = useState>({})
+ const [selectedHealthDomainId, setSelectedHealthDomainId] = useState(null)
+
+ // Memoized stats - avoids recalculation on every render
+ const stats = useMemo(() => ({
+ availableCount: domains?.filter(d => d.is_available).length || 0,
+ watchingCount: domains?.filter(d => !d.is_available).length || 0,
+ domainsUsed: domains?.length || 0,
+ domainLimit: subscription?.domain_limit || 5,
+ }), [domains, subscription?.domain_limit])
+
+ const canAddMore = stats.domainsUsed < stats.domainLimit
+
+ // Memoized filtered domains
+ const filteredDomains = useMemo(() => {
+ if (!domains) return []
+
+ return domains.filter(domain => {
+ if (searchQuery && !domain.name.toLowerCase().includes(searchQuery.toLowerCase())) {
+ return false
+ }
+ if (filterStatus === 'available' && !domain.is_available) return false
+ if (filterStatus === 'watching' && domain.is_available) return false
+ return true
+ })
+ }, [domains, searchQuery, filterStatus])
+
+ // Memoized tabs config
+ const tabs = useMemo(() => [
+ { id: 'all', label: 'All', count: stats.domainsUsed },
+ { id: 'available', label: 'Available', count: stats.availableCount, color: 'accent' as const },
+ { id: 'watching', label: 'Monitoring', count: stats.watchingCount },
+ ], [stats])
+
+ // Callbacks - prevent recreation on every render
+ const handleAddDomain = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!newDomain.trim()) return
+
+ setAdding(true)
+ try {
+ await addDomain(newDomain.trim())
+ setNewDomain('')
+ showToast(`Added ${newDomain.trim()} to watchlist`, 'success')
+ } catch (err: any) {
+ showToast(err.message || 'Failed to add domain', 'error')
+ } finally {
+ setAdding(false)
+ }
+ }, [newDomain, addDomain, showToast])
+
+ const handleRefresh = useCallback(async (id: number) => {
+ setRefreshingId(id)
+ try {
+ await refreshDomain(id)
+ showToast('Domain status refreshed', 'success')
+ } catch (err: any) {
+ showToast(err.message || 'Failed to refresh', 'error')
+ } finally {
+ setRefreshingId(null)
+ }
+ }, [refreshDomain, showToast])
+
+ const handleDelete = useCallback(async (id: number, name: string) => {
+ if (!confirm(`Remove ${name} from your watchlist?`)) return
+
+ setDeletingId(id)
+ try {
+ await deleteDomain(id)
+ showToast(`Removed ${name} from watchlist`, 'success')
+ } catch (err: any) {
+ showToast(err.message || 'Failed to remove', 'error')
+ } finally {
+ setDeletingId(null)
+ }
+ }, [deleteDomain, showToast])
+
+ const handleToggleNotify = useCallback(async (id: number, currentState: boolean) => {
+ setTogglingNotifyId(id)
+ try {
+ await api.updateDomainNotify(id, !currentState)
+ showToast(!currentState ? 'Notifications enabled' : 'Notifications disabled', 'success')
+ } catch (err: any) {
+ showToast(err.message || 'Failed to update', 'error')
+ } finally {
+ setTogglingNotifyId(null)
+ }
+ }, [showToast])
+
+ const handleHealthCheck = useCallback(async (domainId: number) => {
+ if (loadingHealth[domainId]) return
+
+ setLoadingHealth(prev => ({ ...prev, [domainId]: true }))
+ try {
+ const report = await api.getDomainHealth(domainId)
+ setHealthReports(prev => ({ ...prev, [domainId]: report }))
+ setSelectedHealthDomainId(domainId)
+ } catch (err: any) {
+ showToast(err.message || 'Health check failed', 'error')
+ } finally {
+ setLoadingHealth(prev => ({ ...prev, [domainId]: false }))
+ }
+ }, [loadingHealth, showToast])
+
+ // Dynamic subtitle
+ const subtitle = useMemo(() => {
+ if (stats.domainsUsed === 0) return 'Start tracking domains to monitor their availability'
+ return `Monitoring ${stats.domainsUsed} domain${stats.domainsUsed !== 1 ? 's' : ''} • ${stats.domainLimit === -1 ? 'Unlimited' : `${stats.domainLimit - stats.domainsUsed} slots left`}`
+ }, [stats])
+
+ // Memoized columns config
+ const columns = useMemo(() => [
+ {
+ key: 'domain',
+ header: 'Domain',
+ render: (domain: any) => (
+
+
+
+ {domain.is_available && (
+
+ )}
+
+
+ {domain.name}
+ {domain.is_available && (
+ AVAILABLE
+ )}
+
+
+ ),
+ },
+ {
+ key: 'status',
+ header: 'Status',
+ align: 'left' as const,
+ hideOnMobile: true,
+ render: (domain: any) => {
+ const health = healthReports[domain.id]
+ if (health) {
+ const config = healthStatusConfig[health.status]
+ const Icon = config.icon
+ return (
+
+
+ {config.label}
+
+ )
+ }
+ return (
+
+ {domain.is_available ? 'Ready to pounce!' : 'Monitoring...'}
+
+ )
+ },
+ },
+ {
+ key: 'notifications',
+ header: 'Alerts',
+ align: 'center' as const,
+ width: '80px',
+ hideOnMobile: true,
+ render: (domain: any) => (
+ {
+ e.stopPropagation()
+ handleToggleNotify(domain.id, domain.notify_on_available)
+ }}
+ disabled={togglingNotifyId === domain.id}
+ className={clsx(
+ "p-2 rounded-lg transition-colors",
+ domain.notify_on_available
+ ? "bg-accent/10 text-accent hover:bg-accent/20"
+ : "text-foreground-muted hover:bg-foreground/5"
+ )}
+ title={domain.notify_on_available ? "Disable alerts" : "Enable alerts"}
+ >
+ {togglingNotifyId === domain.id ? (
+
+ ) : domain.notify_on_available ? (
+
+ ) : (
+
+ )}
+
+ ),
+ },
+ {
+ key: 'actions',
+ header: '',
+ align: 'right' as const,
+ render: (domain: any) => (
+
+ ),
+ },
+ ], [healthReports, togglingNotifyId, loadingHealth, refreshingId, deletingId, handleToggleNotify, handleHealthCheck, handleRefresh, handleDelete])
+
+ return (
+
+ {toast && }
+
+
+ {/* Stats Cards */}
+
+
+
+
+
+
+
+ {/* Add Domain Form */}
+
+
+
+ Add Domain
+
+
+
+ {!canAddMore && (
+
+
+ You've reached your domain limit. Upgrade to track more.
+
+
+ Upgrade
+
+
+ )}
+
+ {/* Filters */}
+
+ setFilterStatus(id as FilterStatus)}
+ />
+
+
+
+ {/* Domain Table */}
+ d.id}
+ emptyIcon={ }
+ emptyTitle={stats.domainsUsed === 0 ? "Your watchlist is empty" : "No domains match your filters"}
+ emptyDescription={stats.domainsUsed === 0 ? "Add a domain above to start tracking" : "Try adjusting your filter criteria"}
+ columns={columns}
+ />
+
+ {/* Health Report Modal */}
+ {selectedHealthDomainId && healthReports[selectedHealthDomainId] && (
+ setSelectedHealthDomainId(null)}
+ />
+ )}
+
+
+ )
+}
+
+// Health Report Modal Component - memoized
+const HealthReportModal = memo(function HealthReportModal({
+ report,
+ onClose
+}: {
+ report: DomainHealthReport
+ onClose: () => void
+}) {
+ const config = healthStatusConfig[report.status]
+ const Icon = config.icon
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+
+
+
+
+
{report.domain}
+
{config.description}
+
+
+
+
+
+
+
+ {/* Score */}
+
+
+
Health Score
+
+
+
= 70 ? "bg-accent" :
+ report.score >= 40 ? "bg-amber-400" : "bg-red-400"
+ )}
+ style={{ width: `${report.score}%` }}
+ />
+
+
= 70 ? "text-accent" :
+ report.score >= 40 ? "text-amber-400" : "text-red-400"
+ )}>
+ {report.score}/100
+
+
+
+
+
+ {/* Check Results */}
+
+ {/* DNS */}
+ {report.dns && (
+
+
+
+ DNS Infrastructure
+
+
+
+
+ {report.dns.has_ns ? '✓' : '✗'}
+
+ Nameservers
+
+
+
+ {report.dns.has_a ? '✓' : '✗'}
+
+ A Record
+
+
+
+ {report.dns.has_mx ? '✓' : '—'}
+
+ MX Record
+
+
+ {report.dns.is_parked && (
+
⚠ Parked at {report.dns.parking_provider || 'unknown provider'}
+ )}
+
+ )}
+
+ {/* HTTP */}
+ {report.http && (
+
+
+
+ Website Status
+
+
+
+ {report.http.is_reachable ? 'Reachable' : 'Unreachable'}
+
+ {report.http.status_code && (
+ HTTP {report.http.status_code}
+ )}
+
+ {report.http.is_parked && (
+
⚠ Parking page detected
+ )}
+
+ )}
+
+ {/* SSL */}
+ {report.ssl && (
+
+
+
+ SSL Certificate
+
+
+ {report.ssl.has_certificate ? (
+
+
+ {report.ssl.is_valid ? '✓ Valid certificate' : '✗ Certificate invalid/expired'}
+
+ {report.ssl.days_until_expiry !== undefined && (
+
30 ? "text-foreground-muted" :
+ report.ssl.days_until_expiry > 7 ? "text-amber-400" : "text-red-400"
+ )}>
+ Expires in {report.ssl.days_until_expiry} days
+
+ )}
+
+ ) : (
+
No SSL certificate
+ )}
+
+
+ )}
+
+ {/* Signals & Recommendations */}
+ {((report.signals?.length || 0) > 0 || (report.recommendations?.length || 0) > 0) && (
+
+ {(report.signals?.length || 0) > 0 && (
+
+
Signals
+
+ {report.signals?.map((signal, i) => (
+
+ •
+ {signal}
+
+ ))}
+
+
+ )}
+ {(report.recommendations?.length || 0) > 0 && (
+
+
Recommendations
+
+ {report.recommendations?.map((rec, i) => (
+
+ →
+ {rec}
+
+ ))}
+
+
+ )}
+
+ )}
+
+
+ {/* Footer */}
+
+
+ Checked at {new Date(report.checked_at).toLocaleString()}
+
+
+
+
+ )
+})
diff --git a/frontend/src/app/command/welcome/page.tsx b/frontend/src/app/command/welcome/page.tsx
new file mode 100755
index 0000000..3b46849
--- /dev/null
+++ b/frontend/src/app/command/welcome/page.tsx
@@ -0,0 +1,221 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { CommandCenterLayout } from '@/components/CommandCenterLayout'
+import { PageContainer } from '@/components/PremiumTable'
+import { useStore } from '@/lib/store'
+import {
+ CheckCircle,
+ Zap,
+ Crown,
+ ArrowRight,
+ Eye,
+ Store,
+ Bell,
+ BarChart3,
+ Sparkles,
+ TrendingUp,
+} from 'lucide-react'
+import Link from 'next/link'
+import clsx from 'clsx'
+
+const planDetails = {
+ trader: {
+ name: 'Trader',
+ icon: TrendingUp,
+ color: 'text-accent',
+ bgColor: 'bg-accent/10',
+ features: [
+ { icon: Eye, text: '50 domains in watchlist', description: 'Track up to 50 domains at once' },
+ { icon: Zap, text: 'Hourly availability checks', description: '24x faster than Scout' },
+ { icon: Store, text: '10 For Sale listings', description: 'List your domains on the marketplace' },
+ { icon: Bell, text: '5 Sniper Alerts', description: 'Get notified when specific domains drop' },
+ { icon: BarChart3, text: 'Deal scores & valuations', description: 'Know what domains are worth' },
+ ],
+ nextSteps: [
+ { href: '/command/watchlist', label: 'Add domains to watchlist', icon: Eye },
+ { href: '/command/alerts', label: 'Set up Sniper Alerts', icon: Bell },
+ { href: '/command/portfolio', label: 'Track your portfolio', icon: BarChart3 },
+ ],
+ },
+ tycoon: {
+ name: 'Tycoon',
+ icon: Crown,
+ color: 'text-amber-400',
+ bgColor: 'bg-amber-400/10',
+ features: [
+ { icon: Eye, text: '500 domains in watchlist', description: 'Massive tracking capacity' },
+ { icon: Zap, text: 'Real-time checks (10 min)', description: 'Never miss a drop' },
+ { icon: Store, text: '50 For Sale listings', description: 'Full marketplace access' },
+ { icon: Bell, text: 'Unlimited Sniper Alerts', description: 'Set as many as you need' },
+ { icon: Sparkles, text: 'SEO Juice Detector', description: 'Find domains with backlinks' },
+ ],
+ nextSteps: [
+ { href: '/command/watchlist', label: 'Add domains to watchlist', icon: Eye },
+ { href: '/command/seo', label: 'Analyze SEO metrics', icon: Sparkles },
+ { href: '/command/alerts', label: 'Create Sniper Alerts', icon: Bell },
+ ],
+ },
+}
+
+export default function WelcomePage() {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const { fetchSubscription, checkAuth } = useStore()
+ const [loading, setLoading] = useState(true)
+ const [showConfetti, setShowConfetti] = useState(true)
+
+ const planId = searchParams.get('plan') as 'trader' | 'tycoon' | null
+ const plan = planId && planDetails[planId] ? planDetails[planId] : planDetails.trader
+
+ useEffect(() => {
+ const init = async () => {
+ await checkAuth()
+ await fetchSubscription()
+ setLoading(false)
+ }
+ init()
+
+ // Hide confetti after animation
+ const timer = setTimeout(() => setShowConfetti(false), 3000)
+ return () => clearTimeout(timer)
+ }, [checkAuth, fetchSubscription])
+
+ if (loading) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ {/* Confetti Effect */}
+ {showConfetti && (
+
+ {Array.from({ length: 50 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+ {/* Success Header */}
+
+
+
+
+
+
+ Welcome to {plan.name}!
+
+
+ Your payment was successful. You now have access to all {plan.name} features.
+
+
+
+ {/* Features Unlocked */}
+
+
+ Features Unlocked
+
+
+ {plan.features.map((feature, i) => (
+
+
+
+
+
+
+
{feature.text}
+
{feature.description}
+
+
+
+ ))}
+
+
+
+ {/* Next Steps */}
+
+
+ Get Started
+
+
+ {plan.nextSteps.map((step, i) => (
+
+
+
+
+ ))}
+
+
+
+ {/* Go to Dashboard */}
+
+
+ Go to Dashboard
+
+
+
+ Need help? Check out our documentation or{' '}
+ contact support.
+
+
+
+
+ {/* Custom CSS for confetti animation */}
+
+
+ )
+}
+
diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
new file mode 100755
index 0000000..b8932c3
--- /dev/null
+++ b/frontend/src/app/dashboard/page.tsx
@@ -0,0 +1,1105 @@
+'use client'
+
+import { useEffect, useState, Suspense } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useStore } from '@/lib/store'
+import { api, PortfolioDomain, PortfolioSummary, DomainValuation } from '@/lib/api'
+import { Header } from '@/components/Header'
+import { Footer } from '@/components/Footer'
+import { Toast, useToast } from '@/components/Toast'
+import {
+ Plus,
+ Trash2,
+ RefreshCw,
+ Loader2,
+ Clock,
+ AlertCircle,
+ Calendar,
+ History,
+ Bell,
+ BellOff,
+ Check,
+ X,
+ Zap,
+ Crown,
+ Briefcase,
+ Eye,
+ DollarSign,
+ Tag,
+ Edit2,
+ Sparkles,
+ CreditCard,
+ Globe,
+} from 'lucide-react'
+import clsx from 'clsx'
+import Link from 'next/link'
+
+type TabType = 'watchlist' | 'portfolio'
+
+interface DomainHistory {
+ id: number
+ status: string
+ is_available: boolean
+ checked_at: string
+}
+
+function DashboardContent() {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const {
+ isAuthenticated,
+ isLoading,
+ checkAuth,
+ domains,
+ subscription,
+ addDomain,
+ deleteDomain,
+ refreshDomain,
+ } = useStore()
+
+ const { toast, showToast, hideToast } = useToast()
+ const [activeTab, setActiveTab] = useState
('watchlist')
+ const [newDomain, setNewDomain] = useState('')
+ const [adding, setAdding] = useState(false)
+ const [refreshingId, setRefreshingId] = useState(null)
+ const [error, setError] = useState(null)
+
+ // Check for upgrade success
+ useEffect(() => {
+ if (searchParams.get('upgraded') === 'true') {
+ showToast('Welcome to your upgraded plan! 🎉', 'success')
+ // Clean up URL
+ window.history.replaceState({}, '', '/dashboard')
+ }
+ }, [searchParams])
+ const [selectedDomainId, setSelectedDomainId] = useState(null)
+ const [domainHistory, setDomainHistory] = useState(null)
+ const [loadingHistory, setLoadingHistory] = useState(false)
+ const [portfolio, setPortfolio] = useState([])
+ const [portfolioSummary, setPortfolioSummary] = useState(null)
+ const [loadingPortfolio, setLoadingPortfolio] = useState(false)
+ const [showAddPortfolioModal, setShowAddPortfolioModal] = useState(false)
+ const [showValuationModal, setShowValuationModal] = useState(false)
+ const [valuationResult, setValuationResult] = useState(null)
+ const [valuatingDomain, setValuatingDomain] = useState('')
+ const [refreshingPortfolioId, setRefreshingPortfolioId] = useState(null)
+ const [portfolioForm, setPortfolioForm] = useState({
+ domain: '',
+ purchase_price: '',
+ purchase_date: '',
+ registrar: '',
+ renewal_date: '',
+ renewal_cost: '',
+ notes: '',
+ })
+ const [addingPortfolio, setAddingPortfolio] = useState(false)
+ const [showEditPortfolioModal, setShowEditPortfolioModal] = useState(false)
+ const [editingPortfolioDomain, setEditingPortfolioDomain] = useState(null)
+ const [editPortfolioForm, setEditPortfolioForm] = useState({
+ purchase_price: '',
+ purchase_date: '',
+ registrar: '',
+ renewal_date: '',
+ renewal_cost: '',
+ notes: '',
+ })
+ const [savingEdit, setSavingEdit] = useState(false)
+ const [showSellModal, setShowSellModal] = useState(false)
+ const [sellingDomain, setSellingDomain] = useState(null)
+ const [sellForm, setSellForm] = useState({
+ sale_date: new Date().toISOString().split('T')[0],
+ sale_price: '',
+ })
+ const [processingSale, setProcessingSale] = useState(false)
+ const [togglingNotifyId, setTogglingNotifyId] = useState(null)
+
+ useEffect(() => {
+ checkAuth()
+ }, [checkAuth])
+
+ useEffect(() => {
+ if (!isLoading && !isAuthenticated) {
+ router.push('/login')
+ }
+ }, [isLoading, isAuthenticated, router])
+
+ // Load portfolio data on mount (so we can show count in tab)
+ useEffect(() => {
+ if (isAuthenticated) {
+ loadPortfolio()
+ }
+ }, [isAuthenticated])
+
+ const handleOpenBillingPortal = async () => {
+ try {
+ const { portal_url } = await api.createPortalSession()
+ window.location.href = portal_url
+ } catch (err: any) {
+ setError(err.message || 'Failed to open billing portal')
+ }
+ }
+
+ const loadPortfolio = async () => {
+ setLoadingPortfolio(true)
+ try {
+ const [portfolioData, summaryData] = await Promise.all([
+ api.getPortfolio(),
+ api.getPortfolioSummary(),
+ ])
+ setPortfolio(portfolioData)
+ setPortfolioSummary(summaryData)
+ } catch (err) {
+ console.error('Failed to load portfolio:', err)
+ } finally {
+ setLoadingPortfolio(false)
+ }
+ }
+
+ const handleAddDomain = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!newDomain.trim()) return
+ setAdding(true)
+ setError(null)
+ try {
+ await addDomain(newDomain)
+ showToast(`Added ${newDomain} to watchlist`, 'success')
+ setNewDomain('')
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to add domain')
+ } finally {
+ setAdding(false)
+ }
+ }
+
+ const handleRefresh = async (id: number) => {
+ setRefreshingId(id)
+ try {
+ await refreshDomain(id)
+ } finally {
+ setRefreshingId(null)
+ }
+ }
+
+ const handleDelete = async (id: number) => {
+ if (!confirm('Remove this domain from your watchlist?')) return
+ await deleteDomain(id)
+ }
+
+ const loadDomainHistory = async (domainId: number) => {
+ if (!subscription?.features?.expiration_tracking) {
+ setError('Check history requires Trader or Tycoon plan')
+ return
+ }
+ setSelectedDomainId(domainId)
+ setLoadingHistory(true)
+ try {
+ const result = await api.getDomainHistory(domainId, 30)
+ setDomainHistory(result.history)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to load history')
+ } finally {
+ setLoadingHistory(false)
+ }
+ }
+
+ const handleAddPortfolioDomain = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!portfolioForm.domain.trim()) return
+ setAddingPortfolio(true)
+ try {
+ await api.addPortfolioDomain({
+ domain: portfolioForm.domain,
+ purchase_price: portfolioForm.purchase_price ? parseFloat(portfolioForm.purchase_price) : undefined,
+ purchase_date: portfolioForm.purchase_date || undefined,
+ registrar: portfolioForm.registrar || undefined,
+ renewal_date: portfolioForm.renewal_date || undefined,
+ renewal_cost: portfolioForm.renewal_cost ? parseFloat(portfolioForm.renewal_cost) : undefined,
+ notes: portfolioForm.notes || undefined,
+ })
+ setPortfolioForm({ domain: '', purchase_price: '', purchase_date: '', registrar: '', renewal_date: '', renewal_cost: '', notes: '' })
+ setShowAddPortfolioModal(false)
+ showToast(`Added ${portfolioForm.domain} to portfolio`, 'success')
+ loadPortfolio()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to add domain to portfolio')
+ } finally {
+ setAddingPortfolio(false)
+ }
+ }
+
+ const handleDeletePortfolioDomain = async (id: number) => {
+ if (!confirm('Remove this domain from your portfolio?')) return
+ try {
+ await api.deletePortfolioDomain(id)
+ showToast('Domain removed from portfolio', 'success')
+ loadPortfolio()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to delete domain')
+ }
+ }
+
+ const handleRefreshPortfolioValue = async (id: number) => {
+ setRefreshingPortfolioId(id)
+ try {
+ await api.refreshDomainValue(id)
+ loadPortfolio()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to refresh value')
+ } finally {
+ setRefreshingPortfolioId(null)
+ }
+ }
+
+ const handleGetValuation = async (domain: string) => {
+ setValuatingDomain(domain)
+ setShowValuationModal(true)
+ try {
+ const result = await api.getDomainValuation(domain)
+ setValuationResult(result)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to get valuation')
+ setShowValuationModal(false)
+ } finally {
+ setValuatingDomain('')
+ }
+ }
+
+ const handleToggleNotify = async (domainId: number, currentNotify: boolean) => {
+ setTogglingNotifyId(domainId)
+ try {
+ await api.updateDomainNotify(domainId, !currentNotify)
+ const { fetchDomains } = useStore.getState()
+ await fetchDomains()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to update notification setting')
+ } finally {
+ setTogglingNotifyId(null)
+ }
+ }
+
+ const handleOpenEditPortfolio = (domain: PortfolioDomain) => {
+ setEditingPortfolioDomain(domain)
+ setEditPortfolioForm({
+ purchase_price: domain.purchase_price?.toString() || '',
+ purchase_date: domain.purchase_date || '',
+ registrar: domain.registrar || '',
+ renewal_date: domain.renewal_date || '',
+ renewal_cost: domain.renewal_cost?.toString() || '',
+ notes: domain.notes || '',
+ })
+ setShowEditPortfolioModal(true)
+ }
+
+ const handleSaveEditPortfolio = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!editingPortfolioDomain) return
+ setSavingEdit(true)
+ try {
+ await api.updatePortfolioDomain(editingPortfolioDomain.id, {
+ purchase_price: editPortfolioForm.purchase_price ? parseFloat(editPortfolioForm.purchase_price) : undefined,
+ purchase_date: editPortfolioForm.purchase_date || undefined,
+ registrar: editPortfolioForm.registrar || undefined,
+ renewal_date: editPortfolioForm.renewal_date || undefined,
+ renewal_cost: editPortfolioForm.renewal_cost ? parseFloat(editPortfolioForm.renewal_cost) : undefined,
+ notes: editPortfolioForm.notes || undefined,
+ })
+ setShowEditPortfolioModal(false)
+ setEditingPortfolioDomain(null)
+ showToast('Domain updated', 'success')
+ loadPortfolio()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to update domain')
+ } finally {
+ setSavingEdit(false)
+ }
+ }
+
+ const handleOpenSellModal = (domain: PortfolioDomain) => {
+ setSellingDomain(domain)
+ setSellForm({ sale_date: new Date().toISOString().split('T')[0], sale_price: '' })
+ setShowSellModal(true)
+ }
+
+ const handleSellDomain = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!sellingDomain || !sellForm.sale_price) return
+ setProcessingSale(true)
+ try {
+ await api.markDomainSold(sellingDomain.id, sellForm.sale_date, parseFloat(sellForm.sale_price))
+ setShowSellModal(false)
+ const soldDomainName = sellingDomain.domain
+ setSellingDomain(null)
+ showToast(`Marked ${soldDomainName} as sold 🎉`, 'success')
+ loadPortfolio()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to mark domain as sold')
+ } finally {
+ setProcessingSale(false)
+ }
+ }
+
+ const formatDate = (dateStr: string | null) => {
+ if (!dateStr) return 'Not checked'
+ const date = new Date(dateStr)
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
+ }
+
+ const formatExpirationDate = (dateStr: string | null) => {
+ if (!dateStr) return null
+ const date = new Date(dateStr)
+ const now = new Date()
+ const daysUntil = Math.ceil((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
+ if (daysUntil < 0) return { text: 'Expired', urgent: true }
+ if (daysUntil <= 7) return { text: `${daysUntil}d`, urgent: true }
+ if (daysUntil <= 30) return { text: `${daysUntil}d`, urgent: false }
+ return { text: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), urgent: false }
+ }
+
+ const formatCurrency = (value: number | null) => {
+ if (value === null) return '—'
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value)
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ if (!isAuthenticated) return null
+
+ const canAddMore = subscription ? subscription.domains_used < subscription.domain_limit : true
+ const availableCount = domains.filter(d => d.is_available).length
+ const expiringCount = domains.filter(d => {
+ if (!d.expiration_date) return false
+ const daysUntil = Math.ceil((new Date(d.expiration_date).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24))
+ return daysUntil <= 30 && daysUntil > 0
+ }).length
+
+ const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
+ const isProOrHigher = tierName === 'Professional' || tierName === 'Enterprise' || tierName === 'Trader' || tierName === 'Tycoon'
+ const isEnterprise = tierName === 'Enterprise' || tierName === 'Tycoon'
+
+ // Feature flags based on subscription
+ const hasPortfolio = (subscription?.portfolio_limit ?? 0) !== 0 || subscription?.features?.domain_valuation
+ const hasDomainValuation = subscription?.features?.domain_valuation ?? false
+ const hasExpirationTracking = subscription?.features?.expiration_tracking ?? false
+ const hasHistory = (subscription?.history_days ?? 0) > 0
+
+ return (
+
+ {/* Background Effects - matching landing page */}
+
+
+
+
+
+
+ {/* Header */}
+
+
+
Command Center
+
+ Your hunting ground.
+
+
+ Your domains. Your intel. Your edge.
+
+
+
+
+ {isEnterprise && }
+ {tierName}
+
+ {isProOrHigher ? (
+
+
+ Billing
+
+ ) : (
+
+
+ Upgrade
+
+ )}
+
+
+
+ {/* Tabs - Landing Page Style */}
+
+ setActiveTab('watchlist')}
+ className={clsx(
+ "flex items-center gap-2.5 px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300",
+ activeTab === 'watchlist'
+ ? "bg-accent text-background shadow-lg shadow-accent/20"
+ : "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
+ )}
+ >
+
+ Watchlist
+ {domains.length > 0 && (
+ {domains.length}
+ )}
+
+ {hasPortfolio ? (
+ setActiveTab('portfolio')}
+ className={clsx(
+ "flex items-center gap-2.5 px-6 py-3 text-sm font-medium rounded-xl transition-all duration-300",
+ activeTab === 'portfolio'
+ ? "bg-accent text-background shadow-lg shadow-accent/20"
+ : "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
+ )}
+ >
+
+ Portfolio
+ {portfolio.length > 0 && (
+ {portfolio.length}
+ )}
+
+ ) : (
+
+
+ Portfolio
+ Pro
+
+ )}
+
+
+ {error && (
+
+
+
{error}
+
setError(null)} className="text-danger hover:text-danger/80">
+
+ )}
+
+ {/* Watchlist Tab */}
+ {activeTab === 'watchlist' && (
+
+ {/* Stats Row - Landing Page Style */}
+
+
+
+
+
+
+
+
Tracked
+
{domains.length}
+
+
+
+
+
+
+
+
+
Available
+
{availableCount}
+
+
+
+
+
+
+
+
+
Monitoring
+
{domains.filter(d => d.notify_on_available).length}
+
+
+ {hasExpirationTracking && (
+
+
+
+
+
+
+
Expiring
+
{expiringCount}
+
+
+ )}
+
+
+ {/* Limit Warning */}
+ {!canAddMore && (
+
+
+
+
+
You've reached your domain limit
+
Upgrade to track more domains and unlock premium features
+
+
+
+
+ Upgrade Now
+
+
+ )}
+
+ {/* Add Domain */}
+
+
+
+
setNewDomain(e.target.value)}
+ placeholder="Add domain to watchlist..."
+ disabled={!canAddMore}
+ className="w-full pl-12 pr-4 py-3 bg-background-secondary border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 disabled:opacity-50 transition-all"
+ />
+
+
+ {adding ? : }
+ Add
+
+
+
+ {/* Domain Table */}
+ {domains.length === 0 ? (
+
+
+
+
+
No domains tracked yet
+
Add your first domain above to start monitoring
+
+ ) : (
+
+
+
+
+ Domain
+ Status
+ {hasExpirationTracking && (
+ Expiration
+ )}
+ Last Check
+ Actions
+
+
+
+ {domains.map((domain) => {
+ const exp = formatExpirationDate(domain.expiration_date)
+ const isMonitoring = domain.notify_on_available
+ return (
+
+
+
+
+
+ {isMonitoring && (
+
+ )}
+
+
{domain.name}
+
+
+
+
+ {domain.is_available ? 'Available' : isMonitoring ? 'Monitoring' : 'Registered'}
+
+
+ {hasExpirationTracking && (
+
+ {exp ? (
+
+ {exp.text}
+
+ ) : — }
+
+ )}
+
+
+ {formatDate(domain.last_checked)}
+
+
+
+
+ {hasHistory && (
+ loadDomainHistory(domain.id)} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/10 rounded-lg transition-all" title="History">
+
+
+ )}
+ {hasDomainValuation ? (
+ handleGetValuation(domain.name)} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/10 rounded-lg transition-all" title="Valuation">
+
+
+ ) : (
+
+
+
+ )}
+ handleToggleNotify(domain.id, domain.notify_on_available)}
+ disabled={togglingNotifyId === domain.id}
+ className={clsx(
+ "p-2 rounded-lg transition-all",
+ domain.notify_on_available
+ ? "text-accent hover:bg-accent/10"
+ : "text-foreground-subtle hover:text-foreground hover:bg-foreground/10"
+ )}
+ title={domain.notify_on_available ? "Monitoring active" : "Enable monitoring"}
+ >
+ {togglingNotifyId === domain.id ? : domain.notify_on_available ? : }
+
+ handleRefresh(domain.id)} disabled={refreshingId === domain.id} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/10 rounded-lg transition-all" title="Refresh">
+
+
+ handleDelete(domain.id)} className="p-2 text-foreground-subtle hover:text-danger hover:bg-danger/10 rounded-lg transition-all" title="Remove">
+
+
+
+
+
+ )
+ })}
+
+
+
+ )}
+
+ )}
+
+ {/* Portfolio Tab */}
+ {activeTab === 'portfolio' && (
+
+ {/* Portfolio Stats */}
+ {portfolioSummary && (
+
+
+
Total Value
+
{formatCurrency(portfolioSummary.total_value)}
+
+
+
Invested
+
{formatCurrency(portfolioSummary.total_invested)}
+
+
+
P/L
+
= 0 ? "text-accent" : "text-danger")}>
+ {portfolioSummary.unrealized_profit >= 0 ? '+' : ''}{formatCurrency(portfolioSummary.unrealized_profit)}
+
+
+
+
ROI
+
= 0 ? "text-accent" : "text-danger")}>
+ {portfolioSummary.overall_roi >= 0 ? '+' : ''}{portfolioSummary.overall_roi.toFixed(1)}%
+
+
+
+ )}
+
+ {/* Add Domain */}
+
setShowAddPortfolioModal(true)} className="flex items-center gap-2 px-5 py-3 bg-foreground text-background text-ui-sm font-medium rounded-xl hover:bg-foreground/90 transition-all">
+
+ Add Domain
+
+
+ {loadingPortfolio ? (
+
+ ) : portfolio.length === 0 ? (
+
+
+
+
+
Your portfolio is empty
+
Add domains you own to track their value and ROI
+
+ ) : (
+
+
+
+
+ Domain
+ Purchased
+ Value
+ Renewal
+ ROI
+ Actions
+
+
+
+ {portfolio.map((domain) => {
+ const roi = domain.roi
+ const renewal = formatExpirationDate(domain.renewal_date)
+ return (
+
+
+
+
+
+
{domain.domain}
+ {domain.status === 'sold' &&
Sold }
+ {domain.registrar &&
{domain.registrar}
}
+
+
+
+
+ {domain.purchase_price ? formatCurrency(domain.purchase_price) : '—'}
+ {domain.purchase_date && {new Date(domain.purchase_date).toLocaleDateString()}
}
+
+
+ {domain.estimated_value ? formatCurrency(domain.estimated_value) : '—'}
+
+
+ {renewal ? (
+
+ {renewal.text}
+
+ ) : — }
+
+
+ {roi !== null ? (
+ = 0 ? "text-accent" : "text-danger")}>
+ {roi >= 0 ? '+' : ''}{roi.toFixed(1)}%
+
+ ) : — }
+
+
+
+ {domain.status !== 'sold' && (
+ handleOpenSellModal(domain)} className="p-2 text-foreground-subtle hover:text-accent hover:bg-accent/10 rounded-lg transition-all" title="Mark as Sold">
+
+
+ )}
+ handleOpenEditPortfolio(domain)} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/10 rounded-lg transition-all" title="Edit">
+
+
+ handleRefreshPortfolioValue(domain.id)} disabled={refreshingPortfolioId === domain.id} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/10 rounded-lg transition-all" title="Refresh Value">
+
+
+ handleDeletePortfolioDomain(domain.id)} className="p-2 text-foreground-subtle hover:text-danger hover:bg-danger/10 rounded-lg transition-all" title="Remove">
+
+
+
+
+
+ )
+ })}
+
+
+
+ )}
+
+ )}
+
+
+
+ {/* Modals */}
+ {showAddPortfolioModal && (
+
+
+
+
+
Add to Portfolio
+ setShowAddPortfolioModal(false)} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/5 rounded-lg">
+
+
+
+ Domain *
+ setPortfolioForm({ ...portfolioForm, domain: e.target.value })} placeholder="example.com" required className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all" />
+
+
+
+ Registrar
+ setPortfolioForm({ ...portfolioForm, registrar: e.target.value })} placeholder="e.g., Porkbun" className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all" />
+
+
+
+ Notes
+ setPortfolioForm({ ...portfolioForm, notes: e.target.value })} placeholder="Add notes..." rows={3} className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all resize-none" />
+
+
+
setShowAddPortfolioModal(false)} className="flex-1 py-3 bg-foreground/5 text-foreground text-ui-sm font-medium rounded-xl hover:bg-foreground/10 transition-all">Cancel
+
+ {addingPortfolio ? : }
+ Add
+
+
+
+
+
+
+ )}
+
+ {showValuationModal && (
+
+
+
+
+
Domain Valuation
+ { setShowValuationModal(false); setValuationResult(null) }} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/5 rounded-lg">
+
+ {valuatingDomain ? (
+
+
+
Analyzing {valuatingDomain}...
+
+ ) : valuationResult ? (
+
+
+
{valuationResult.domain}
+
{formatCurrency(valuationResult.estimated_value)}
+
{valuationResult.confidence} confidence
+
+
+ {Object.entries(valuationResult.scores).map(([key, value]) => key !== 'overall' && (
+
+ ))}
+
+
+ {valuationResult.factors.has_numbers && Has numbers }
+ {valuationResult.factors.has_hyphens && Has hyphens }
+ {valuationResult.factors.is_dictionary_word && Dictionary word }
+ {valuationResult.factors.length} chars
+ .{valuationResult.factors.tld}
+
+
+ ) : null}
+
+
+
+ )}
+
+ {selectedDomainId && domainHistory && (
+
+
+
+
+
+
Check History
+
{domains.find(d => d.id === selectedDomainId)?.name}
+
+
{ setSelectedDomainId(null); setDomainHistory(null) }} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/5 rounded-lg">
+
+ {loadingHistory ? (
+
+ ) : domainHistory.length === 0 ? (
+
No history yet
+ ) : (
+
+ {domainHistory.map((check) => (
+
+
+
+
{check.is_available ? 'Available' : 'Registered'}
+
+
{formatDate(check.checked_at)}
+
+ ))}
+
+ )}
+
+
+
+ )}
+
+ {showEditPortfolioModal && editingPortfolioDomain && (
+
+
+
+
+
+
Edit Domain
+
{editingPortfolioDomain.domain}
+
+
{ setShowEditPortfolioModal(false); setEditingPortfolioDomain(null) }} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/5 rounded-lg">
+
+
+
+
+ Registrar
+ setEditPortfolioForm({ ...editPortfolioForm, registrar: e.target.value })} placeholder="e.g., Namecheap" className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all" />
+
+
+
+ Notes
+ setEditPortfolioForm({ ...editPortfolioForm, notes: e.target.value })} placeholder="Add notes..." rows={3} className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all resize-none" />
+
+
+ { setShowEditPortfolioModal(false); setEditingPortfolioDomain(null) }} className="flex-1 py-3 bg-foreground/5 text-foreground text-ui-sm font-medium rounded-xl hover:bg-foreground/10 transition-all">Cancel
+
+ {savingEdit ? : }
+ Save
+
+
+
+
+
+
+ )}
+
+ {showSellModal && sellingDomain && (
+
+
+
+
+
+
Mark as Sold
+
{sellingDomain.domain}
+
+
{ setShowSellModal(false); setSellingDomain(null) }} className="p-2 text-foreground-subtle hover:text-foreground hover:bg-foreground/5 rounded-lg">
+
+ {sellingDomain.estimated_value && (
+
+
+ Estimated Value
+ {formatCurrency(sellingDomain.estimated_value)}
+
+ {sellingDomain.purchase_price && (
+
+ Purchase Price
+ {formatCurrency(sellingDomain.purchase_price)}
+
+ )}
+
+ )}
+
+
+ Sale Price *
+ setSellForm({ ...sellForm, sale_price: e.target.value })} placeholder="Enter sale price..." required className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all" />
+
+
+ Sale Date
+ setSellForm({ ...sellForm, sale_date: e.target.value })} className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground focus:outline-none focus:border-accent/50 transition-all" />
+
+ {sellForm.sale_price && sellingDomain.purchase_price && (
+ = 0 ? "bg-accent/10" : "bg-danger/10")}>
+
+ Profit
+ = 0 ? "text-accent" : "text-danger")}>
+ {parseFloat(sellForm.sale_price) - sellingDomain.purchase_price >= 0 ? '+' : ''}{formatCurrency(parseFloat(sellForm.sale_price) - sellingDomain.purchase_price)}
+
+
+
+ )}
+
+ { setShowSellModal(false); setSellingDomain(null) }} className="flex-1 py-3 bg-foreground/5 text-foreground text-ui-sm font-medium rounded-xl hover:bg-foreground/10 transition-all">Cancel
+
+ {processingSale ? : }
+ Confirm
+
+
+
+
+
+
+ )}
+
+
+
+ {/* Toast Notification */}
+ {toast && (
+
+ )}
+
+ )
+}
+
+export default function DashboardPage() {
+ return (
+
+
+
+ }>
+
+
+ )
+}
diff --git a/frontend/src/app/intel/[tld]/metadata.ts b/frontend/src/app/discover/[tld]/metadata.ts
similarity index 94%
rename from frontend/src/app/intel/[tld]/metadata.ts
rename to frontend/src/app/discover/[tld]/metadata.ts
index 2735a8c..32a1b7c 100644
--- a/frontend/src/app/intel/[tld]/metadata.ts
+++ b/frontend/src/app/discover/[tld]/metadata.ts
@@ -27,7 +27,7 @@ export async function generateTLDMetadata(tld: string, price?: number, trend?: n
openGraph: {
title,
description,
- url: `${siteUrl}/intel/${tld}`,
+ url: `${siteUrl}/discover/${tld}`,
type: 'article',
images: [
{
@@ -45,7 +45,7 @@ export async function generateTLDMetadata(tld: string, price?: number, trend?: n
images: [`${siteUrl}/api/og/tld?tld=${tld}&price=${price || 0}&trend=${trend || 0}`],
},
alternates: {
- canonical: `${siteUrl}/intel/${tld}`,
+ canonical: `${siteUrl}/discover/${tld}`,
},
}
}
@@ -80,7 +80,7 @@ export function generateTLDStructuredData(tld: string, price: number, trend: num
dateModified: new Date().toISOString(),
mainEntityOfPage: {
'@type': 'WebPage',
- '@id': `${siteUrl}/intel/${tld}`,
+ '@id': `${siteUrl}/discover/${tld}`,
},
},
// Product (Domain TLD)
@@ -130,14 +130,14 @@ export function generateTLDStructuredData(tld: string, price: number, trend: num
{
'@type': 'ListItem',
position: 2,
- name: 'Intel',
- item: `${siteUrl}/intel`,
+ name: 'Discover',
+ item: `${siteUrl}/discover`,
},
{
'@type': 'ListItem',
position: 3,
name: `.${tldUpper}`,
- item: `${siteUrl}/intel/${tld}`,
+ item: `${siteUrl}/discover/${tld}`,
},
],
},
diff --git a/frontend/src/app/intel/[tld]/page.tsx b/frontend/src/app/discover/[tld]/page.tsx
similarity index 72%
rename from frontend/src/app/intel/[tld]/page.tsx
rename to frontend/src/app/discover/[tld]/page.tsx
index 86e94a1..7bbceac 100644
--- a/frontend/src/app/intel/[tld]/page.tsx
+++ b/frontend/src/app/discover/[tld]/page.tsx
@@ -25,6 +25,7 @@ import {
Shield,
Zap,
AlertTriangle,
+ Sparkles,
} from 'lucide-react'
import Link from 'next/link'
import clsx from 'clsx'
@@ -112,14 +113,14 @@ const RELATED_TLDS: Record = {
type ChartPeriod = '1M' | '3M' | '1Y' | 'ALL'
-// Shimmer component for unauthenticated users
+// Shimmer component
function Shimmer({ className }: { className?: string }) {
return (
)
}
@@ -141,11 +142,11 @@ function PriceChart({
if (!isAuthenticated) {
return (
-
+
-
- Sign in to view price history
+
+ Sign in to view price history
)
@@ -153,7 +154,7 @@ function PriceChart({
if (data.length === 0) {
return (
-
+
No price history available
)
@@ -226,6 +227,7 @@ function PriceChart({
strokeOpacity="0.05"
strokeWidth="0.2"
vectorEffect="non-scaling-stroke"
+ className="text-white/20"
/>
))}
@@ -268,7 +270,7 @@ function PriceChart({
{/* Hover dot */}
{hoveredIndex !== null && containerRef.current && (
-
+
${data[hoveredIndex].price.toFixed(2)}
-
+
{new Date(data[hoveredIndex].date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
@@ -316,7 +318,7 @@ function DomainResultCard({
return (
{result.is_available ? (
@@ -335,9 +337,9 @@ function DomainResultCard({
)}
-
{result.domain}
+
{result.domain}
{result.is_available ? 'Available for registration' : 'Already registered'}
@@ -349,7 +351,7 @@ function DomainResultCard({
-
+
Register from ${cheapestPrice.toFixed(2)} /yr
@@ -357,23 +359,23 @@ function DomainResultCard({
href={`${registrarUrl}${result.domain}`}
target="_blank"
rel="noopener noreferrer"
- className="inline-flex items-center gap-2 px-5 py-2.5 bg-accent text-background text-ui-sm font-medium rounded-xl hover:bg-accent-hover transition-all"
+ className="inline-flex items-center gap-2 px-5 py-2.5 bg-accent text-[#020202] text-sm font-bold rounded-lg hover:bg-accent-hover transition-all uppercase tracking-wide"
>
Register at {cheapestRegistrar}
) : (
-
+
{result.registrar && (
-
+
Registrar: {result.registrar}
)}
{result.expiration_date && (
-
+
Expires: {new Date(result.expiration_date).toLocaleDateString()}
)}
@@ -383,7 +385,7 @@ function DomainResultCard({
@@ -604,7 +606,7 @@ export default function TldDetailPage() {
if (loading || authLoading) {
return (
-
+
@@ -622,21 +624,21 @@ export default function TldDetailPage() {
if (error || !details) {
return (
-
+
-
-
+
+
-
TLD Not Found
-
{error || `The TLD .${tld} could not be found.`}
+
TLD Not Found
+
{error || `The TLD .${tld} could not be found.`}
- Back to Intel
+ Back to Discover
@@ -646,24 +648,32 @@ export default function TldDetailPage() {
}
return (
-
- {/* Subtle ambient */}
-
-
+
+ {/* Background Effects */}
+
-
+
{/* Breadcrumb */}
-
-
- Intel
+
+
+ Discover
-
- .{details.tld}
+
+ .{details.tld}
{/* Hero */}
@@ -671,46 +681,46 @@ export default function TldDetailPage() {
{/* Left: TLD Info */}
-
+
.{details.tld}
{getTrendIcon(details.trend)}
{details.trend === 'up' ? 'Rising' : details.trend === 'down' ? 'Falling' : 'Stable'}
-
{details.description}
-
{details.trend_reason}
+
{details.description}
+
{details.trend_reason}
{/* Quick Stats - All data from table */}
-
Buy (1y)
+
Buy (1y)
{isAuthenticated ? (
-
${details.pricing.min.toFixed(2)}
+
${details.pricing.min.toFixed(2)}
) : (
)}
-
Renew (1y)
+
Renew (1y)
{isAuthenticated ? (
-
+
${details.min_renewal_price.toFixed(2)}
{renewalInfo?.isTrap && (
@@ -724,16 +734,16 @@ export default function TldDetailPage() {
)}
-
1y Change
+
1y Change
{isAuthenticated ? (
0 ? "text-orange-400" :
details.price_change_1y < 0 ? "text-accent" :
- "text-foreground"
+ "text-white"
)}>
{details.price_change_1y > 0 ? '+' : ''}{details.price_change_1y.toFixed(0)}%
@@ -742,16 +752,16 @@ export default function TldDetailPage() {
)}
-
3y Change
+
3y Change
{isAuthenticated ? (
0 ? "text-orange-400" :
details.price_change_3y < 0 ? "text-accent" :
- "text-foreground"
+ "text-white"
)}>
{details.price_change_3y > 0 ? '+' : ''}{details.price_change_3y.toFixed(0)}%
@@ -763,10 +773,10 @@ export default function TldDetailPage() {
{/* Risk Assessment */}
{isAuthenticated && (
-
-
+
+
-
Risk Assessment
+
Risk Assessment
{getRiskBadge()}
@@ -775,16 +785,16 @@ export default function TldDetailPage() {
{/* Right: Price Card */}
-
+
{isAuthenticated ? (
<>
-
+
${details.pricing.min.toFixed(2)}
- /yr
+ /yr
-
+
Cheapest at {details.cheapest_registrar}
@@ -793,7 +803,7 @@ export default function TldDetailPage() {
href={getRegistrarUrl(details.cheapest_registrar, `example.${tld}`)}
target="_blank"
rel="noopener noreferrer"
- className="flex items-center justify-center gap-2 w-full py-3.5 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
+ className="flex items-center justify-center gap-2 w-full py-3.5 bg-accent text-[#020202] font-bold rounded-lg hover:bg-accent-hover transition-all uppercase tracking-wide text-sm"
>
Register Domain
@@ -801,10 +811,10 @@ export default function TldDetailPage() {
{savings && savings.amount > 0.5 && (
-
+
-
+
Save ${savings.amount.toFixed(2)} /yr vs {savings.expensiveName}
@@ -817,7 +827,7 @@ export default function TldDetailPage() {
Sign in to View Prices
@@ -834,7 +844,7 @@ export default function TldDetailPage() {
Renewal Trap Detected
-
+
The renewal price (${renewalInfo.renewal.toFixed(2)}) is {renewalInfo.ratio.toFixed(1)}x higher than the registration price (${renewalInfo.registration.toFixed(2)}).
Consider the total cost of ownership before registering.
@@ -846,22 +856,22 @@ export default function TldDetailPage() {
-
Price History
+ Price History
{isAuthenticated && !hasPriceHistory && (
- Pro
+ Pro
)}
{hasPriceHistory && (
-
+
{(['1M', '3M', '1Y', 'ALL'] as ChartPeriod[]).map(period => (
setChartPeriod(period)}
className={clsx(
- "px-3 py-1.5 text-ui-sm font-medium rounded-md transition-all",
+ "px-3 py-1.5 text-xs font-medium rounded transition-all",
chartPeriod === period
- ? "bg-foreground text-background"
- : "text-foreground-muted hover:text-foreground"
+ ? "bg-white text-black"
+ : "text-white/40 hover:text-white"
)}
>
{period}
@@ -871,26 +881,26 @@ export default function TldDetailPage() {
)}
-
+
{!isAuthenticated ? (
-
+
-
- Sign in to view price history
-
+
+ Sign in to view price history
+
Sign in →
) : !hasPriceHistory ? (
-
+
- Price history requires Trader or Tycoon plan
-
-
+ Price history requires Trader or Tycoon plan
+
+
Upgrade to Unlock
@@ -904,21 +914,21 @@ export default function TldDetailPage() {
/>
{filteredHistory.length > 0 && (
-
-
+
+
{new Date(filteredHistory[0]?.date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
-
+
- High
- ${chartStats.high.toFixed(2)}
+ High
+ ${chartStats.high.toFixed(2)}
- Low
- ${chartStats.low.toFixed(2)}
+ Low
+ ${chartStats.low.toFixed(2)}
-
Today
+
Today
)}
>
@@ -928,10 +938,10 @@ export default function TldDetailPage() {
{/* Domain Search */}
-
+
Check .{details.tld} Availability
-
+
setDomainSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleDomainCheck()}
placeholder="Enter domain name"
- className="w-full px-4 py-3.5 pr-20 bg-background border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent transition-all"
+ className="w-full px-4 py-3.5 pr-20 bg-[#0a0a0a] border border-white/10 rounded-xl text-white placeholder:text-white/30 focus:outline-none focus:ring-1 focus:ring-accent/50 focus:border-accent/50 transition-all"
/>
-
+
.{tld}
{checkingDomain ? (
@@ -974,76 +984,76 @@ export default function TldDetailPage() {
{/* Registrar Comparison */}
- Compare Registrars
+ Compare Registrars
{isAuthenticated ? (
-
+
-
-
+
+
Registrar
-
+
Register
-
+
Renew
-
+
Transfer
-
+
-
+
{details.registrars.map((registrar, i) => {
const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5
const isBestValue = i === 0 && !hasRenewalTrap
return (
-
+
- {registrar.name}
+ {registrar.name}
{isBestValue && (
- Best
+ Best Value
)}
{i === 0 && hasRenewalTrap && (
- Cheap Start
+ Promo
)}
-
+
${registrar.registration_price.toFixed(2)}
-
+
)}
-
+
${registrar.transfer_price.toFixed(2)}
-
+
Visit
-
+
@@ -1085,7 +1095,7 @@ export default function TldDetailPage() {
) : (
-
+
{[1, 2, 3, 4].map(i => (
@@ -1096,13 +1106,13 @@ export default function TldDetailPage() {
))}
-
+
-
-
Sign in to compare registrar prices
+
+
Sign in to compare registrar prices
Join the Hunt
@@ -1114,22 +1124,22 @@ export default function TldDetailPage() {
{/* TLD Info */}
- About .{details.tld}
+ About .{details.tld}
-
-
-
Registry
-
{details.registry}
+
+
+
Registry
+
{details.registry}
-
-
-
Introduced
-
{details.introduced || 'Unknown'}
+
+
+
Introduced
+
{details.introduced || 'Unknown'}
-
-
-
Type
-
{details.type}
+
+
+
Type
+
{details.type}
@@ -1137,19 +1147,19 @@ export default function TldDetailPage() {
{/* Related TLDs */}
{relatedTlds.length > 0 && (
- Similar Extensions
+ Similar Extensions
{relatedTlds.map(related => (
-
+
.{related.tld}
{isAuthenticated ? (
-
+
from ${related.price.toFixed(2)}/yr
) : (
@@ -1162,16 +1172,16 @@ export default function TldDetailPage() {
)}
{/* CTA */}
-
-
+
+
Track .{details.tld} Domains
-
+
Monitor specific domains and get instant notifications when they become available.
{isAuthenticated ? 'Go to Command Center' : 'Start Monitoring Free'}
@@ -1185,3 +1195,4 @@ export default function TldDetailPage() {
)
}
+
diff --git a/frontend/src/app/intel/page.tsx b/frontend/src/app/discover/page.tsx
similarity index 68%
rename from frontend/src/app/intel/page.tsx
rename to frontend/src/app/discover/page.tsx
index 9637c8e..71b22c9 100644
--- a/frontend/src/app/intel/page.tsx
+++ b/frontend/src/app/discover/page.tsx
@@ -15,6 +15,7 @@ import {
Globe,
AlertTriangle,
ArrowUpDown,
+ Sparkles,
} from 'lucide-react'
import Link from 'next/link'
import clsx from 'clsx'
@@ -52,7 +53,7 @@ interface PaginationData {
has_more: boolean
}
-// TLDs that are shown completely to non-authenticated users (gemäß pounce_public.md)
+// TLDs that are shown completely to non-authenticated users
const PUBLIC_PREVIEW_TLDS = ['com', 'net', 'org']
// Sparkline component
@@ -64,7 +65,7 @@ function Sparkline({ trend }: { trend: number }) {
{isNeutral ? (
-
+
) : isPositive ? (
+
+
+ )
+}
+
+export default function DiscoverPage() {
const { isAuthenticated, checkAuth, isLoading: authLoading } = useStore()
const [tlds, setTlds] = useState([])
const [trending, setTrending] = useState([])
@@ -153,8 +166,6 @@ export default function IntelPage() {
}
}
- // Check if TLD should show full data for non-authenticated users
- // Gemäß pounce_public.md: .com, .net, .org are fully visible
const isPublicPreviewTld = (tld: TldData) => {
return PUBLIC_PREVIEW_TLDS.includes(tld.tld.toLowerCase())
}
@@ -198,85 +209,92 @@ export default function IntelPage() {
if (authLoading) {
return (
-
+
)
}
return (
-
- {/* Background Effects */}
-
-
-
+
+ {/* Cinematic Background - Matches Landing Page */}
+
+ {/* Fine Noise */}
+
+
+ {/* Architectural Grid - Ultra fine */}
+
+ {/* Ambient Light - Very Subtle */}
+
-
+
- {/* Header - gemäß pounce_public.md: "TLD Market Inflation Monitor" */}
+ {/* Header */}
-
-
-
Real-time Market Data
+
+
+ Domain Intelligence
-
- TLD Market
- Inflation Monitor
+
+ Discover
+ Market Opportunities
-
+
Don't fall for promo prices. See renewal costs, spot traps, and track price trends across {pagination.total}+ extensions.
- {/* Top Movers Cards */}
+ {/* Feature Pills */}
-
+
-
Renewal Trap Detection
+
Renewal Trap Detection
-
+
-
-
-
+
+
+
-
Risk Levels
+
Risk Levels
-
+
- 1y/3y Trends
+ 1y/3y Trends
{/* Login Banner for non-authenticated users */}
{!isAuthenticated && (
-
-
+
+
+
-
+
-
Stop overpaying. Know the true costs.
-
+
Stop overpaying. Know the true costs.
+
Unlock renewal prices and risk analysis for all {pagination.total}+ TLDs.
Start Hunting
@@ -287,7 +305,7 @@ export default function IntelPage() {
{/* Trending Section - Top Movers */}
{trending.length > 0 && (
-
+
Top Movers
@@ -295,28 +313,28 @@ export default function IntelPage() {
{trending.map((item) => (
- .{item.tld}
+ .{item.tld}
0
- ? "text-[#f97316] bg-[#f9731615]"
- : "text-accent bg-accent-muted"
+ ? "text-orange-400 bg-orange-400/10 border-orange-400/20"
+ : "text-accent bg-accent/10 border-accent/20"
)}>
{item.price_change > 0 ? '+' : ''}{item.price_change.toFixed(1)}%
-
+
{item.reason}
-
-
+
+ Current Price
+
${item.current_price.toFixed(2)}/yr
-
))}
@@ -327,7 +345,7 @@ export default function IntelPage() {
{/* Search & Sort Controls */}
-
+
{searchQuery && (
@@ -347,7 +365,7 @@ export default function IntelPage() {
setSearchQuery('')
setPage(0)
}}
- className="absolute right-4 top-1/2 -translate-y-1/2 p-1 text-foreground-subtle hover:text-foreground transition-colors"
+ className="absolute right-4 top-1/2 -translate-y-1/2 p-1 text-white/30 hover:text-white transition-colors"
>
@@ -361,34 +379,28 @@ export default function IntelPage() {
setSortBy(e.target.value)
setPage(0)
}}
- className="appearance-none pl-4 pr-10 py-3 bg-background-secondary/50 border border-border rounded-xl
- text-body text-foreground focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent
+ className="appearance-none pl-4 pr-10 py-3.5 bg-white/[0.03] border border-white/[0.08] rounded-xl
+ text-white focus:outline-none focus:ring-1 focus:ring-accent/50 focus:border-accent/50
transition-all cursor-pointer min-w-[180px]"
>
-
Most Popular
-
Alphabetical
-
Price: Low → High
-
Price: High → Low
+
Most Popular
+
Alphabetical
+
Price: Low → High
+
Price: High → Low
-
+
- {/* TLD Table - gemäß pounce_public.md:
- - .com, .net, .org: vollständig sichtbar
- - Alle anderen: Buy Price + Trend sichtbar, Renewal + Risk geblurrt */}
+ {/* TLD Table */}
tld.tld}
loading={loading}
onRowClick={(tld) => {
- if (isAuthenticated) {
- window.location.href = `/intel/${tld.tld}`
- } else {
- window.location.href = `/login?redirect=/intel/${tld.tld}`
- }
+ window.location.href = `/discover/${tld.tld}`
}}
- emptyIcon={ }
+ emptyIcon={ }
emptyTitle="No TLDs found"
emptyDescription={searchQuery ? `No TLDs matching "${searchQuery}"` : "Check back later for TLD data"}
columns={[
@@ -398,7 +410,7 @@ export default function IntelPage() {
width: '100px',
render: (tld) => (
-
+
.{tld.tld}
@@ -409,9 +421,8 @@ export default function IntelPage() {
header: 'Current Price',
align: 'right',
width: '120px',
- // Buy price is visible for all TLDs (gemäß pounce_public.md)
render: (tld) => (
-
+
${tld.min_registration_price.toFixed(2)}
),
@@ -421,7 +432,6 @@ export default function IntelPage() {
header: 'Trend (1y)',
width: '100px',
hideOnMobile: true,
- // Trend is visible for all TLDs
render: (tld) => {
const change = tld.price_change_1y || 0
return (
@@ -429,7 +439,7 @@ export default function IntelPage() {
0 ? "text-orange-400" : change < 0 ? "text-accent" : "text-foreground-muted"
+ change > 0 ? "text-orange-400" : change < 0 ? "text-accent" : "text-white/30"
)}>
{change > 0 ? '+' : ''}{change.toFixed(0)}%
@@ -442,21 +452,19 @@ export default function IntelPage() {
header: 'Renewal Price',
align: 'right',
width: '130px',
- // Renewal price: visible for .com/.net/.org OR authenticated users
- // Geblurrt/Locked für alle anderen
render: (tld) => {
const showData = isAuthenticated || isPublicPreviewTld(tld)
if (!showData) {
return (
-
-
$XX.XX
-
+
+ $XX.XX
+
)
}
return (
-
+
${tld.min_renewal_price?.toFixed(2) || '—'}
{getRenewalTrap(tld)}
@@ -469,18 +477,16 @@ export default function IntelPage() {
header: 'Risk Level',
align: 'center',
width: '140px',
- // Risk: visible for .com/.net/.org OR authenticated users
- // Geblurrt/Locked für alle anderen
render: (tld) => {
const showData = isAuthenticated || isPublicPreviewTld(tld)
if (!showData) {
return (
-
-
- Hidden
+
+
+ Hidden
-
+
)
}
@@ -493,7 +499,7 @@ export default function IntelPage() {
align: 'right',
width: '80px',
render: () => (
-
+
),
},
]}
@@ -501,24 +507,24 @@ export default function IntelPage() {
{/* Pagination */}
{!loading && pagination.total > pagination.limit && (
-
+
setPage(Math.max(0, page - 1))}
disabled={page === 0}
- className="px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground
- bg-foreground/5 hover:bg-foreground/10 rounded-lg
+ className="px-4 py-2 text-sm font-medium text-white/50 hover:text-white
+ bg-white/5 hover:bg-white/10 rounded-lg border border-white/5
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Previous
-
+
Page {currentPage} of {totalPages}
setPage(page + 1)}
disabled={!pagination.has_more}
- className="px-4 py-2 text-sm font-medium text-foreground-muted hover:text-foreground
- bg-foreground/5 hover:bg-foreground/10 rounded-lg
+ className="px-4 py-2 text-sm font-medium text-white/50 hover:text-white
+ bg-white/5 hover:bg-white/10 rounded-lg border border-white/5
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Next
@@ -529,10 +535,10 @@ export default function IntelPage() {
{/* Stats */}
{!loading && (
-
+
{searchQuery
? `Found ${pagination.total} TLDs matching "${searchQuery}"`
- : `${pagination.total} TLDs tracked`
+ : `${pagination.total} TLDs tracked in real-time`
}
diff --git a/frontend/src/app/intelligence/page.tsx b/frontend/src/app/intelligence/page.tsx
new file mode 100755
index 0000000..88cf19b
--- /dev/null
+++ b/frontend/src/app/intelligence/page.tsx
@@ -0,0 +1,26 @@
+'use client'
+
+import { useEffect } from 'react'
+import { useRouter } from 'next/navigation'
+
+/**
+ * Redirect /intelligence to /tld-pricing
+ * This page is kept for backwards compatibility
+ */
+export default function IntelligenceRedirect() {
+ const router = useRouter()
+
+ useEffect(() => {
+ router.replace('/tld-pricing')
+ }, [router])
+
+ return (
+
+
+
+
Redirecting to TLD Pricing...
+
+
+ )
+}
+
diff --git a/frontend/src/app/market/page.tsx b/frontend/src/app/market/page.tsx
index 9a928da..506dfc7 100644
--- a/frontend/src/app/market/page.tsx
+++ b/frontend/src/app/market/page.tsx
@@ -150,7 +150,7 @@ export default function MarketPage() {
const [searchQuery, setSearchQuery] = useState('')
const [selectedPlatform, setSelectedPlatform] = useState('All')
const [maxBid, setMaxBid] = useState('')
-
+
useEffect(() => {
checkAuth()
loadAuctions()
@@ -268,7 +268,7 @@ export default function MarketPage() {
)
}
-
+
return (
{/* Background Effects */}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index 6e5d1eb..960fd7d 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -34,17 +34,21 @@ import {
AlertTriangle,
Briefcase,
Coins,
+ Layers,
+ ArrowUpRight,
+ ShieldCheck,
+ Smartphone,
+ Globe2,
+ Server,
+ Database,
+ Cpu,
+ Network,
+ Share2,
+ Key
} from 'lucide-react'
import Link from 'next/link'
import clsx from 'clsx'
-interface TrendingTld {
- tld: string
- reason: string
- current_price: number
- price_change: number
-}
-
interface HotAuction {
domain: string
current_bid: number
@@ -52,37 +56,29 @@ interface HotAuction {
platform: string
}
-// Shimmer for loading states
-function Shimmer({ className }: { className?: string }) {
- return (
-
- )
-}
-
-// Animated counter
+// Animated counter with easing
function AnimatedNumber({ value, suffix = '' }: { value: number, suffix?: string }) {
const [count, setCount] = useState(0)
useEffect(() => {
- const duration = 2000
+ const duration = 2500
const steps = 60
const increment = value / steps
- let current = 0
+
+ let startTime = Date.now()
const timer = setInterval(() => {
- current += increment
- if (current >= value) {
+ const now = Date.now()
+ const progress = Math.min((now - startTime) / duration, 1)
+ const easeOutQuart = 1 - Math.pow(1 - progress, 4)
+
+ if (progress < 1) {
+ setCount(Math.floor(value * easeOutQuart))
+ } else {
setCount(value)
clearInterval(timer)
- } else {
- setCount(Math.floor(current))
}
- }, duration / steps)
+ }, 1000 / 60)
return () => clearInterval(timer)
}, [value])
@@ -90,36 +86,44 @@ function AnimatedNumber({ value, suffix = '' }: { value: number, suffix?: string
return <>{count.toLocaleString()}{suffix}>
}
-// Live Market Ticker
+// High-end Live Market Ticker - Monochrome & Technical
function MarketTicker({ auctions }: { auctions: HotAuction[] }) {
const tickerRef = useRef
(null)
- // Duplicate items for seamless loop
- const items = [...auctions, ...auctions]
-
if (auctions.length === 0) return null
+ // Create enough duplicates to fill even large screens
+ const items = [...auctions, ...auctions, ...auctions]
+
return (
-
-
- {items.map((auction, i) => (
-
-
-
-
{auction.domain}
+
+
+
+
+
+
+ {items.map((auction, i) => (
+
+
+
+
+ {auction.domain}
+
+
+
+ ${auction.current_bid.toLocaleString()}
+ {auction.time_remaining}
+
-
${auction.current_bid}
-
{auction.time_remaining}
-
{auction.platform}
-
- ))}
+ ))}
+
)
@@ -127,9 +131,7 @@ function MarketTicker({ auctions }: { auctions: HotAuction[] }) {
export default function HomePage() {
const { checkAuth, isLoading, isAuthenticated } = useStore()
- const [trendingTlds, setTrendingTlds] = useState
([])
const [hotAuctions, setHotAuctions] = useState([])
- const [loadingTlds, setLoadingTlds] = useState(true)
const [loadingAuctions, setLoadingAuctions] = useState(true)
useEffect(() => {
@@ -139,198 +141,557 @@ export default function HomePage() {
const fetchData = async () => {
try {
- const [trending, auctions] = await Promise.all([
- api.getTrendingTlds(),
- api.getHotAuctions(8).catch(() => [])
- ])
- setTrendingTlds(trending.trending.slice(0, 4))
+ const auctions = await api.getHotAuctions(8).catch(() => [])
setHotAuctions(auctions.slice(0, 8))
} catch (error) {
console.error('Failed to fetch data:', error)
} finally {
- setLoadingTlds(false)
setLoadingAuctions(false)
}
}
if (isLoading) {
return (
-
-
+
)
}
- const getTrendIcon = (priceChange: number) => {
- if (priceChange > 0) return
- if (priceChange < 0) return
- return
- }
-
return (
-
- {/* Background Effects */}
-
-
-
+
+ {/* Cinematic Background - Architectural & Fine */}
+
+ {/* Fine Noise */}
+
+
+ {/* Architectural Grid - Ultra fine */}
+
+ {/* Ambient Light - Very Subtle & Localized */}
+
- {/* Hero Section - "Bloomberg meets Apple" */}
-
-
-
- {/* Puma Logo */}
-
-
-
- {/* Glow ring */}
-
+ {/* HERO SECTION: Brutally Catchy & Noble */}
+
+
+
+
+ {/* Left: Typography & Brand */}
+
+ {/* Brand Seal - COLORFUL AGAIN */}
+
-
-
- {/* Main Headline - kompakter */}
-
-
- The market never sleeps.
-
-
- You should.
-
-
-
- {/* Subheadline - gemäß pounce_public.md */}
-
- Domain Intelligence for Investors.{' '}
- Scan, track, and trade digital assets.
-
-
- {/* Tagline */}
-
- Don't guess. Know.
-
-
- {/* Domain Checker - PROMINENT */}
-
-
- {/* Glow effect behind search */}
-
-
+
+ {/* Headline - Improved Spacing & Color */}
+
+
+ The market
+
+
+ never sleeps.
+
+
+ You should.
+
+
+
+ {/* Subline & Stats */}
+
+
+ Institutional-grade intelligence for the digital asset economy.
+ Scan. Track. Trade. Yield.
+
+
+ {/* Stats Grid - Moved here */}
+
+
+
+
24/7
+
Live Monitoring
+
+
+
+
- {/* Trust Indicators */}
-
-
-
-
- Live Auctions
-
-
-
- Instant Alerts
-
-
-
-
Price Intel
+ {/* Right: The Artifact (Domain Checker) - HIGHER VISIBILITY */}
+
+ {/* Stronger Glow for Visibility */}
+
+
+
+
+
+
+
+
+
+
+ {/* Subtle Background within card */}
+
+
+
+
+
+
+ Terminal Access
+
+
+
+
+ {/* The Checker Component */}
+
+
+
+
+
+ SECURE CONNECTION
+ V2.0.4 [STABLE]
+
+
+
- {/* Live Market Ticker */}
+ {/* Ticker - Minimalist */}
{!loadingAuctions && hotAuctions.length > 0 && (
)}
- {/* Live Market Teaser - gemäß pounce_public.md */}
+ {/* THE PARADIGM SHIFT - Problem / Solution */}
+
+
+
+
+
The Problem
+
+ 99% of domains are dead capital.
+
+
+
+ Most investors guess. They buy domains based on "sound" and pay renewal fees for years, hoping for a random buyer.
+
+
+ Traditional marketplaces are flooded with spam, parking pages generate pennies, and valuable assets gather dust.
+
+
+
+
+
+
+
The Pounce Standard
+
Asset Class V2.0
+
+
+
+ Data-Driven: Don't guess price. Know the valuation.
+
+
+
+ Liquid: Instant settlement. No middlemen.
+
+
+
+ Yield-Bearing: Intent Routing turns traffic into revenue.
+
+
+
+
+
+
+
+
+ {/* THE THREE PILLARS + YIELD - Architectural Layout */}
+
+
+ {/* Section Header */}
+
+
+ Core Architecture
+
+ The Domain Lifecycle
+ Engine.
+
+
+
+ // INTELLIGENCE_LAYER_ACTIVE
+ // MARKET_PROTOCOL_READY
+ // YIELD_GENERATION_STANDBY
+
+
+
+
+
+ {/* 1. INTELLIGENCE (Discover) */}
+
+
+
+
+
+
+ Module 01
+
+
Intelligence
+
+ "Find the Asset." We scan 886+ TLDs in real-time to uncover hidden opportunities before the market reacts.
+
+
+
+
+
+
+
+
Global Scan
+
Zone file analysis & expiration monitoring.
+
+
+
+
+
+
Valuation AI
+
Instant fair-market value estimation.
+
+
+
+
+
+
+ {/* 2. MARKET (Acquire) */}
+
+
+
+
+
+
+ Module 02
+
+
Market
+
+ "Secure the Asset." Direct access to liquidity. A verified marketplace where assets move instantly, securely, and with 0% commission fees.
+
+
+
+
+
+
+
+
Verified Owners
+
Mandatory DNS verification. No fakes.
+
+
+
+
+
+
Direct Execution
+
P2P transfers without middlemen.
+
+
+
+
+
+
+ {/* 3. YIELD (Monetize) */}
+
+
+
+
+
+
+ Module 03
+
+
Yield
+
+ "Let the Asset Work." Our "Intent Routing" engine transforms idle domains into active revenue generators via automated traffic monetization.
+
+
+
+
+
+
+
+
Intent Routing
+
Traffic directed to high-value partners.
+
+
+
+
+
+
Passive Income
+
Monthly payouts from your portfolio.
+
+
+
+
+
+
+
+
+
+
+ {/* DEEP DIVE: YIELD - The "Unicorn" Feature */}
+
+
+
+
+
The Endgame
+
Intent Routing™
+
+ Domains usually park for pennies. We route them for dollars.
+ We analyze the intent behind a domain (e.g. "kredit.ch") and route traffic directly to high-paying affiliates.
+
+
+
+
+
+
+
+
+
1. Connect
+
User points NS records to Pounce. No coding required.
+
+
+
+
+
+
2. Analyze
+
AI detects intent. "zahnarzt-zh" -> Intent: "Dentist Booking".
+
+
+
+
+
+
3. Route
+
Traffic is sent to partners (e.g. Doctolib). You get paid per lead.
+
+
+
+
+
+ {/* DEEP DIVE: MARKET - The "Velvet Rope" */}
+
+
+
+
+
+
+
+
zurich-immo.ch
+
$950
+
+
+
+
Source
+
Pounce Direct
+
+
+ Seller Verified
+ Yes (DNS Check)
+
+
+ Commission
+ 0%
+
+
+
+
+ Contact Seller
+
+
+
+
+
+
+
Exclusive Exchange
+
The Velvet Rope Strategy.
+
+ We don't run an open flea market. We run an exclusive club.
+ Buying is open to everyone, but selling is reserved for members.
+
+
+
+
+
+
Zero Spam
+
Gatekeeper technology filters 99% of junk domains automatically.
+
+
+
+
+
+
Verified Owners
+
Sellers must verify ownership via DNS before listing.
+
+
+
+
+
+
0% Commission
+
Members keep 100% of the sale price. Direct settlement.
+
+
+
+
+
+
+
+ {/* PRICING TEASER - "Access Levels" */}
+
+
+
Clearance Levels
+
+
+ {/* Scout */}
+
+
Scout
+
$0/mo
+
+ • Market Overview
+ • Basic Search
+ • 5 Watchlist Items
+
+
+ Start Free
+
+
+
+ {/* Trader - Highlight */}
+
+
Recommended
+
Trader
+
$9/mo
+
+ Clean Feed (No Spam)
+ Renewal Price Intel
+ Sell Domains (0% Fee)
+ 50 Watchlist Items
+
+
+ Upgrade
+
+
+
+ {/* Tycoon */}
+
+
Tycoon
+
$29/mo
+
+ Full Portfolio Monitor
+ Priority Alerts (10m)
+ 500 Watchlist Items
+ Featured Listings
+
+
+ Go Professional
+
+
+
+
+
+
+ {/* LIVE FEED TEASER - The "Terminal" Look */}
{!isAuthenticated && !loadingAuctions && hotAuctions.length > 0 && (
-
-
-
-
-
- Live Market Preview
-
-
- View All
+
+
+
+
+
+
Market Activity
+
LIVE_FEED_V2.0 // ENCRYPTED
+
+
+ View All Markets
-
- {/* Mini Table with blur on last row */}
-
-
-
-
- Domain
- Price
- Time Left
-
-
-
- {hotAuctions.slice(0, 4).map((auction, idx) => (
-
-
- {auction.domain}
-
-
- ${auction.current_bid}
-
-
- {auction.time_remaining}
-
-
- ))}
- {/* Blurred last row - the hook */}
- {hotAuctions.length > 4 && (
-
-
-
- {hotAuctions[4]?.domain || 'premium.io'}
- $XXX
- Xh Xm
-
-
-
- )}
-
-
+
+
+
- {/* Sign in CTA overlay */}
-
-
-
-
- Sign in to see {hotAuctions.length - 4}+ more domains
-
-
- Start Hunting →
-
+ {/* Table Header */}
+
+
Asset Identifier
+
Valuation
+
Current Bid
+
Time to Expiry
+
+
+ {/* Table Rows */}
+
+ {hotAuctions.slice(0, 5).map((auction, idx) => (
+
+
+
${(auction.current_bid * 1.5).toFixed(0)}
+
${auction.current_bid.toLocaleString()}
+
{auction.time_remaining}
+
+ ))}
+
+ {/* Gatekeeper Overlay */}
+
+
+
+
+
Access Full Terminal
+
+
+
+ {/* Blurred Data */}
+
+
premium-crypto.ai $12,500
+
defi-bank.io $8,200
+
cloud-systems.net $4,150
+
@@ -338,607 +699,45 @@ export default function HomePage() {
)}
- {/* Three Pillars: DISCOVER, TRACK, ACQUIRE */}
-
-
- {/* Section Header */}
-
- Your Command Center
-
- Three moves to dominate.
-
-
-
- {/* Pillars */}
-
- {/* DISCOVER */}
-
-
-
-
-
-
-
Discover
-
- Instant domain intel. Not just "taken" — but why ,
- when it expires , and
- smarter alternatives .
-
-
-
-
- Real-time availability across 886+ TLDs
-
-
-
- Expiry dates & WHOIS data
-
-
-
- AI-powered alternatives
-
-
-
-
-
- {/* TRACK */}
-
-
- {/* Popular badge */}
-
-
- Most Popular
-
-
-
-
-
-
-
Track
-
- Your private watchlist with 4-layer health analysis .
- Know the second it weakens.
-
-
-
-
- DNS, HTTP, SSL, WHOIS monitoring
-
-
-
- Real-time health status alerts
-
-
-
- Parked & pre-drop detection
-
-
-
-
-
- {/* ACQUIRE */}
-
-
-
-
-
-
-
Trade
-
- Buy & sell directly. 0% Commission .
- Verified owners .
- Ready to close.
-
-
-
-
- Pounce Direct Marketplace
-
-
-
- DNS-verified ownership
-
-
-
- Secure direct contact
-
-
-
-
-
-
-
-
- {/* Transition Element */}
-
-
- {/* Beyond Hunting: Sell & Alert */}
-
- {/* Subtle background pattern */}
-
+ {/* FINAL CTA - Minimalist & Authoritative */}
+
+
-
- {/* Section Header - Left aligned for flow */}
-
-
-
- Own. Protect. Monetize.
-
-
- Intelligence that gives you the edge. Know what others don't.
-
-
-
-
- {/* For Sale Marketplace */}
-
-
-
-
-
-
-
-
-
-
Sell Domains
-
Marketplace
-
-
-
- Create "For Sale" pages with DNS verification. Buyers contact you directly.
-
-
-
-
- Verified Owner badge
-
-
-
- Pounce Score valuation
-
-
-
- Secure contact form
-
-
-
- Browse
-
-
-
-
-
- {/* Sniper Alerts */}
-
-
-
-
-
-
-
-
-
-
Sniper Alerts
-
Hyper-Personalized
-
-
-
- Custom filters that notify you when matching domains appear.
-
-
-
-
- TLD, length, price filters
-
-
-
- Email & SMS alerts
-
-
-
- Real-time matching
-
-
-
- Set Up
-
-
-
-
-
- {/* Portfolio Health */}
-
-
-
-
-
-
-
-
-
-
Portfolio
-
Domain Insurance
-
-
-
- Monitor your domains 24/7. SSL, renewals, uptime & P&L tracking.
-
-
-
-
- Expiry reminders
-
-
-
- Uptime monitoring
-
-
-
- Valuation & P&L
-
-
-
- Manage
-
-
-
-
-
- {/* YIELD - Passive Income */}
-
-
-
-
- New
-
-
-
-
-
-
-
-
-
-
Yield
-
Passive Income
-
-
-
- Turn parked domains into passive income via intent routing.
-
-
-
-
- AI-powered intent detection
-
-
-
- 70% revenue share
-
-
-
- Verified affiliate partners
-
-
-
- Activate
-
-
-
-
-
-
-
-
- {/* Transition to TLDs */}
-
-
- {/* Trending TLDs Section */}
-
-
- {/* Section Header */}
-
-
-
TLD Pricing
-
- The real price tag.
-
-
- Don't fall for $0.99 promos. We show renewal costs, price trends, and renewal traps across 886+ TLDs.
-
-
-
-
- Trap Detection
-
-
-
-
-
-
-
- Risk Levels
-
-
-
-
- Explore Intel
-
-
-
-
- {/* TLD Cards */}
- {loadingTlds ? (
-
- {[...Array(4)].map((_, i) => (
-
-
-
-
-
- ))}
-
- ) : (
-
- {trendingTlds.map((item, index) => (
-
-
-
-
-
- .{item.tld}
- 0
- ? "text-orange-400 bg-orange-400/10"
- : (item.price_change ?? 0) < 0
- ? "text-accent bg-accent/10"
- : "text-foreground-muted bg-foreground/5"
- )}>
- {getTrendIcon(item.price_change ?? 0)}
- {(item.price_change ?? 0) > 0 ? '+' : ''}{(item.price_change ?? 0).toFixed(1)}%
-
-
-
-
{item.reason}
-
-
- {isAuthenticated ? (
- ${(item.current_price ?? 0).toFixed(2)}/yr
- ) : (
-
-
- Sign in to view
-
- )}
-
-
-
-
- ))}
-
- )}
-
-
-
- {/* Social Proof / Stats Section */}
-
-
-
-
-
-
-
- The edge you need.
-
-
-
-
-
- +
-
-
TLDs Tracked Daily
-
-
-
- 24/ 7
-
-
Always Watching
-
-
-
-
-
-
-
-
- {/* Pricing CTA Section */}
-
-
-
Pricing
-
- Simple. Transparent. Powerful.
+
+
+ Stop guessing.
+ Start knowing.
-
- Start free. Scale when you're ready.
-
- {/* Quick Plans */}
-
- {/* Free Plan */}
-
-
-
-
-
- 5 domains watched
-
-
-
- Daily status checks
-
-
-
- Market overview
-
-
-
+
+
+
+ Initialize
+
+
- {/* Pro Plan */}
-
-
-
- Popular
-
-
-
-
-
-
- 50 domains watched
-
-
-
- Priority alerts
-
-
-
- Full auction access
-
-
-
-
-
-
- Compare All Plans
-
-
-
- {isAuthenticated ? "Go to Dashboard" : "Start Free"}
-
+ View Pricing
-
-
-
- {/* Final CTA */}
-
-
-
Join the hunters.
-
- Ready to pounce?
-
-
- Track your first domain in under a minute. Free forever, no credit card.
-
-
- {isAuthenticated ? "Go to Dashboard" : "Start Hunting — It's Free"}
-
-
- {!isAuthenticated && (
-
-
- Free forever • No credit card • 5 domains included
-
- )}
+
+ Encrypted
+ Global
+ Verified
+
- {/* Ticker Animation Keyframes */}