diff --git a/backend/app/api/yield_domains.py b/backend/app/api/yield_domains.py index b4fcff4..4316636 100644 --- a/backend/app/api/yield_domains.py +++ b/backend/app/api/yield_domains.py @@ -273,7 +273,7 @@ async def get_yield_domain( @router.post("/activate", response_model=ActivateYieldResponse) async def activate_domain_for_yield( request: ActivateYieldRequest, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ @@ -284,7 +284,10 @@ async def activate_domain_for_yield( domain = request.domain.lower().strip() # Check if domain already exists - existing = db.query(YieldDomain).filter(YieldDomain.domain == domain).first() + existing_result = await db.execute( + select(YieldDomain).where(YieldDomain.domain == domain) + ) + existing = existing_result.scalar_one_or_none() if existing: if existing.user_id == current_user.id: raise HTTPException( @@ -313,17 +316,20 @@ async def activate_domain_for_yield( # Find best matching partner if intent_result.suggested_partners: - partner = db.query(AffiliatePartner).filter( - AffiliatePartner.slug == intent_result.suggested_partners[0], - AffiliatePartner.is_active == True, - ).first() + partner_result = await db.execute( + select(AffiliatePartner).where( + AffiliatePartner.slug == intent_result.suggested_partners[0], + AffiliatePartner.is_active == True, + ) + ) + partner = partner_result.scalar_one_or_none() if partner: yield_domain.partner_id = partner.id yield_domain.active_route = partner.slug db.add(yield_domain) - db.commit() - db.refresh(yield_domain) + await db.commit() + await db.refresh(yield_domain) # Create DNS instructions dns_instructions = DNSSetupInstructions( @@ -362,16 +368,19 @@ async def activate_domain_for_yield( @router.post("/domains/{domain_id}/verify", response_model=DNSVerificationResult) async def verify_domain_dns( domain_id: int, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Verify DNS configuration for a yield domain. """ - domain = db.query(YieldDomain).filter( - YieldDomain.id == domain_id, - YieldDomain.user_id == current_user.id, - ).first() + result = await db.execute( + select(YieldDomain).where( + YieldDomain.id == domain_id, + YieldDomain.user_id == current_user.id, + ) + ) + domain = result.scalar_one_or_none() if not domain: raise HTTPException(status_code=404, detail="Yield domain not found") @@ -421,7 +430,7 @@ async def verify_domain_dns( domain.dns_verified_at = datetime.utcnow() domain.status = "active" domain.activated_at = datetime.utcnow() - db.commit() + await db.commit() return DNSVerificationResult( domain=domain.domain, @@ -438,16 +447,19 @@ async def verify_domain_dns( async def update_yield_domain( domain_id: int, update: YieldDomainUpdate, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Update yield domain settings. """ - domain = db.query(YieldDomain).filter( - YieldDomain.id == domain_id, - YieldDomain.user_id == current_user.id, - ).first() + result = await db.execute( + select(YieldDomain).where( + YieldDomain.id == domain_id, + YieldDomain.user_id == current_user.id, + ) + ) + domain = result.scalar_one_or_none() if not domain: raise HTTPException(status_code=404, detail="Yield domain not found") @@ -455,10 +467,13 @@ async def update_yield_domain( # Apply updates if update.active_route is not None: # Validate partner exists - partner = db.query(AffiliatePartner).filter( - AffiliatePartner.slug == update.active_route, - AffiliatePartner.is_active == True, - ).first() + partner_result = await db.execute( + select(AffiliatePartner).where( + AffiliatePartner.slug == update.active_route, + AffiliatePartner.is_active == True, + ) + ) + partner = partner_result.scalar_one_or_none() if not partner: raise HTTPException(status_code=400, detail="Invalid partner route") domain.active_route = update.active_route @@ -475,8 +490,8 @@ async def update_yield_domain( domain.status = "active" domain.paused_at = None - db.commit() - db.refresh(domain) + await db.commit() + await db.refresh(domain) return _domain_to_response(domain) @@ -484,22 +499,25 @@ async def update_yield_domain( @router.delete("/domains/{domain_id}") async def delete_yield_domain( domain_id: int, - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Remove a domain from yield program. """ - domain = db.query(YieldDomain).filter( - YieldDomain.id == domain_id, - YieldDomain.user_id == current_user.id, - ).first() + result = await db.execute( + select(YieldDomain).where( + YieldDomain.id == domain_id, + YieldDomain.user_id == current_user.id, + ) + ) + domain = result.scalar_one_or_none() if not domain: raise HTTPException(status_code=404, detail="Yield domain not found") - db.delete(domain) - db.commit() + await db.delete(domain) + await db.commit() return {"message": "Yield domain removed"} @@ -514,29 +532,53 @@ async def list_transactions( status: Optional[str] = Query(None), limit: int = Query(50, le=100), offset: int = Query(0, ge=0), - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ List yield transactions for user's domains. """ # Get user's domain IDs - domain_ids = db.query(YieldDomain.id).filter( - YieldDomain.user_id == current_user.id - ).subquery() + domain_ids_result = await db.execute( + select(YieldDomain.id).where(YieldDomain.user_id == current_user.id) + ) + domain_ids = [row[0] for row in domain_ids_result.all()] - query = db.query(YieldTransaction).filter( + if not domain_ids: + return YieldTransactionListResponse( + transactions=[], + total=0, + total_gross=Decimal("0"), + total_net=Decimal("0"), + ) + + query = select(YieldTransaction).where( YieldTransaction.yield_domain_id.in_(domain_ids) ) if domain_id: - query = query.filter(YieldTransaction.yield_domain_id == domain_id) + query = query.where(YieldTransaction.yield_domain_id == domain_id) if status: - query = query.filter(YieldTransaction.status == status) + query = query.where(YieldTransaction.status == status) - total = query.count() - transactions = query.order_by(YieldTransaction.created_at.desc()).offset(offset).limit(limit).all() + # Get count + count_query = select(func.count(YieldTransaction.id)).where( + YieldTransaction.yield_domain_id.in_(domain_ids) + ) + if domain_id: + count_query = count_query.where(YieldTransaction.yield_domain_id == domain_id) + if status: + count_query = count_query.where(YieldTransaction.status == status) + + count_result = await db.execute(count_query) + total = count_result.scalar() or 0 + + # Get transactions + result = await db.execute( + query.order_by(YieldTransaction.created_at.desc()).offset(offset).limit(limit) + ) + transactions = list(result.scalars().all()) # Aggregates total_gross = sum(tx.gross_amount for tx in transactions) @@ -559,19 +601,28 @@ async def list_payouts( status: Optional[str] = Query(None), limit: int = Query(20, le=50), offset: int = Query(0, ge=0), - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ List user's yield payouts. """ - query = db.query(YieldPayout).filter(YieldPayout.user_id == current_user.id) + query = select(YieldPayout).where(YieldPayout.user_id == current_user.id) if status: - query = query.filter(YieldPayout.status == status) + query = query.where(YieldPayout.status == status) - total = query.count() - payouts = query.order_by(YieldPayout.created_at.desc()).offset(offset).limit(limit).all() + # Get count + count_result = await db.execute( + select(func.count(YieldPayout.id)).where(YieldPayout.user_id == current_user.id) + ) + total = count_result.scalar() or 0 + + # Get payouts + result = await db.execute( + query.order_by(YieldPayout.created_at.desc()).offset(offset).limit(limit) + ) + payouts = list(result.scalars().all()) # Aggregates total_paid = sum(p.amount for p in payouts if p.status == "completed") @@ -592,14 +643,17 @@ async def list_payouts( @router.get("/partners", response_model=list[AffiliatePartnerResponse]) async def list_partners( category: Optional[str] = Query(None, description="Filter by intent category"), - db: Session = Depends(get_db), + db: AsyncSession = Depends(get_db), ): """ List available affiliate partners. """ - query = db.query(AffiliatePartner).filter(AffiliatePartner.is_active == True) - - partners = query.order_by(AffiliatePartner.priority.desc()).all() + result = await db.execute( + select(AffiliatePartner) + .where(AffiliatePartner.is_active == True) + .order_by(AffiliatePartner.priority.desc()) + ) + partners = list(result.scalars().all()) # Filter by category if specified if category: @@ -678,6 +732,4 @@ def _payout_to_response(payout: YieldPayout) -> YieldPayoutResponse: ) -# Missing import -from sqlalchemy import Integer diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 49361bd..924c3a6 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -30,7 +30,9 @@ import { Radar, Scan, Radio, - Cpu + Cpu, + Clock, + ExternalLink } from 'lucide-react' import Link from 'next/link' import clsx from 'clsx' @@ -42,21 +44,20 @@ interface HotAuction { platform: string } -// High-end Live Market Ticker - Monochrome & Technical +// Compact Ticker for Mobile function MarketTicker({ auctions }: { auctions: HotAuction[] }) { const tickerRef = useRef(null) if (auctions.length === 0) return null - // Create enough duplicates to fill even large screens const items = [...auctions, ...auctions, ...auctions] return (
-
-
+
+
-
+
(
-
-
- +
+
+ {auction.domain}
-
- ${auction.current_bid.toLocaleString()} - {auction.time_remaining} +
+ ${auction.current_bid.toLocaleString()} + {auction.time_remaining}
))} @@ -110,7 +111,7 @@ export default function HomePage() { return (
-
+
) @@ -118,120 +119,117 @@ export default function HomePage() { return (
- {/* Cinematic Background - Architectural & Fine */} + {/* Background */}
-
+
- {/* HERO SECTION: Brutally Catchy & Noble */} -
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* HERO SECTION */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
-
+
{/* Left: Typography & Brand */}
- {/* Brand Seal */} -
-
+ {/* Brand Seal - Mobile */} +
+
Pounce Seal
-
- Est. 2025 // Global Operations +
+ Est. 2025
- {/* Headline */} -

+ {/* Headline - Responsive */} +

The market never sleeps. - + You should.

- {/* Subline & Stats */} + {/* Subline */}
-

+

Transforming domains from static addresses into yield-bearing financial assets. Scan. Acquire. Route. Profit.

- {/* Stats Grid */} -
-
-
886+
-
TLDs Scanned
-
-
-
24/7
-
Live Recon
-
-
-
10s
-
Latency
-
-
-
$1B+
-
Assets Tracked
-
+ {/* Stats Grid - Mobile 2x2 */} +
+ {[ + { value: '886+', label: 'TLDs Scanned' }, + { value: '24/7', label: 'Live Recon' }, + { value: '10s', label: 'Latency' }, + { value: '$1B+', label: 'Assets Tracked' }, + ].map((stat, i) => ( +
+
{stat.value}
+
{stat.label}
+
+ ))}
- {/* Right: The Artifact (Domain Checker) */} -
+ {/* Right: Domain Checker */} +
-
+
{/* Tech Corners */} -
-
-
-
+
+
+
+
-
+
-
- +
+ Terminal Access -
+
-
+
-
+
SECURE CONNECTION - V2.0.4 [STABLE] + V2.0.4
@@ -241,54 +239,52 @@ export default function HomePage() {
- {/* Ticker - Minimalist */} + {/* Ticker */} {!loadingAuctions && hotAuctions.length > 0 && ( )} - {/* THE PARADIGM SHIFT - Problem / Solution */} -
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* PARADIGM SHIFT */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
-
+
- The Broken Model -

- 99% of portfolios are
bleeding cash. + The Broken Model +

+ 99% of portfolios are
bleeding cash.

-
-

- Investors pay renewal fees for years, hoping for a "Unicorn" sale that never happens. It's gambling, not investing. -

-

- Traditional parking pays pennies. Marketplaces charge 20% fees. The system is designed to drain your capital. -

+
+

Investors pay renewal fees for years, hoping for a "Unicorn" sale that never happens.

+

Traditional parking pays pennies. Marketplaces charge 20% fees. The system drains your capital.

-
- The Pounce Protocol -

Asset Class V2.0

-
    -
  • - +
    + The Pounce Protocol +

    Asset Class V2.0

    +
      +
    • +
      Deep Recon - Zone file analysis reveals what's truly valuable. Don't guess. Know. + Zone file analysis reveals what's truly valuable.
    • -
    • - +
    • +
      Frictionless Liquidity - Instant settlement. Verified owners. 0% Commission. + Instant settlement. 0% Commission.
    • -
    • - +
    • +
      Automated Yield - Domains pay for their own renewals via Intent Routing™. + Domains pay for their own renewals.
    @@ -298,320 +294,281 @@ export default function HomePage() {
+ {/* ═══════════════════════════════════════════════════════════════════════ */} {/* CORE ARCHITECTURE - 3 Pillars */} -
+ {/* ═══════════════════════════════════════════════════════════════════════ */} +
{/* Section Header */} -
+
- Core Architecture -

+ Core Architecture +

The Lifecycle
Engine.

-

+

// INTELLIGENCE_LAYER_ACTIVE
// MARKET_PROTOCOL_READY
// YIELD_GENERATION_STANDBY

-
- - {/* 1. INTELLIGENCE */} -
-
- -
- -
-
-
- Module 01 -
-

Intelligence

-

- "Identify Targets." We scan 886+ TLDs in real-time to uncover hidden opportunities before the market reacts. -

+ {/* Pillars - Stacked on Mobile */} +
+ {[ + { + module: '01', + title: 'Intelligence', + desc: '"Identify Targets." We scan 886+ TLDs in real-time to uncover hidden opportunities.', + features: [ + { icon: Scan, title: 'Global Scan', desc: 'Zone file analysis' }, + { icon: Target, title: 'Valuation AI', desc: 'Instant fair-market value' }, + ], + }, + { + module: '02', + title: 'Market', + desc: '"Secure the Asset." Direct access to liquidity with verified owners and 0% commission.', + features: [ + { icon: ShieldCheck, title: 'Verified Owners', desc: 'Mandatory DNS check' }, + { icon: Gavel, title: 'Direct Execution', desc: 'P2P transfers' }, + ], + }, + { + module: '03', + title: 'Yield', + desc: '"Deploy the Asset." Transform idle domains into automated revenue generators.', + features: [ + { icon: Layers, title: 'Intent Routing', desc: 'Traffic to partners' }, + { icon: Coins, title: 'Passive Income', desc: 'Monthly payouts' }, + ], + }, + ].map((pillar, i) => ( +
+
+
-
-
- -
-
Global Scan
-
Zone file analysis & expiration monitoring.
+
+
+
+ Module {pillar.module}
+

{pillar.title}

+

+ {pillar.desc} +

-
- -
-
Valuation AI
-
Instant fair-market value estimation.
-
+ +
+ {pillar.features.map((f, j) => ( +
+ +
+
{f.title}
+
{f.desc}
+
+
+ ))}
-
- - {/* 2. MARKET */} -
-
- -
- -
-
-
- Module 02 -
-

Market

-

- "Secure the Asset." Direct access to liquidity. A verified exchange 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 */} -
-
- -
- -
-
-
- Module 03 -
-

Yield

-

- "Deploy the Asset." 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 */} -
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* INTENT ROUTING */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
-
- The Endgame -

Intent Routing™

-

- We don't build websites. We build signposts.
- Our engine detects user intent (e.g. "kredit.ch" = Loan Search) and routes traffic directly to high-paying partners. +

+ The Endgame +

Intent Routing™

+

+ Our engine detects user intent and routes traffic directly to high-paying partners.

-
- {/* Step 1 */} -
-
- -
-

1. Connect

-

Point your nameservers to `ns.pounce.io`. The system takes over instantly.

-
- - {/* Step 2 */} -
-
- -
-

2. Analyze

-

We scan the semantic intent. `zahnarzt-zh.ch` is identified as "Medical Booking Lead".

-
- - {/* Step 3 */} -
-
- -
-

3. Route

-

Traffic is routed to vertical partners (e.g. Doctolib, Comparis). You earn per qualified lead.

-
+
+ {[ + { icon: Network, step: '1', title: 'Connect', desc: 'Point nameservers to ns.pounce.io' }, + { icon: Cpu, step: '2', title: 'Analyze', desc: 'We scan the semantic intent of your domain' }, + { icon: Share2, step: '3', title: 'Route', desc: 'Traffic is routed to vertical partners' }, + ].map((item, i) => ( +
+
+ +
+

{item.step}. {item.title}

+

{item.desc}

+
+ ))}
- {/* DEEP DIVE: MARKET */} -
-
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MARKET DEEP DIVE */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+ {/* Demo Card */}
-
-
-
-
- zurich-immo.ch - $950 +
+
+
+
+ zurich-immo.ch + $950
-
+
Source -
Pounce Direct +
Pounce Direct
- Seller Verified - DNS Check Passed + Verified + DNS Passed
Commission - 0% + 0%
-
-
+ + {/* Content */}
- 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 verified members. + Exclusive Exchange +

The Velvet Rope.

+

+ Buying is open to everyone, selling is reserved for verified members.

-
    -
  • - -
    -

    Zero Noise

    -

    Gatekeeper technology filters 99% of junk domains automatically.

    -
    -
  • -
  • - -
    -

    Verified Owners

    -

    Sellers must verify ownership via DNS before listing. No fakes.

    -
    -
  • -
  • - -
    -

    0% Commission

    -

    Members keep 100% of the sale price. Direct settlement.

    -
    -
  • +
      + {[ + { icon: Shield, title: 'Zero Noise', desc: 'Gatekeeper tech filters 99% of junk' }, + { icon: Key, title: 'Verified Owners', desc: 'DNS verification required before listing' }, + { icon: Zap, title: '0% Commission', desc: 'Keep 100% of the sale price' }, + ].map((item, i) => ( +
    • + +
      +

      {item.title}

      +

      {item.desc}

      +
      +
    • + ))}
- {/* PRICING TEASER - "Access Levels" */} -
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* PRICING */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
-

Clearance Levels

+

Clearance Levels

-
+
{/* Scout */} -
-

Scout

-
$0/mo
-
    -
  • Recon Overview
  • -
  • Basic Scan
  • -
  • 5 Targets
  • +
    +

    Scout

    +
    $0/mo
    +
      +
    • Recon Overview
    • +
    • Basic Scan
    • +
    • 5 Targets
    - + Join the Hunt
    - {/* Trader - Highlight */} -
    -
    Recommended
    -

    Trader

    -
    $9/mo
    -
      -
    • Clean Feed (No Noise)
    • -
    • Renewal Intel
    • -
    • Liquidate (0% Fee)
    • -
    • 50 Targets
    • + {/* Trader */} +
      +
      Recommended
      +

      Trader

      +
      $9/mo
      +
        +
      • Clean Feed
      • +
      • Renewal Intel
      • +
      • 0% Commission
      • +
      • 50 Targets
      - + Gear Up
      {/* Tycoon */} -
      -

      Tycoon

      -
      $29/mo
      -
        -
      • Full Portfolio Monitor
      • -
      • First Strike Alerts (10m)
      • -
      • 500 Targets
      • -
      • Featured Listings
      • +
        +

        Tycoon

        +
        $29/mo
        +
          +
        • Full Monitor
        • +
        • 10m Alerts
        • +
        • 500 Targets
        • +
        • Featured
        - - Go Professional + + Go Pro
- {/* LIVE FEED TEASER - The "Terminal" Look */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* LIVE OPS */} + {/* ═══════════════════════════════════════════════════════════════════════ */} {!isAuthenticated && !loadingAuctions && hotAuctions.length > 0 && ( -
+
-
+
-

Live Ops

-

FEED_V2.0 // ENCRYPTED

+

Live Ops

+

FEED_V2.0 // ENCRYPTED

- - Recon All Markets + + Recon All
-
+ {/* MOBILE: Card Layout */} +
+ {hotAuctions.slice(0, 4).map((auction, idx) => ( +
+
+
+
+ {auction.domain} +
+ ${auction.current_bid.toLocaleString()} +
+
+ {auction.time_remaining} + {auction.platform} +
+
+ ))} +
+ + {/* DESKTOP: Table Layout */} +
{/* Table Header */} @@ -627,7 +584,7 @@ export default function HomePage() { {hotAuctions.slice(0, 5).map((auction, idx) => (
-
+
{auction.domain}
${(auction.current_bid * 1.5).toFixed(0)}
@@ -635,63 +592,61 @@ export default function HomePage() {
{auction.time_remaining}
))} - - {/* Gatekeeper Overlay */} -
-
- - - Enter HQ - - -
- {/* Blurred Data */} -
-
premium-crypto.ai$12,500
-
defi-bank.io$8,200
-
cloud-systems.net$4,150
-
-
+
+
+ + {/* Gatekeeper */} +
+
+ + + Enter HQ + + +
+
+
premium-crypto.ai$12,500
+
defi-bank.io$8,200
)} - {/* FINAL CTA - Minimalist & Authoritative */} -
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* FINAL CTA */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
-

+

Stop guessing.
Start knowing.

-
+
- + Initialize View Pricing
-
+
Encrypted Global Verified diff --git a/frontend/src/app/terminal/listing/page.tsx b/frontend/src/app/terminal/listing/page.tsx index 3ea32f1..19d5fd8 100755 --- a/frontend/src/app/terminal/listing/page.tsx +++ b/frontend/src/app/terminal/listing/page.tsx @@ -6,9 +6,10 @@ import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { Sidebar } from '@/components/Sidebar' import { - Plus, Shield, Eye, MessageSquare, ExternalLink, Loader2, Trash2, - CheckCircle, AlertCircle, Copy, DollarSign, X, Tag, Sparkles, - TrendingUp, Gavel, Target, Menu, Settings, LogOut, Crown, Zap, Coins, Check + Plus, Shield, Eye, ExternalLink, Loader2, Trash2, + CheckCircle, AlertCircle, Copy, X, Tag, Sparkles, + TrendingUp, Gavel, Target, Menu, Settings, LogOut, Crown, Zap, Coins, + ArrowRight, RefreshCw, Globe, Lock } from 'lucide-react' import Link from 'next/link' import Image from 'next/image' @@ -49,31 +50,38 @@ export default function MyListingsPage() { const [listings, setListings] = useState([]) const [loading, setLoading] = useState(true) - const [showCreateModal, setShowCreateModal] = useState(false) + const [showCreateWizard, setShowCreateWizard] = useState(false) + const [selectedListing, setSelectedListing] = useState(null) const [deletingId, setDeletingId] = useState(null) const [menuOpen, setMenuOpen] = useState(false) const tier = subscription?.tier || 'scout' + const isScout = tier === 'scout' const listingLimits: Record = { scout: 0, trader: 5, tycoon: 50 } const maxListings = listingLimits[tier] || 0 - const canAddMore = listings.length < maxListings + const canAddMore = listings.length < maxListings && !isScout const isTycoon = tier === 'tycoon' const activeListings = listings.filter(l => l.status === 'active').length + const draftListings = listings.filter(l => l.status === 'draft').length const totalViews = listings.reduce((sum, l) => sum + l.view_count, 0) const totalInquiries = listings.reduce((sum, l) => sum + l.inquiry_count, 0) useEffect(() => { checkAuth() }, [checkAuth]) - useEffect(() => { if (prefillDomain) setShowCreateModal(true) }, [prefillDomain]) + useEffect(() => { if (prefillDomain && !isScout) setShowCreateWizard(true) }, [prefillDomain, isScout]) const loadListings = useCallback(async () => { + if (isScout) { + setLoading(false) + return + } setLoading(true) try { const data = await api.getMyListings() setListings(data) } catch (err) { console.error(err) } finally { setLoading(false) } - }, []) + }, [isScout]) useEffect(() => { loadListings() }, [loadListings]) @@ -87,6 +95,17 @@ export default function MyListingsPage() { finally { setDeletingId(null) } } + const handlePublish = async (listing: Listing) => { + if (!listing.is_verified) { + setSelectedListing(listing) + return + } + try { + await api.updateListing(listing.id, { status: 'active' }) + await loadListings() + } catch (err: any) { alert(err.message || 'Failed to publish') } + } + const mobileNavItems = [ { href: '/terminal/radar', label: 'Radar', icon: Target, active: false }, { href: '/terminal/market', label: 'Market', icon: Gavel, active: false }, @@ -113,6 +132,70 @@ export default function MyListingsPage() { ]} ] + // ============================================================================ + // SCOUT UPGRADE PROMPT (Feature not available for free tier) + // ============================================================================ + if (isScout) { + return ( +
+
+
+
+
+
+ +
+

Sell Your Domains

+

+ List your domains directly on Pounce Market. +

+

+ 0% commission. Verified ownership. Instant visibility. +

+ +
+
+
+ + Trader +
+ $9/mo +
+
    +
  • 5 Active Listings
  • +
  • DNS Ownership Verification
  • +
  • Direct Buyer Contact
  • +
+
+ +
+
+
+ + Tycoon +
+ $29/mo +
+
    +
  • 50 Active Listings
  • +
  • Featured Placement
  • +
  • Priority in Market Feed
  • +
+
+ + + Upgrade Now + +
+
+
+
+ ) + } + + // ============================================================================ + // MAIN LISTING VIEW (For Trader & Tycoon) + // ============================================================================ return (
@@ -124,14 +207,18 @@ export default function MyListingsPage() {
- For Sale + Pounce Direct
{listings.length}/{maxListings}
-
+
{activeListings}
-
Active
+
Live
+
+
+
{draftListings}
+
Draft
{totalViews}
@@ -151,18 +238,23 @@ export default function MyListingsPage() {
- Domain Marketplace + 💎 Pounce Direct

For Sale {listings.length}/{maxListings}

+

Sell directly. 0% commission. Verified ownership.

{activeListings}
-
Active
+
Live
+
+
+
{draftListings}
+
Draft
{totalViews}
@@ -173,8 +265,9 @@ export default function MyListingsPage() {
Leads
-
@@ -183,8 +276,9 @@ export default function MyListingsPage() { {/* ADD BUTTON MOBILE */}
-
@@ -199,12 +293,15 @@ export default function MyListingsPage() {

No listings yet

-

Create your first listing

+

Create your first listing to start selling

+
) : (
{/* Header */} -
+
Domain
Price
Status
@@ -214,77 +311,31 @@ export default function MyListingsPage() {
{listings.map((listing) => ( -
- {/* Mobile */} -
-
-
-
- {listing.is_verified ? : } -
- {listing.domain} -
- {listing.status} -
-
- ${listing.asking_price?.toLocaleString() || 'Negotiable'} - {listing.view_count} views · {listing.inquiry_count} leads -
-
- - View - - -
-
- - {/* Desktop */} -
-
-
- {listing.is_verified ? : } -
-
- {listing.domain} - {isTycoon && Featured} -
-
-
${listing.asking_price?.toLocaleString() || '—'}
-
- {listing.status} -
-
{listing.view_count}
-
{listing.inquiry_count}
-
- - - - -
-
-
+ handleDelete(listing.id, listing.domain)} + onVerify={() => setSelectedListing(listing)} + onPublish={() => handlePublish(listing)} + isDeleting={deletingId === listing.id} + /> ))}
)} - {!canAddMore && ( + {!canAddMore && listings.length >= maxListings && (

Listing Limit Reached

-

Upgrade for more listings

- - Upgrade - +

+ {tier === 'trader' ? 'Upgrade to Tycoon for 50 listings' : 'Contact us for enterprise plans'} +

+ {tier === 'trader' && ( + + Upgrade to Tycoon + + )}
)}
@@ -307,105 +358,609 @@ export default function MyListingsPage() { {/* DRAWER */} {menuOpen && ( -
-
setMenuOpen(false)} /> -
-
-
- Pounce -

POUNCE

Terminal v1.0

-
- -
-
- {drawerNavSections.map((section) => ( -
-
{section.title}
- {section.items.map((item: any) => ( - setMenuOpen(false)} className={clsx("flex items-center gap-3 px-4 py-2.5 border-l-2 border-transparent", item.active ? "text-accent border-accent bg-white/[0.02]" : "text-white/60")}> - {item.label} - - ))} -
- ))} -
- setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-white/50">Settings - {user?.is_admin && setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-amber-500/70">Admin} -
-
-
-
-
-

{user?.name || user?.email?.split('@')[0] || 'User'}

{tierName}

-
- {tierName === 'Scout' && setMenuOpen(false)} className="flex items-center justify-center gap-2 w-full py-2.5 bg-accent text-black text-xs font-bold uppercase mb-2">Upgrade} - -
-
-
+ setMenuOpen(false)} + onLogout={() => { logout(); setMenuOpen(false) }} + /> )} - {/* CREATE MODAL */} - {showCreateModal && ( - setShowCreateModal(false)} - onSuccess={() => { loadListings(); setShowCreateModal(false) }} + {/* CREATE WIZARD (3-Step) */} + {showCreateWizard && ( + setShowCreateWizard(false)} + onSuccess={() => { loadListings(); setShowCreateWizard(false) }} prefillDomain={prefillDomain || ''} /> )} + + {/* DNS VERIFICATION MODAL */} + {selectedListing && ( + setSelectedListing(null)} + onVerified={() => { loadListings(); setSelectedListing(null) }} + /> + )}
) } // ============================================================================ -// CREATE MODAL (simplified) +// LISTING ROW COMPONENT // ============================================================================ -function CreateListingModal({ onClose, onSuccess, prefillDomain }: { onClose: () => void; onSuccess: () => void; prefillDomain: string }) { - const [domain, setDomain] = useState(prefillDomain) - const [price, setPrice] = useState('') - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!domain.trim()) return - setLoading(true) - setError(null) - try { - await api.createListing({ domain: domain.trim(), asking_price: price ? parseFloat(price) : null, currency: 'USD', price_type: price ? 'fixed' : 'negotiable' }) - onSuccess() - } catch (err: any) { setError(err.message || 'Failed') } - finally { setLoading(false) } - } +function ListingRow({ + listing, + isTycoon, + onDelete, + onVerify, + onPublish, + isDeleting +}: { + listing: Listing + isTycoon: boolean + onDelete: () => void + onVerify: () => void + onPublish: () => void + isDeleting: boolean +}) { + const isDraft = listing.status === 'draft' + const isActive = listing.status === 'active' + const needsVerification = !listing.is_verified return ( -
-
e.stopPropagation()}> -
-
New Listing
- +
+ {/* Mobile */} +
+
+
+
+ {listing.is_verified ? : } +
+
+ {listing.domain} + {isTycoon && Featured} +
+
+ {listing.status}
-
- {error &&
{error}
} -
- - setDomain(e.target.value)} required - className="w-full px-3 py-2.5 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50" placeholder="example.com" /> -
-
- - setPrice(e.target.value)} min="0" - className="w-full px-3 py-2.5 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50" placeholder="Leave empty for negotiable" /> -
-
- - + )} + {isDraft && !needsVerification && ( + + )} + {isActive && ( + + View + + )} + +
+
+ + {/* Desktop */} +
+
+
+ {listing.is_verified ? : }
- +
+ {listing.domain} + {isTycoon && Featured} + {!listing.is_verified && Unverified} +
+
+
${listing.asking_price?.toLocaleString() || '—'}
+
+ {listing.status} +
+
{listing.view_count}
+
{listing.inquiry_count}
+
+ {isDraft && needsVerification && ( + + )} + {isDraft && !needsVerification && ( + + )} + {isActive && ( + + + + )} + +
+
+
+ ) +} + +// ============================================================================ +// CREATE LISTING WIZARD (3-Step Process) +// ============================================================================ + +function CreateListingWizard({ onClose, onSuccess, prefillDomain }: { + onClose: () => void + onSuccess: () => void + prefillDomain: string +}) { + const [step, setStep] = useState(1) + const [domain, setDomain] = useState(prefillDomain) + const [price, setPrice] = useState('') + const [priceType, setPriceType] = useState<'fixed' | 'negotiable'>('negotiable') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [createdListing, setCreatedListing] = useState(null) + const [verificationData, setVerificationData] = useState(null) + const [verifying, setVerifying] = useState(false) + const [verified, setVerified] = useState(false) + + // Step 1: Create listing + const handleCreateListing = async () => { + if (!domain.trim()) return + setLoading(true) + setError(null) + try { + const listing = await api.createListing({ + domain: domain.trim(), + asking_price: price ? parseFloat(price) : null, + currency: 'USD', + price_type: priceType + }) + setCreatedListing(listing) + + // Start DNS verification + const verification = await api.startDnsVerification(listing.id) + setVerificationData(verification) + setStep(2) + } catch (err: any) { + setError(err.message || 'Failed to create listing') + } finally { + setLoading(false) + } + } + + // Step 2: Check DNS verification + const handleCheckVerification = async () => { + if (!createdListing) return + setVerifying(true) + try { + const result = await api.checkDnsVerification(createdListing.id) + if (result.verified) { + setVerified(true) + setStep(3) + } else { + setError(result.message || 'DNS record not found yet. Please wait for propagation.') + } + } catch (err: any) { + setError(err.message || 'Verification check failed') + } finally { + setVerifying(false) + } + } + + // Step 3: Publish + const handlePublish = async () => { + if (!createdListing) return + setLoading(true) + try { + await api.updateListing(createdListing.id, { status: 'active' }) + onSuccess() + } catch (err: any) { + setError(err.message || 'Failed to publish') + } finally { + setLoading(false) + } + } + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text) + } + + return ( +
+
e.stopPropagation()}> + {/* Header */} +
+
+ + New Listing +
+
+ {/* Step Indicators */} +
+ {[1, 2, 3].map((s) => ( +
s ? "bg-accent/20 text-accent border-accent/40" : + "bg-white/5 text-white/30 border-white/10" + )}> + {step > s ? : s} +
+ ))} +
+ +
+
+ + {/* Step Content */} +
+ {error && ( +
+ {error} +
+ )} + + {/* STEP 1: Domain & Price */} + {step === 1 && ( +
+
+

Enter Domain Details

+

Step 1 of 3: Set your domain and price

+
+ +
+ + setDomain(e.target.value)} + required + className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50" + placeholder="yourdomain.com" + /> +
+ +
+ +
+ + +
+
+ + {priceType === 'fixed' && ( +
+ + setPrice(e.target.value)} + min="0" + className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50" + placeholder="Enter price" + /> +
+ )} + + +
+ )} + + {/* STEP 2: DNS Verification */} + {step === 2 && verificationData && ( +
+
+

Verify Ownership

+

Step 2 of 3: Add a DNS TXT record to prove you own this domain

+
+ +
+
+ +
+ Add this TXT record to your domain's DNS settings. This proves you control the domain. +
+
+
+ +
+
+ +
+ TXT +
+
+ +
+ +
+
+ {verificationData.dns_record_name} +
+ +
+
+ +
+ +
+
+ {verificationData.verification_code} +
+ +
+
+
+ +
+ 💡 DNS changes can take 1-5 minutes to propagate. If verification fails, wait a moment and try again. +
+ + +
+ )} + + {/* STEP 3: Publish */} + {step === 3 && ( +
+
+ +
+ +
+

Ownership Verified!

+

Step 3 of 3: Publish your listing to the Pounce Market

+
+ +
+
{domain}
+
+ {price ? `$${parseFloat(price).toLocaleString()}` : 'Make Offer'} +
+
+ +
+ ✅ Your listing will appear in the Market Feed with the 💎 Pounce Direct badge. +
+ + +
+ )} +
+
+
+ ) +} + +// ============================================================================ +// DNS VERIFICATION MODAL (For existing drafts) +// ============================================================================ + +function DnsVerificationModal({ listing, onClose, onVerified }: { + listing: Listing + onClose: () => void + onVerified: () => void +}) { + const [verificationData, setVerificationData] = useState(null) + const [loading, setLoading] = useState(true) + const [verifying, setVerifying] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + loadVerificationData() + }, [listing.id]) + + const loadVerificationData = async () => { + try { + const data = await api.startDnsVerification(listing.id) + setVerificationData(data) + } catch (err: any) { + setError(err.message || 'Failed to load verification data') + } finally { + setLoading(false) + } + } + + const handleCheck = async () => { + setVerifying(true) + setError(null) + try { + const result = await api.checkDnsVerification(listing.id) + if (result.verified) { + onVerified() + } else { + setError(result.message || 'DNS record not found yet') + } + } catch (err: any) { + setError(err.message || 'Check failed') + } finally { + setVerifying(false) + } + } + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text) + } + + return ( +
+
e.stopPropagation()}> +
+
+ + DNS Verification +
+ +
+ +
+ {loading ? ( +
+ +
+ ) : verificationData ? ( +
+ {error && ( +
+ {error} +
+ )} + +
+

{listing.domain}

+

Add this TXT record to verify ownership

+
+ +
+ +
+
+ {verificationData.dns_record_name} +
+ +
+
+ +
+ +
+
+ {verificationData.verification_code} +
+ +
+
+ + +
+ ) : ( +
Failed to load verification data
+ )} +
+
+
+ ) +} + +// ============================================================================ +// MOBILE DRAWER +// ============================================================================ + +function MobileDrawer({ user, tierName, TierIcon, drawerNavSections, onClose, onLogout }: any) { + return ( +
+
+
+
+
+ Pounce +

POUNCE

Terminal v1.0

+
+ +
+
+ {drawerNavSections.map((section: any) => ( +
+
{section.title}
+ {section.items.map((item: any) => ( + + {item.label} + + ))} +
+ ))} +
+ Settings + {user?.is_admin && Admin} +
+
+
+
+
+

{user?.name || user?.email?.split('@')[0] || 'User'}

{tierName}

+
+ {tierName === 'Scout' && Upgrade} + +
) diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index e31a23f..3a709be 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -10,46 +10,41 @@ export function Footer() { return (
- {/* Background Noise */}
-
-
+
+
{/* Brand Column */} -
-
+
+
-
+
POUNCE - + POUNCE
-

- Global domain intelligence for serious investors.
- Scan. Acquire. Route. Yield. +

+ Global domain intelligence for serious investors. Scan. Acquire. Route. Yield.

- {/* Newsletter Input - Tech Style */} -
+ {/* Newsletter - Hidden on Mobile */} +
- - {/* Links Columns */} -
-

Protocol

-
    -
  • - - Acquire - -
  • -
  • - - Discover - -
  • -
  • - - Yield - -
  • -
  • - - Pricing - -
  • + {/* Protocol Links */} +
    +

    Protocol

    +
      + {[ + { href: '/acquire', label: 'Acquire' }, + { href: '/discover', label: 'Discover' }, + { href: '/yield', label: 'Yield' }, + { href: '/pricing', label: 'Pricing' }, + ].map((link) => ( +
    • + + {link.label} + +
    • + ))}
    -
    -

    Intel

    -
      + {/* Info Links */} +
      +

      Intel

      +
      • - Briefings + Briefings
      • - About Us + About
      • - Contact HQ + Contact
      • {!isAuthenticated && (
      • - Login + Login
      • )}
      -
      + {/* Legal - Hidden on Small Mobile, Merged into Intel on Mobile */} +

      Legal

      • - Privacy Policy + Privacy
      • - Terms of Service + Terms
      • Imprint @@ -149,16 +138,27 @@ export function Footer() {
      + {/* Mobile Legal Links */} +
      + Privacy + | + Terms + | + Imprint +
      + {/* Bottom Bar */} -
      +

      © {new Date().getFullYear()} POUNCE AG — ZURICH

      -
      - System Status: Online - v2.0.4 [Stable] +
      + + + Online + + v2.0.4
) } - diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 04f8cbf..eba5a99 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -11,33 +11,27 @@ import { CreditCard, LayoutDashboard, Coins, + ArrowRight, + Sparkles, + Target, + LogOut, + User, } from 'lucide-react' import { useState, useEffect } from 'react' +import Image from 'next/image' import clsx from 'clsx' -/** - * Public Header Component - * - * Used for: - * - Landing page (/) - * - Public pages (pricing, about, contact, blog, etc.) - * - Auth pages (login, register) - * - * For logged-in users in the Command Center, use TerminalLayout instead. - */ export function Header() { const pathname = usePathname() const { isAuthenticated, user, logout, subscription } = useStore() - const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + const [menuOpen, setMenuOpen] = useState(false) - // Close mobile menu on route change useEffect(() => { - setMobileMenuOpen(false) + setMenuOpen(false) }, [pathname]) const tierName = subscription?.tier_name || subscription?.tier || 'Scout' - // Navigation: Discover | Acquire | Yield | Pricing const publicNavItems = [ { href: '/discover', label: 'Discover', icon: TrendingUp }, { href: '/acquire', label: 'Acquire', icon: Gavel }, @@ -50,151 +44,212 @@ export function Header() { return pathname.startsWith(href) } - // Check if we're on a Command Center page (should use Sidebar instead) const isCommandCenterPage = pathname.startsWith('/terminal') || pathname.startsWith('/admin') - // If logged in and on Command Center page, don't render this header if (isAuthenticated && isCommandCenterPage) { return null } return ( -
-
- {/* Left side: Logo + Nav Links */} -
+ <> +
+
{/* Logo */} - - + +
+ Pounce +
+ POUNCE - {/* Main Nav Links (Desktop) */} -
- {/* Right side */} -
- - {/* Mobile Menu */} - {mobileMenuOpen && ( -
- + + {/* Mobile Menu Button */} + +
+
+ + {/* Mobile Drawer */} + {menuOpen && ( +
+
setMenuOpen(false)} + /> + +
+ {/* Header */} +
+
+ Pounce +
+

Pounce

+

Domain Intelligence

+
+
+ +
+ + {/* Navigation */} +
+
+
+
+ Explore +
+ {publicNavItems.map((item) => ( + setMenuOpen(false)} + className={clsx( + "flex items-center gap-3 px-4 py-3 transition-colors border-l-2", + isActive(item.href) + ? "border-accent text-white bg-white/[0.03]" + : "border-transparent text-white/60 active:text-white active:bg-white/[0.03]" + )} + > + + {item.label} + + ))} +
+ + {isAuthenticated && ( +
+
+
+ Terminal +
+ setMenuOpen(false)} + className="flex items-center gap-3 px-4 py-3 text-accent active:bg-white/[0.03] transition-colors border-l-2 border-transparent active:border-accent" + > + + Command Center + +
+ )} +
+ + {/* Footer */} +
+ {isAuthenticated ? ( + <> +
+
+ +
+
+

+ {user?.name || user?.email?.split('@')[0] || 'User'} +

+

{tierName}

+
+
+ + setMenuOpen(false)} + className="flex items-center justify-center gap-2 w-full py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2" + > + + Open Terminal + + + + + ) : ( + <> + setMenuOpen(false)} + className="flex items-center justify-center gap-2 w-full py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2" + > + + Start Free + + + setMenuOpen(false)} + className="flex items-center justify-center gap-2 w-full py-2 border border-white/10 text-white/50 text-[10px] font-mono uppercase tracking-wider active:bg-white/5 transition-all" + > + Sign In + + + + )} +
+
)} -
+ ) } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5b08643..e9fe9da 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -354,6 +354,33 @@ class ApiClient { return this.request(`/listings/${id}`, { method: 'DELETE' }) } + async updateListing(id: number, data: { status?: string; title?: string; description?: string; asking_price?: number }) { + return this.request(`/listings/${id}`, { method: 'PUT', body: JSON.stringify(data) }) + } + + async startDnsVerification(id: number) { + return this.request<{ + verification_code: string + dns_record_type: string + dns_record_name: string + dns_record_value: string + instructions: string + status: string + }>(`/listings/${id}/verify-dns`, { method: 'POST' }) + } + + async checkDnsVerification(id: number) { + return this.request<{ + verified: boolean + status: string + message: string + }>(`/listings/${id}/verify-dns/check`) + } + + async getListingInquiries(id: number) { + return this.request(`/listings/${id}/inquiries`) + } + // Subscription async getSubscription() { return this.request<{