diff --git a/backend/app/api/listings.py b/backend/app/api/listings.py index 96db302..01e45ce 100644 --- a/backend/app/api/listings.py +++ b/backend/app/api/listings.py @@ -516,10 +516,41 @@ async def create_listing( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): - """Create a new domain listing.""" + """ + Create a new domain listing. + + SECURITY: Domain must be in user's portfolio before listing for sale. + DNS verification happens in the verification step (separate endpoint). + """ + from app.models.portfolio import PortfolioDomain + + domain_lower = data.domain.lower() + + # SECURITY CHECK: Domain must be in user's portfolio + portfolio_result = await db.execute( + select(PortfolioDomain).where( + PortfolioDomain.domain == domain_lower, + PortfolioDomain.user_id == current_user.id, + ) + ) + portfolio_domain = portfolio_result.scalar_one_or_none() + + if not portfolio_domain: + raise HTTPException( + status_code=403, + detail="Domain must be in your portfolio before listing for sale. Add it to your portfolio first.", + ) + + # Check if domain is sold + if portfolio_domain.is_sold: + raise HTTPException( + status_code=400, + detail="Cannot list a sold domain for sale.", + ) + # Check if domain is already listed existing = await db.execute( - select(DomainListing).where(DomainListing.domain == data.domain.lower()) + select(DomainListing).where(DomainListing.domain == domain_lower) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="This domain is already listed") @@ -550,7 +581,7 @@ async def create_listing( ) # Generate slug - slug = _generate_slug(data.domain) + slug = _generate_slug(domain_lower) # Check slug uniqueness slug_check = await db.execute( @@ -561,7 +592,7 @@ async def create_listing( # Get valuation try: - valuation = await valuation_service.estimate_value(data.domain, db, save_result=False) + valuation = await valuation_service.estimate_value(domain_lower, db, save_result=False) pounce_score = min(100, int(valuation.get("score", 50))) estimated_value = valuation.get("value", 0) # Fixed: was 'estimated_value', service returns 'value' except Exception: @@ -571,7 +602,7 @@ async def create_listing( # Create listing listing = DomainListing( user_id=current_user.id, - domain=data.domain.lower(), + domain=domain_lower, slug=slug, title=data.title, description=data.description, diff --git a/backend/app/api/portfolio.py b/backend/app/api/portfolio.py index 83dc238..501bbd9 100644 --- a/backend/app/api/portfolio.py +++ b/backend/app/api/portfolio.py @@ -1,10 +1,12 @@ """Portfolio API routes.""" +import secrets 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 +import dns.resolver from app.database import get_db from app.api.deps import get_current_user @@ -71,6 +73,11 @@ class PortfolioDomainResponse(BaseModel): notes: Optional[str] tags: Optional[str] roi: Optional[float] + # DNS Verification fields + is_dns_verified: bool = False + verification_status: str = "unverified" + verification_code: Optional[str] = None + verified_at: Optional[datetime] = None created_at: datetime updated_at: datetime @@ -78,6 +85,25 @@ class PortfolioDomainResponse(BaseModel): from_attributes = True +class DNSVerificationStartResponse(BaseModel): + """Response when starting DNS verification.""" + domain_id: int + domain: str + verification_code: str + dns_record_type: str + dns_record_name: str + dns_record_value: str + instructions: str + status: str + + +class DNSVerificationCheckResponse(BaseModel): + """Response when checking DNS verification.""" + verified: bool + status: str + message: str + + class PortfolioSummary(BaseModel): """Summary of user's portfolio.""" total_domains: int @@ -204,6 +230,10 @@ async def get_portfolio( notes=d.notes, tags=d.tags, roi=d.roi, + is_dns_verified=d.is_dns_verified, + verification_status=d.verification_status, + verification_code=d.verification_code, + verified_at=d.verified_at, created_at=d.created_at, updated_at=d.updated_at, ) @@ -351,6 +381,10 @@ async def add_portfolio_domain( notes=domain.notes, tags=domain.tags, roi=domain.roi, + is_dns_verified=domain.is_dns_verified, + verification_status=domain.verification_status, + verification_code=domain.verification_code, + verified_at=domain.verified_at, created_at=domain.created_at, updated_at=domain.updated_at, ) @@ -398,6 +432,10 @@ async def get_portfolio_domain( notes=domain.notes, tags=domain.tags, roi=domain.roi, + is_dns_verified=domain.is_dns_verified, + verification_status=domain.verification_status, + verification_code=domain.verification_code, + verified_at=domain.verified_at, created_at=domain.created_at, updated_at=domain.updated_at, ) @@ -454,6 +492,10 @@ async def update_portfolio_domain( notes=domain.notes, tags=domain.tags, roi=domain.roi, + is_dns_verified=domain.is_dns_verified, + verification_status=domain.verification_status, + verification_code=domain.verification_code, + verified_at=domain.verified_at, created_at=domain.created_at, updated_at=domain.updated_at, ) @@ -510,6 +552,10 @@ async def mark_domain_sold( notes=domain.notes, tags=domain.tags, roi=domain.roi, + is_dns_verified=domain.is_dns_verified, + verification_status=domain.verification_status, + verification_code=domain.verification_code, + verified_at=domain.verified_at, created_at=domain.created_at, updated_at=domain.updated_at, ) @@ -593,6 +639,10 @@ async def refresh_domain_value( notes=domain.notes, tags=domain.tags, roi=domain.roi, + is_dns_verified=domain.is_dns_verified, + verification_status=domain.verification_status, + verification_code=domain.verification_code, + verified_at=domain.verified_at, created_at=domain.created_at, updated_at=domain.updated_at, ) @@ -617,3 +667,219 @@ async def get_domain_valuation( return ValuationResponse(**valuation) + +# ============== DNS Verification Endpoints ============== + +def _generate_verification_code() -> str: + """Generate a unique verification code.""" + return f"pounce-verify-{secrets.token_hex(8)}" + + +def _domain_to_response(domain: PortfolioDomain) -> PortfolioDomainResponse: + """Convert PortfolioDomain to response schema.""" + 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, + is_dns_verified=domain.is_dns_verified, + verification_status=domain.verification_status, + verification_code=domain.verification_code, + verified_at=domain.verified_at, + created_at=domain.created_at, + updated_at=domain.updated_at, + ) + + +@router.post("/{domain_id}/verify-dns", response_model=DNSVerificationStartResponse) +async def start_dns_verification( + domain_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """ + Start DNS verification for a portfolio domain. + + Returns a verification code that must be added as a TXT record. + """ + 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", + ) + + if domain.is_dns_verified: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Domain is already verified", + ) + + # Generate or reuse existing verification code + if not domain.verification_code: + domain.verification_code = _generate_verification_code() + + domain.verification_status = "pending" + domain.verification_started_at = datetime.utcnow() + + await db.commit() + await db.refresh(domain) + + return DNSVerificationStartResponse( + domain_id=domain.id, + domain=domain.domain, + verification_code=domain.verification_code, + dns_record_type="TXT", + dns_record_name=f"_pounce.{domain.domain}", + dns_record_value=domain.verification_code, + instructions=f"Add a TXT record to your DNS settings:\n\nHost/Name: _pounce\nType: TXT\nValue: {domain.verification_code}\n\nDNS changes can take up to 48 hours to propagate, but usually complete within minutes.", + status=domain.verification_status, + ) + + +@router.get("/{domain_id}/verify-dns/check", response_model=DNSVerificationCheckResponse) +async def check_dns_verification( + domain_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """ + Check if DNS verification is complete. + + Looks for the TXT record and verifies it matches the expected code. + """ + 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", + ) + + if domain.is_dns_verified: + return DNSVerificationCheckResponse( + verified=True, + status="verified", + message="Domain ownership already verified", + ) + + if not domain.verification_code: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Verification not started. Call POST /verify-dns first.", + ) + + # Check DNS TXT record + txt_record_name = f"_pounce.{domain.domain}" + verified = False + + try: + resolver = dns.resolver.Resolver() + resolver.timeout = 5 + resolver.lifetime = 10 + + answers = resolver.resolve(txt_record_name, 'TXT') + + for rdata in answers: + txt_value = rdata.to_text().strip('"') + if txt_value == domain.verification_code: + verified = True + break + except dns.resolver.NXDOMAIN: + return DNSVerificationCheckResponse( + verified=False, + status="pending", + message=f"TXT record not found. Please add a TXT record at _pounce.{domain.domain}", + ) + except dns.resolver.NoAnswer: + return DNSVerificationCheckResponse( + verified=False, + status="pending", + message="TXT record exists but has no value. Check your DNS configuration.", + ) + except dns.resolver.Timeout: + return DNSVerificationCheckResponse( + verified=False, + status="pending", + message="DNS query timed out. Please try again.", + ) + except Exception as e: + return DNSVerificationCheckResponse( + verified=False, + status="error", + message=f"DNS lookup error: {str(e)}", + ) + + if verified: + domain.is_dns_verified = True + domain.verification_status = "verified" + domain.verified_at = datetime.utcnow() + await db.commit() + + return DNSVerificationCheckResponse( + verified=True, + status="verified", + message="Domain ownership verified successfully! You can now list this domain for sale or activate Yield.", + ) + else: + return DNSVerificationCheckResponse( + verified=False, + status="pending", + message=f"TXT record found but value doesn't match. Expected: {domain.verification_code}", + ) + + +@router.get("/verified", response_model=List[PortfolioDomainResponse]) +async def get_verified_domains( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """ + Get only DNS-verified portfolio domains. + + These domains can be used for Yield or For Sale listings. + """ + result = await db.execute( + select(PortfolioDomain).where( + and_( + PortfolioDomain.user_id == current_user.id, + PortfolioDomain.is_dns_verified == True, + PortfolioDomain.is_sold == False, + ) + ).order_by(PortfolioDomain.domain.asc()) + ) + domains = result.scalars().all() + + return [_domain_to_response(d) for d in domains] + diff --git a/backend/app/api/yield_domains.py b/backend/app/api/yield_domains.py index 4316636..9b8b856 100644 --- a/backend/app/api/yield_domains.py +++ b/backend/app/api/yield_domains.py @@ -279,11 +279,43 @@ async def activate_domain_for_yield( """ Activate a domain for yield/intent routing. + SECURITY: Domain must be in user's portfolio AND DNS-verified. This creates the yield domain record and returns DNS setup instructions. """ + from app.models.portfolio import PortfolioDomain + domain = request.domain.lower().strip() - # Check if domain already exists + # SECURITY CHECK 1: Domain must be in user's portfolio + portfolio_result = await db.execute( + select(PortfolioDomain).where( + PortfolioDomain.domain == domain, + PortfolioDomain.user_id == current_user.id, + ) + ) + portfolio_domain = portfolio_result.scalar_one_or_none() + + if not portfolio_domain: + raise HTTPException( + status_code=403, + detail="Domain must be in your portfolio before activating Yield. Add it to your portfolio first.", + ) + + # SECURITY CHECK 2: Domain must be DNS-verified + if not portfolio_domain.is_dns_verified: + raise HTTPException( + status_code=403, + detail="Domain must be DNS-verified before activating Yield. Verify ownership in your portfolio first.", + ) + + # SECURITY CHECK 3: Domain must not be sold + if portfolio_domain.is_sold: + raise HTTPException( + status_code=400, + detail="Cannot activate Yield for a sold domain.", + ) + + # Check if domain already exists in yield system existing_result = await db.execute( select(YieldDomain).where(YieldDomain.domain == domain) ) diff --git a/backend/app/models/portfolio.py b/backend/app/models/portfolio.py index 4b6737a..2a916fe 100644 --- a/backend/app/models/portfolio.py +++ b/backend/app/models/portfolio.py @@ -45,6 +45,13 @@ class PortfolioDomain(Base): # Status status: Mapped[str] = mapped_column(String(50), default="active") # active, expired, sold, parked + # DNS Verification (required for Yield and For Sale) + is_dns_verified: Mapped[bool] = mapped_column(Boolean, default=False) + verification_status: Mapped[str] = mapped_column(String(50), default="unverified") # unverified, pending, verified, failed + verification_code: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + verification_started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + verified_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + # Notes notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True) tags: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) # Comma-separated diff --git a/frontend/src/app/acquire/page.tsx b/frontend/src/app/acquire/page.tsx index 63ad8b5..f42b5ba 100644 --- a/frontend/src/app/acquire/page.tsx +++ b/frontend/src/app/acquire/page.tsx @@ -5,7 +5,6 @@ import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { Header } from '@/components/Header' import { Footer } from '@/components/Footer' -import { PlatformBadge } from '@/components/PremiumTable' import { Clock, ExternalLink, @@ -13,22 +12,17 @@ import { Flame, Timer, Gavel, - DollarSign, X, Lock, TrendingUp, ChevronUp, ChevronDown, - ChevronsUpDown, - Sparkles, Diamond, ShieldCheck, - Zap, Filter, - Check, - Shield, ArrowUpRight, - ArrowRight + ArrowRight, + Loader2 } from 'lucide-react' import Link from 'next/link' import clsx from 'clsx' @@ -72,72 +66,30 @@ interface Auction { } type TabType = 'all' | 'ending' | 'hot' -type SortField = 'domain' | 'ending' | 'bid' | 'bids' -type SortDirection = 'asc' | 'desc' const PLATFORMS = [ - { id: 'All', name: 'All Sources' }, + { id: 'All', name: 'All' }, { id: 'GoDaddy', name: 'GoDaddy' }, { id: 'Sedo', name: 'Sedo' }, { id: 'NameJet', name: 'NameJet' }, { id: 'DropCatch', name: 'DropCatch' }, ] -// Premium TLDs that look professional const PREMIUM_TLDS = ['com', 'io', 'ai', 'co', 'de', 'ch', 'net', 'org', 'app', 'dev', 'xyz'] -// Vanity Filter: Only show "beautiful" domains to non-authenticated users function isVanityDomain(auction: Auction): boolean { - const domain = auction.domain - const parts = domain.split('.') + const parts = auction.domain.split('.') if (parts.length < 2) return false - const name = parts[0] const tld = parts.slice(1).join('.').toLowerCase() - - // Check TLD is premium if (!PREMIUM_TLDS.includes(tld)) return false - - // Check length (max 12 characters for the name) if (name.length > 12) return false - - // No hyphens if (name.includes('-')) return false - - // No numbers (unless domain is 4 chars or less - short domains are valuable) if (name.length > 4 && /\d/.test(name)) return false - return true } -// Generate a mock "Deal Score" for display purposes -function getDealScore(auction: Auction): number | null { - let score = 50 - - const name = auction.domain.split('.')[0] - if (name.length <= 4) score += 20 - else if (name.length <= 6) score += 10 - - if (['com', 'io', 'ai'].includes(auction.tld)) score += 15 - - if (auction.age_years && auction.age_years > 5) score += 10 - - if (auction.num_bids >= 20) score += 15 - else if (auction.num_bids >= 10) score += 10 - - return Math.min(score, 100) -} - -function SortIcon({ field, currentField, direction }: { field: SortField, currentField: SortField, direction: SortDirection }) { - if (field !== currentField) { - return - } - return direction === 'asc' - ? - : -} - -export default function MarketPage() { +export default function AcquirePage() { const { isAuthenticated, checkAuth, isLoading: authLoading } = useStore() const [allAuctions, setAllAuctions] = useState([]) @@ -146,12 +98,11 @@ export default function MarketPage() { const [pounceItems, setPounceItems] = useState([]) const [loading, setLoading] = useState(true) const [activeTab, setActiveTab] = useState('all') - const [sortField, setSortField] = useState('ending') - const [sortDirection, setSortDirection] = useState('asc') const [searchQuery, setSearchQuery] = useState('') const [selectedPlatform, setSelectedPlatform] = useState('All') - const [maxBid, setMaxBid] = useState('') + const [searchFocused, setSearchFocused] = useState(false) + const [showFilters, setShowFilters] = useState(false) useEffect(() => { checkAuth() @@ -206,143 +157,291 @@ export default function MarketPage() { } } - // Apply Vanity Filter for non-authenticated users const displayAuctions = useMemo(() => { const current = getCurrentAuctions() - if (isAuthenticated) { - return current - } + if (isAuthenticated) return current return current.filter(isVanityDomain) }, [activeTab, allAuctions, endingSoon, hotAuctions, isAuthenticated]) const filteredAuctions = displayAuctions.filter(auction => { - if (searchQuery && !auction.domain.toLowerCase().includes(searchQuery.toLowerCase())) { - return false - } - if (selectedPlatform !== 'All' && auction.platform !== selectedPlatform) { - return false - } - if (maxBid && auction.current_bid > parseFloat(maxBid)) { - return false - } + if (searchQuery && !auction.domain.toLowerCase().includes(searchQuery.toLowerCase())) return false + if (selectedPlatform !== 'All' && auction.platform !== selectedPlatform) return false return true }) - const handleSort = (field: SortField) => { - if (sortField === field) { - setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc') - } else { - setSortField(field) - setSortDirection('asc') - } - } - - const sortedAuctions = [...filteredAuctions].sort((a, b) => { - const modifier = sortDirection === 'asc' ? 1 : -1 - switch (sortField) { - case 'domain': - return a.domain.localeCompare(b.domain) * modifier - case 'bid': - return (a.current_bid - b.current_bid) * modifier - case 'bids': - return (a.num_bids - b.num_bids) * modifier - default: - return 0 - } - }) - - const formatCurrency = (amount: number, currency = 'USD') => { - return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount) + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amount) } const getTimeColor = (timeRemaining: string) => { - if (timeRemaining.includes('m') && !timeRemaining.includes('h')) return 'text-red-400 font-bold' - if (timeRemaining.includes('h') && parseInt(timeRemaining) < 12) return 'text-amber-400 font-bold' + if (timeRemaining.includes('m') && !timeRemaining.includes('h')) return 'text-red-400' + if (timeRemaining.includes('h') && parseInt(timeRemaining) < 12) return 'text-amber-400' return 'text-white/40' } - const hotPreview = hotAuctions.slice(0, 4) - if (authLoading) { return (
-
+
) } return ( -
- {/* Cinematic Background - Architectural & Fine */} +
+ {/* Background */}
-
-
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE HEADER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+ {/* Title Row */} +
+
+
+ Acquire +
+
+ {filteredAuctions.length} assets +
+
+ + {/* Stats Grid */} +
+
+
{allAuctions.length}
+
Total
+
+
+
{endingSoon.length}
+
Ending
+
+
+
{hotAuctions.length}
+
Hot
+
+
+
+
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE SEARCH & FILTERS */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+ {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + className="flex-1 bg-transparent px-3 py-2.5 text-sm font-mono text-white placeholder:text-white/20 outline-none" + /> + {searchQuery && ( + + )} +
+
+ + {/* Tab Filters */} +
+ {[ + { id: 'all' as const, label: 'All', icon: Gavel }, + { id: 'ending' as const, label: 'Ending', icon: Timer }, + { id: 'hot' as const, label: 'Hot', icon: Flame }, + ].map((tab) => ( + + ))} +
+ + {/* Platform Filter Toggle */} + + + {showFilters && ( +
+ {PLATFORMS.map((p) => ( + + ))} +
+ )} +
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE AUCTION LIST */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+ {/* Login Banner */} + {!isAuthenticated && ( +
+
+ +
+

Unlock Full Access

+

Valuations & deal scores

+
+ + Join + +
+
+ )} + + {loading ? ( +
+ +
+ ) : filteredAuctions.length === 0 ? ( +
+ No assets found +
+ ) : ( + + )} +
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* DESKTOP LAYOUT */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
- {/* Hero Header - High Tech */} -
-
+ {/* Hero Header */} +
+
- +
Live Liquidity Pool -

+

Acquire Assets.

-

+

Global liquidity pool. Verified assets only. Aggregated from GoDaddy, Sedo, and Pounce Direct.

-
+
-
{formatCurrency(allAuctions.length, 'USD').replace('$', '')}
-
Live Opportunities
+
{allAuctions.length}
+
Live
+
+
+
{endingSoon.length}
+
Ending
{pounceItems.length}
-
Direct Listings
+
Direct
- {/* Login Banner for non-authenticated users */} + {/* Login Banner */} {!isAuthenticated && ( -
-
-
-
-
- -
-
-
+
+
+
+
-

Restricted Access

-

- Sign in to unlock valuations, deal scores, and unfiltered data. -

+

Restricted Access

+

Sign in to unlock valuations and deal scores.

Authorize @@ -350,63 +449,40 @@ export default function MarketPage() {
)} - {/* Pounce Direct Section - Featured */} + {/* Featured Direct Listings */} {pounceItems.length > 0 && ( -
-
+
+
Direct Listings
- // 0% COMMISSION // INSTANT SETTLEMENT + // 0% COMMISSION
- - How to list my domains? -
-
- {pounceItems.map((item) => ( +
+ {pounceItems.slice(0, 3).map((item) => ( -
- +
+ + Available
- -
-
- - Available Now -
- -

- {item.domain} -

- -
- {item.verified && ( - - Verified - - )} - - {isAuthenticated ? `Score: ${item.pounce_score}/100` : 'Score: [LOCKED]'} - -
- -
-
- Buy Price - {formatCurrency(item.price, item.currency)} -
-
- Acquire -
-
+

+ {item.domain} +

+
+ {formatCurrency(item.price)} + {item.verified && ( + + Verified + + )}
))} @@ -414,233 +490,133 @@ export default function MarketPage() {
)} - {/* Search & Filters - Tech Bar */} -
-
- {/* Search */} -
- - setSearchQuery(e.target.value)} - className="w-full pl-14 pr-5 py-3.5 bg-[#0A0A0A] border border-white/10 text-white placeholder:text-white/20 font-mono text-base focus:outline-none focus:border-accent focus:bg-[#0F0F0F] transition-all rounded-none" - /> -
+ {/* Search & Filters Bar */} +
+
+ {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="w-full pl-12 pr-4 py-3 bg-[#0A0A0A] border border-white/10 text-white placeholder:text-white/20 font-mono text-sm focus:outline-none focus:border-accent transition-all" + /> +
+ + {/* Filters */} +
+ - {/* Filters */} -
-
- - -
- -
- {[ - { id: 'all' as const, label: 'ALL', icon: Gavel }, - { id: 'ending' as const, label: 'ENDING', icon: Timer }, - { id: 'hot' as const, label: 'HOT', icon: Flame }, - ].map((tab) => ( - - ))} -
+
+ {[ + { id: 'all' as const, label: 'All', icon: Gavel }, + { id: 'ending' as const, label: 'Ending', icon: Timer }, + { id: 'hot' as const, label: 'Hot', icon: Flame }, + ].map((tab) => ( + + ))}
+
- {/* Auctions Table - The Terminal */} -
-
- - - - - - - - - - - - - {loading ? ( - Array.from({ length: 10 }).map((_, idx) => ( - - - - - - - - - )) - ) : sortedAuctions.length === 0 ? ( - - - - ) : ( - sortedAuctions.map((auction) => ( - - - - - {/* Valuation - blurred for non-authenticated */} - - - - - )) - )} - -
- - - Source - - - - - Valuation - {!isAuthenticated && } - - - -
- {searchQuery ? `// NO_ASSETS_FOUND: "${searchQuery}"` : '// NO_DATA_AVAILABLE'} -
-
- - {auction.domain} - -
- {auction.platform} -
-
-
-
- {auction.platform} - {auction.platform === 'Pounce' && } -
-
-
- - {formatCurrency(auction.current_bid)} - - {auction.buy_now_price && ( - Buy Now - )} -
-
- {isAuthenticated ? ( - - ${(auction.current_bid * 1.5).toFixed(0)} - - ) : ( -
- - $X,XXX - -
- )} -
- - {auction.time_remaining} - - - - - -
+ {/* Desktop Table */} +
+ {/* Table Header */} +
+
Domain
+
Platform
+
Price
+
Time
+
+ + {/* Table Body */} + {loading ? ( +
+ +
+ ) : filteredAuctions.length === 0 ? ( +
No assets found
+ ) : ( + + )}
- {/* Stats */} + {/* Stats Footer */} {!loading && ( -
+
System Status: Online - - {searchQuery - ? `Assets Found: ${sortedAuctions.length}` - : `Total Assets: ${allAuctions.length}` - } - + Assets: {filteredAuctions.length}
)} {/* Bottom CTA */} {!isAuthenticated && ( -
-
-
-
-
- -
-

Eliminate Noise.

-

- Our 'Trader' plan filters 99% of junk domains automatically. - Stop digging through spam. Start acquiring assets. -

- - Upgrade Intel - - -
+
+
+
+
+

Eliminate Noise.

+

+ Our Trader plan filters 99% of junk domains automatically. +

+ + Upgrade + +
)}
diff --git a/frontend/src/app/terminal/portfolio/page.tsx b/frontend/src/app/terminal/portfolio/page.tsx index 91db10f..5ecd4ab 100755 --- a/frontend/src/app/terminal/portfolio/page.tsx +++ b/frontend/src/app/terminal/portfolio/page.tsx @@ -19,6 +19,8 @@ import { Menu, Settings, Shield, + ShieldCheck, + ShieldAlert, LogOut, Crown, Sparkles, @@ -34,7 +36,9 @@ import { CheckCircle, AlertCircle, TrendingDown, - BarChart3 + BarChart3, + Copy, + Check } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' @@ -83,6 +87,7 @@ export default function PortfolioPage() { const [deletingId, setDeletingId] = useState(null) const [showAddModal, setShowAddModal] = useState(false) const [selectedDomain, setSelectedDomain] = useState(null) + const [verifyingDomain, setVerifyingDomain] = useState(null) const [filter, setFilter] = useState<'all' | 'active' | 'sold'>('all') // Sorting @@ -401,13 +406,24 @@ export default function PortfolioPage() {
- {!domain.is_sold && canListForSale && ( - - Sell - + {!domain.is_sold && ( + domain.is_dns_verified ? ( + canListForSale && ( + + Sell + + ) + ) : ( + + ) )} + ) )}