diff --git a/frontend/src/app/terminal/intel/page.tsx b/frontend/src/app/terminal/intel/page.tsx index 8e4e6a9..7f0a08b 100755 --- a/frontend/src/app/terminal/intel/page.tsx +++ b/frontend/src/app/terminal/intel/page.tsx @@ -58,7 +58,10 @@ function getTierLevel(tier: UserTier): number { } } -const formatPrice = (p: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p) +const formatPrice = (p: number) => { + if (typeof p !== 'number' || isNaN(p)) return '$0' + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p) +} // ============================================================================ // MAIN PAGE @@ -75,6 +78,7 @@ export default function IntelPage() { const [tldData, setTldData] = useState([]) const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) const [refreshing, setRefreshing] = useState(false) const [total, setTotal] = useState(0) @@ -86,27 +90,39 @@ export default function IntelPage() { const loadData = useCallback(async () => { setLoading(true) + setError(null) try { const response = await api.getTldOverview(500, 0, 'popularity') + console.log('TLD API Response:', response) + + if (!response || !response.tlds) { + setError('No TLD data available') + setTldData([]) + setTotal(0) + return + } + const mapped: TLDData[] = (response.tlds || []).map((tld: any) => ({ - 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, + tld: tld.tld || '', + min_price: tld.min_registration_price || 0, + avg_price: tld.avg_registration_price || 0, + max_price: tld.max_registration_price || 0, + min_renewal_price: tld.min_renewal_price || 0, + avg_renewal_price: tld.avg_renewal_price || 0, + price_change_7d: tld.price_change_7d || 0, + price_change_1y: tld.price_change_1y || 0, + price_change_3y: tld.price_change_3y || 0, + risk_level: tld.risk_level || 'low', + 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) + setTotal(response.total || mapped.length) + } catch (err: any) { + console.error('Failed to load TLD data:', err) + setError(err.message || 'Failed to load TLD data') + setTldData([]) } finally { setLoading(false) } @@ -132,7 +148,7 @@ export default function IntelPage() { }, [sortField, canSeeRenewal, canSee3yTrend]) const filteredData = useMemo(() => { - let data = tldData + let data = [...tldData] const techTlds = ['ai', 'io', 'app', 'dev', 'tech', 'cloud', 'digital', 'software', 'code', 'systems', 'network', 'data', 'cyber', 'online', 'web', 'api', 'hosting'] if (filterType === 'tech') data = data.filter(t => techTlds.includes(t.tld) || t.tld === 'io' || t.tld === 'ai') @@ -147,13 +163,13 @@ export default function IntelPage() { data.sort((a, b) => { switch (sortField) { case 'tld': return mult * a.tld.localeCompare(b.tld) - case 'price': return mult * (a.min_price - b.min_price) - case 'renewal': return mult * (a.min_renewal_price - b.min_renewal_price) + case 'price': return mult * ((a.min_price || 0) - (b.min_price || 0)) + case 'renewal': return mult * ((a.min_renewal_price || 0) - (b.min_renewal_price || 0)) case 'change': return mult * ((a.price_change_1y || 0) - (b.price_change_1y || 0)) case 'change3y': return mult * ((a.price_change_3y || 0) - (b.price_change_3y || 0)) case 'risk': - const riskMap = { low: 1, medium: 2, high: 3 } - return mult * (riskMap[a.risk_level] - riskMap[b.risk_level]) + const riskMap: Record = { low: 1, medium: 2, high: 3 } + return mult * ((riskMap[a.risk_level] || 1) - (riskMap[b.risk_level] || 1)) case 'popularity': return mult * ((a.popularity_rank || 999) - (b.popularity_rank || 999)) default: return 0 } @@ -163,11 +179,12 @@ export default function IntelPage() { }, [tldData, filterType, searchQuery, sortField, sortDirection]) const stats = useMemo(() => { - const lowest = tldData.length > 0 ? Math.min(...tldData.map(t => t.min_price)) : 0 - const hottest = tldData.reduce((prev, current) => (prev.price_change_1y > current.price_change_1y) ? prev : current, tldData[0] || {}) + if (!tldData.length) return { lowest: 0, traps: 0, avgRenewal: 0 } + const prices = tldData.map(t => t.min_price).filter(p => p > 0) + const lowest = prices.length > 0 ? Math.min(...prices) : 0 const traps = tldData.filter(t => t.risk_level === 'high').length - const avgRenewal = tldData.length > 0 ? tldData.reduce((sum, t) => sum + t.min_renewal_price, 0) / tldData.length : 0 - return { lowest, hottest, traps, avgRenewal } + const avgRenewal = tldData.length > 0 ? tldData.reduce((sum, t) => sum + (t.min_renewal_price || 0), 0) / tldData.length : 0 + return { lowest, traps, avgRenewal } }, [tldData]) return ( @@ -284,6 +301,14 @@ export default function IntelPage() {
+ ) : error ? ( +
+
+ +
+

{error}

+ +
) : filteredData.length === 0 ? (
@@ -339,7 +364,7 @@ export default function IntelPage() { {filteredData.map((tld) => { - const isTrap = tld.min_renewal_price > tld.min_price * 1.5 + const isTrap = (tld.min_renewal_price || 0) > (tld.min_price || 1) * 1.5 const trend = tld.price_change_1y || 0 const trend3y = tld.price_change_3y || 0 diff --git a/frontend/src/app/terminal/listing/page.tsx b/frontend/src/app/terminal/listing/page.tsx index 2ffa242..e195184 100755 --- a/frontend/src/app/terminal/listing/page.tsx +++ b/frontend/src/app/terminal/listing/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useEffect, useState, useMemo, useCallback } from 'react' +import { useEffect, useState, useCallback } from 'react' import { useSearchParams } from 'next/navigation' import { useStore } from '@/lib/store' import { api } from '@/lib/api' -import { TerminalLayout } from '@/components/TerminalLayout' +import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { Plus, Shield, @@ -16,7 +16,6 @@ import { CheckCircle, AlertCircle, Copy, - RefreshCw, DollarSign, X, Tag, @@ -24,71 +23,12 @@ import { ArrowRight, TrendingUp, Globe, - MoreHorizontal, - Crown + Crown, + Check } from 'lucide-react' import Link from 'next/link' import clsx from 'clsx' -// ============================================================================ -// SHARED COMPONENTS -// ============================================================================ - -function Tooltip({ children, content }: { children: React.ReactNode; content: string }) { - return ( -
- {children} -
- {content} -
-
-
- ) -} - -function StatCard({ - label, - value, - subValue, - icon: Icon, - trend -}: { - label: string - value: string | number - subValue?: string - icon: any - trend?: 'up' | 'down' | 'neutral' | 'active' -}) { - return ( -
-
- -
-
-
- - {label} -
-
- {value} - {subValue && {subValue}} -
- {trend && ( -
- {trend === 'active' ? '● LIVE MONITORING' : trend === 'up' ? '▲ POSITIVE' : '▼ NEGATIVE'} -
- )} -
-
- ) -} - // ============================================================================ // TYPES // ============================================================================ @@ -151,7 +91,6 @@ export default function MyListingsPage() { const [listings, setListings] = useState([]) const [loading, setLoading] = useState(true) - // Modals const [showCreateModal, setShowCreateModal] = useState(false) const [showVerifyModal, setShowVerifyModal] = useState(false) const [showInquiriesModal, setShowInquiriesModal] = useState(false) @@ -163,8 +102,8 @@ export default function MyListingsPage() { const [creating, setCreating] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) + const [copied, setCopied] = useState(false) - // Create form state const [newListing, setNewListing] = useState({ domain: '', title: '', @@ -174,6 +113,16 @@ export default function MyListingsPage() { allow_offers: true, }) + const tier = subscription?.tier || 'scout' + const limits: Record = { scout: 0, trader: 5, tycoon: 50 } + const maxListings = limits[tier] || 0 + const canList = tier !== 'scout' + const isTycoon = tier === 'tycoon' + + const activeCount = listings.filter(l => l.status === 'active').length + const totalViews = listings.reduce((sum, l) => sum + l.view_count, 0) + const totalInquiries = listings.reduce((sum, l) => sum + l.inquiry_count, 0) + const loadListings = useCallback(async () => { setLoading(true) try { @@ -214,7 +163,7 @@ export default function MyListingsPage() { allow_offers: newListing.allow_offers, }), }) - setSuccess('Listing created! Now verify ownership to publish.') + setSuccess('Listing created! Verify ownership to publish.') setShowCreateModal(false) setNewListing({ domain: '', title: '', description: '', asking_price: '', price_type: 'negotiable', allow_offers: true }) loadListings() @@ -268,7 +217,7 @@ export default function MyListingsPage() { ) if (result.verified) { - setSuccess('Domain verified! You can now publish your listing.') + setSuccess('Domain verified! You can now publish.') setShowVerifyModal(false) loadListings() } else { @@ -308,8 +257,8 @@ export default function MyListingsPage() { const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text) - setSuccess('Copied to clipboard!') - setTimeout(() => setSuccess(null), 2000) + setCopied(true) + setTimeout(() => setCopied(false), 2000) } const formatPrice = (price: number | null, currency: string) => { @@ -321,569 +270,497 @@ export default function MyListingsPage() { }).format(price) } - // Tier limits (from pounce_pricing.md: Trader=5, Tycoon=50, Scout=0) - const tier = subscription?.tier || 'scout' - const limits = { scout: 0, trader: 5, tycoon: 50 } - const maxListings = limits[tier as keyof typeof limits] || 0 - const canList = tier !== 'scout' - const isTycoon = tier === 'tycoon' - - const activeCount = listings.filter(l => l.status === 'active').length - const totalViews = listings.reduce((sum, l) => sum + l.view_count, 0) - const totalInquiries = listings.reduce((sum, l) => sum + l.inquiry_count, 0) - return ( - -
- - {/* Ambient Background Glow */} -
-
-
-
- -
- - {/* Header Section */} -
-
-
-
-

For Sale

-
-

- List your domains on the Pounce Marketplace. 0% commission, instant visibility. -

+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* HEADER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+
+ + Marketplace
+

+ For Sale + {listings.length}/{maxListings} +

+ +

+ List domains on Pounce Marketplace. 0% commission. +

+
+ +
+ {canList && ( +
+
+
{activeCount}
+
Active
+
+
+
{totalViews}
+
Views
+
+
+
{totalInquiries}
+
Inquiries
+
+
+ )} +
+
+
- {/* Messages */} - {error && ( -
- -

{error}

- -
- )} - - {success && ( -
- -

{success}

- -
- )} + {/* Messages */} + {error && ( +
+ +

{error}

+ +
+ )} + + {success && ( +
+ +

{success}

+ +
+ )} - {/* Paywall */} - {!canList && ( -
-
-
- -

Unlock Domain Selling

-

- List your domains with 0% commission on the Pounce Marketplace. -

- - {/* Plan comparison */} -
-
-
- - Trader -
-

$9/month

-

5 Listings

-
-
-
- - Tycoon -
-

$29/month

-

50 Listings

-

+ Featured Badge

-
-
- - - Upgrade Now - -
-
- )} - - {/* Stats Grid */} - {canList && ( -
- - - 0 ? 'up' : 'neutral'} - /> - 0 ? 'up' : 'neutral'} - /> -
- )} - - {/* Listings Table */} - {canList && ( -
- {/* Table Header */} -
-
Domain
-
Status
-
Price
-
Views
-
Inquiries
-
Actions
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* PAYWALL */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {!canList && ( +
+
+ +

Unlock Domain Selling

+

+ List your domains with 0% commission on Pounce Marketplace. +

+ +
+
+ +
Trader
+
$9/mo
+
5 Listings
+
+ +
Tycoon
+
$29/mo
+
50 Listings
+
+ Featured Badge
+
+
- {loading ? ( -
- -
- ) : listings.length === 0 ? ( -
-
- + + Upgrade Now + +
+
+ )} + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* LISTINGS TABLE */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {canList && ( +
+ {loading ? ( +
+ +
+ ) : listings.length === 0 ? ( +
+
+ +
+

No listings yet

+

Create your first listing to start selling

+ +
+ ) : ( +
+ {listings.map((listing) => ( +
+
+
+
+ {listing.domain.charAt(0).toUpperCase()} + {isTycoon && listing.status === 'active' && ( +
+ +
+ )} +
+ +
+
+ {listing.domain} + {listing.is_verified && } + + {listing.status} + +
+
{listing.title || 'No headline'}
+
+
+ +
+
+
{formatPrice(listing.asking_price, listing.currency)}
+ {listing.pounce_score &&
Score: {listing.pounce_score}
} +
+ +
+ + + {listing.view_count} + + +
+ +
+ {!listing.is_verified ? ( + + ) : listing.status === 'draft' ? ( + + ) : ( + + + + )} + + +
+
-

No listings yet

-

- Create your first listing to start selling. -

-
+ ))} +
+ )} +
+ )} + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* CREATE MODAL */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {showCreateModal && ( +
setShowCreateModal(false)}> +
e.stopPropagation()}> +
+

Create Listing

+

List your domain for sale

+
+ +
+
+ + setNewListing({ ...newListing, domain: e.target.value })} + placeholder="example.com" + className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono" + /> +
+ +
+ + setNewListing({ ...newListing, title: e.target.value })} + placeholder="Short, catchy title" + className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50" + /> +
+ +
+
+ +
+ + setNewListing({ ...newListing, asking_price: e.target.value })} + placeholder="Make Offer" + className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono" + /> +
+
+ +
+ + +
+
+ + + +
+ + +
+
+
+
+ )} + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* VERIFY MODAL */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {showVerifyModal && verificationInfo && selectedListing && ( +
setShowVerifyModal(false)}> +
e.stopPropagation()}> +
+

Verify Ownership

+

+ Add this DNS TXT record to {selectedListing.domain} +

+
+ +
+
+
+
Type
+
+ {verificationInfo.dns_record_type} +
+
+
+
Name / Host
+
copyToClipboard(verificationInfo.dns_record_name)} + > + {verificationInfo.dns_record_name} + {copied ? : } +
+
+
+ +
+
Value
+
copyToClipboard(verificationInfo.dns_record_value)} + > + {verificationInfo.dns_record_value} + {copied ? : } +
+
+ +
+

+ After adding the record, it may take up to 24 hours to propagate. Click verify to check. +

+
+
+ +
+ + +
+
+
+ )} + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* INQUIRIES MODAL */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {showInquiriesModal && selectedListing && ( +
setShowInquiriesModal(false)}> +
e.stopPropagation()}> +
+
+

Inquiries

+

+ {inquiries.length} for {selectedListing.domain} +

+
+ +
+ +
+ {loadingInquiries ? ( +
+ +
+ ) : inquiries.length === 0 ? ( +
+ +

No inquiries yet

) : ( -
- {listings.map((listing) => ( -
- - {/* Mobile View */} -
-
-
-
{listing.domain}
-
{listing.title || 'No headline'}
-
-
-
{formatPrice(listing.asking_price, listing.currency)}
-
-
-
- - {listing.status} - -
- - {!listing.is_verified && } -
-
+ inquiries.map((inquiry) => ( +
+
+
+
{inquiry.name}
+
{inquiry.email}
+ {inquiry.company &&
{inquiry.company}
}
- - {/* Desktop View */} -
-
-
- {listing.domain.charAt(0).toUpperCase()} - {/* Featured Badge for Tycoon */} - {isTycoon && listing.status === 'active' && ( -
- -
- )} -
-
-
- {listing.domain} - {listing.is_verified && ( - - )} -
-
{listing.title || 'No headline'}
-
+
+ {inquiry.offer_amount && ( +
+ ${inquiry.offer_amount.toLocaleString()} +
+ )} +
+ {new Date(inquiry.created_at).toLocaleDateString()}
- -
- - - {listing.status} - -
- -
-
{formatPrice(listing.asking_price, listing.currency)}
- {listing.pounce_score &&
Score: {listing.pounce_score}
} -
- -
-
{listing.view_count}
-
- -
- -
- -
- {!listing.is_verified ? ( - - - - ) : listing.status === 'draft' ? ( - - - - ) : ( - - - - - - )} - - - - -
-
- ))} -
+

+ {inquiry.message} +

+
+ + {inquiry.status} + + + Reply + +
+
+ )) )}
- )} - +
- - {/* Create Modal */} - {showCreateModal && ( -
-
-
-

Create Listing

-

List your domain for sale on the marketplace

-
- -
-
- - setNewListing({ ...newListing, domain: e.target.value })} - placeholder="example.com" - className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:bg-white/10 transition-all font-mono" - /> -
- -
- - setNewListing({ ...newListing, title: e.target.value })} - placeholder="Short, catchy title (e.g. Perfect for AI Startups)" - className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" - /> -
- -
-
- -
- - setNewListing({ ...newListing, asking_price: e.target.value })} - placeholder="Make Offer" - className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all font-mono" - /> -
-
- -
- - -
-
- - - -
- - -
-
-
-
- )} - - {/* Verify Modal */} - {showVerifyModal && verificationInfo && selectedListing && ( -
-
-
-

Verify Ownership

-

- Add this DNS TXT record to {selectedListing.domain} to prove you own it. -

-
- -
-
-
-
Type
-
- {verificationInfo.dns_record_type} -
-
-
-
Name / Host
-
copyToClipboard(verificationInfo.dns_record_name)}> - {verificationInfo.dns_record_name} - -
-
-
- -
-
Value
-
copyToClipboard(verificationInfo.dns_record_value)}> - {verificationInfo.dns_record_value} - -
-
- -
-

- - After adding the record, it may take up to 24 hours to propagate, though typically it's instant. Click verify below to check. -

-
-
- -
- - -
-
-
- )} - - {/* Inquiries Modal */} - {showInquiriesModal && selectedListing && ( -
-
-
-
-

Inquiries

-

- {inquiries.length} inquiry{inquiries.length !== 1 ? 'ies' : ''} for {selectedListing.domain} -

-
- -
- -
- {loadingInquiries ? ( -
- -
- ) : inquiries.length === 0 ? ( -
- -

No inquiries yet

-
- ) : ( - inquiries.map((inquiry) => ( -
-
-
-
{inquiry.name}
-
{inquiry.email}
- {inquiry.company &&
{inquiry.company}
} -
-
- {inquiry.offer_amount && ( -
- ${inquiry.offer_amount.toLocaleString()} -
- )} -
- {new Date(inquiry.created_at).toLocaleDateString()} -
-
-
-

- {inquiry.message} -

-
- - {inquiry.status} - - - Reply via Email - -
-
- )) - )} -
- -
- -
-
-
- )} -
- + )} + ) } - -function InfoIcon(props: any) { - return ( - - ) -}