From 5857123ed5c5e926a5e0c0eb77351ee29b3e2ecc Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Thu, 11 Dec 2025 08:40:18 +0100 Subject: [PATCH] style: Unify Terminal backgrounds & redesign TLD detail page - Remove all hardcoded 'bg-black' backgrounds from Terminal pages - Let global background with emerald glow shine through - Redesign TLD detail page to match Market/Radar/Intel style - Use consistent StatCards, glassmorphism containers, and spacing - Improve Quick Check section on TLD detail page - Unify Watchlist and Listing page backgrounds for consistency --- .../src/app/terminal/intel/[tld]/page.tsx | 771 +++++++-------- frontend/src/app/terminal/listing/page.tsx | 839 +++++++++------- frontend/src/app/terminal/watchlist/page.tsx | 900 ++++++++++-------- 3 files changed, 1387 insertions(+), 1123 deletions(-) diff --git a/frontend/src/app/terminal/intel/[tld]/page.tsx b/frontend/src/app/terminal/intel/[tld]/page.tsx index f59b06a..f6cd676 100644 --- a/frontend/src/app/terminal/intel/[tld]/page.tsx +++ b/frontend/src/app/terminal/intel/[tld]/page.tsx @@ -3,7 +3,6 @@ import { useEffect, useState, useMemo, useRef } from 'react' import { useParams } from 'next/navigation' import { TerminalLayout } from '@/components/TerminalLayout' -import { PageContainer, StatCard } from '@/components/PremiumTable' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { @@ -24,10 +23,69 @@ import { DollarSign, BarChart3, Shield, + Loader2, + Info, + ChevronDown } 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}} +
+
+
+ +
+
+ ) +} + +// ============================================================================ +// TYPES & DATA +// ============================================================================ + interface TldDetails { tld: string type: string @@ -48,7 +106,6 @@ interface TldDetails { transfer_price: number }> cheapest_registrar: string - // New fields from table min_renewal_price: number price_change_1y: number price_change_3y: number @@ -79,7 +136,6 @@ interface DomainCheckResult { 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=', @@ -92,7 +148,10 @@ const REGISTRAR_URLS: Record = { type ChartPeriod = '1M' | '3M' | '1Y' | 'ALL' -// Premium Chart Component with real data +// ============================================================================ +// SUB-COMPONENTS +// ============================================================================ + function PriceChart({ data, chartStats, @@ -105,7 +164,7 @@ function PriceChart({ if (data.length === 0) { return ( -
+
No price history available
) @@ -124,17 +183,17 @@ function PriceChart({ 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' + const isRising = data[data.length - 1].price >= data[0].price + const strokeColor = isRising ? '#10b981' : '#f43f5e' // emerald-500 : rose-500 return (
setHoveredIndex(null)} > { @@ -147,46 +206,59 @@ function PriceChart({ > - - + + - + {hoveredIndex !== null && points[hoveredIndex] && ( - + + + + )} {/* Tooltip */} {hoveredIndex !== null && points[hoveredIndex] && (
-

${points[hoveredIndex].price.toFixed(2)}

-

{new Date(points[hoveredIndex].date).toLocaleDateString()}

+
+ ${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)} -
) } +// ============================================================================ +// MAIN PAGE +// ============================================================================ + export default function CommandTldDetailPage() { const params = useParams() const { fetchSubscription } = useStore() @@ -213,7 +285,7 @@ export default function CommandTldDetailPage() { 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 + api.getTldOverview(1, 0, 'popularity', tld), ]) if (historyData && compareData) { @@ -221,7 +293,6 @@ export default function CommandTldDetailPage() { a.registration_price - b.registration_price ) - // Get additional data from overview API (1y, 3y change, risk) const tldFromOverview = overviewData?.tlds?.[0] setDetails({ @@ -239,7 +310,6 @@ export default function CommandTldDetailPage() { }, 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, @@ -316,7 +386,6 @@ export default function CommandTldDetailPage() { return baseUrl } - // Calculate renewal trap info const getRenewalInfo = () => { if (!details?.registrars?.length) return null const cheapest = details.registrars[0] @@ -331,23 +400,22 @@ export default function CommandTldDetailPage() { const renewalInfo = getRenewalInfo() - // Risk badge component const getRiskBadge = () => { if (!details) return null const level = details.risk_level const reason = details.risk_reason return ( {reason} @@ -356,366 +424,307 @@ export default function CommandTldDetailPage() { if (loading) { return ( - - -
- -
-
+ +
+ +
) } if (error || !details) { return ( - - -
-
- -
-

TLD Not Found

-

{error || `The TLD .${tld} could not be found.`}

- - - Back to TLD Pricing - -
-
+ +
+ +

TLD Not Found

+

The extension .{tld} is not currently tracked.

+ + Back to Intelligence + +
) } return ( - - - {/* Breadcrumb */} - - - {/* 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} - /> -
+ +
+ + {/* Ambient Background Glow */} +
+
+
- {/* Risk Level */} -
- -
-

Risk Assessment

-

Based on renewal ratio, price volatility, and market trends

-
- {getRiskBadge()} -
+
+ + {/* Header Section */} +
+
+ {/* Breadcrumb */} + - {/* 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) => ( - - ))} -
-
- -
- -
- - {/* Chart Stats */} -
-
-

Period High

-

${chartStats.high.toFixed(2)}

-
-
-

Average

-

${chartStats.avg.toFixed(2)}

-
-
-

Period Low

-

${chartStats.low.toFixed(2)}

-
-
-
- - {/* Registrar Comparison */} -
-

Registrar Comparison

- -
- - - - - - - - - - - - {details.registrars.map((registrar, idx) => { - const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5 - const isBestValue = idx === 0 && !hasRenewalTrap - - return ( - - - - - - - - ) - })} - -
RegistrarRegisterRenewTransfer
-
- {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" - /> -
- -
- - {/* Result */} - {domainResult && ( -
-
- {domainResult.is_available ? ( - - ) : ( - - )} +
+
-

{domainResult.domain}

-

- {domainResult.is_available ? 'Available for registration!' : 'Already registered'} -

+

+ .{details.tld} + {getRiskBadge()} +

+

+ {details.description} +

- - {domainResult.is_available && ( - + + - )} -
- - {/* TLD Info */} -
-

TLD Information

- -
-
-
- - Type -
-

{details.type}

-
-
-
- - Registry -
-

{details.registry}

-
-
-
- - Introduced -
-

{details.introduced || 'Unknown'}

-
-
-
- - Registrars -
-

{details.registrars.length} tracked

+ Back +
+ + {/* Metric Grid */} +
+ + + 0 ? '+' : ''}${details.price_change_1y.toFixed(0)}%`} + subValue="Volatility" + icon={details.price_change_1y > 0 ? TrendingUp : TrendingDown} + trend={details.price_change_1y > 10 ? 'down' : details.price_change_1y < -10 ? 'up' : 'neutral'} + /> + +
+ + {/* Quick Check Bar */} +
+
+
+
+

Check Availability

+

Instantly check if your desired .{details.tld} domain is available across all registrars.

+
+ +
+
+ setDomainSearch(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleDomainCheck()} + placeholder={`example.${details.tld}`} + className="w-full h-12 bg-black/50 border border-white/10 rounded-lg pl-4 pr-4 text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all font-mono" + /> +
+ +
+
+ + {/* Check Result */} + {domainResult && ( +
+
+
+ {domainResult.is_available ? ( +
+ ) : ( +
+ )} +
+
{domainResult.domain}
+
+ {domainResult.is_available ? 'Available for registration' : 'Already Registered'} +
+
+
+ + {domainResult.is_available && ( + + Buy at {details.cheapest_registrar} + + )} +
+
+ )} +
+ +
+ + {/* Left Column: Chart & Info */} +
+ + {/* Price History Chart */} +
+
+
+

Price History

+

Historical registration price trends

+
+
+ {(['1M', '3M', '1Y', 'ALL'] as ChartPeriod[]).map((period) => ( + + ))} +
+
+ +
+ +
+ +
+
+
High
+
${chartStats.high.toFixed(2)}
+
+
+
Average
+
${chartStats.avg.toFixed(2)}
+
+
+
Low
+
${chartStats.low.toFixed(2)}
+
+
+
+ + {/* TLD Info Cards */} +
+
+
+ + Type +
+
{details.type}
+
+
+
+ + Registry +
+
{details.registry}
+
+
+ +
+ + {/* Right Column: Registrars Table */} +
+
+

Registrar Prices

+

Live comparison sorted by price

+
+ +
+ + + + + + + + + + + {details.registrars.map((registrar, idx) => { + const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5 + const isBest = idx === 0 && !hasRenewalTrap + + return ( + + + + + + + ) + })} + +
RegistrarRegRenew
+
{registrar.name}
+ {isBest && Best Value} + {idx === 0 && hasRenewalTrap && Renewal Trap} +
+
+ ${registrar.registration_price.toFixed(2)} +
+
+
+ ${registrar.renewal_price.toFixed(2)} +
+
+ + + +
+
+
+ +
+
- +
) } diff --git a/frontend/src/app/terminal/listing/page.tsx b/frontend/src/app/terminal/listing/page.tsx index 5edc41a..d37e016 100755 --- a/frontend/src/app/terminal/listing/page.tsx +++ b/frontend/src/app/terminal/listing/page.tsx @@ -5,7 +5,6 @@ import { useSearchParams } from 'next/navigation' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { TerminalLayout } from '@/components/TerminalLayout' -import { PageContainer, StatCard, Badge, ActionButton } from '@/components/PremiumTable' import { Plus, Shield, @@ -23,10 +22,77 @@ import { Tag, Store, Sparkles, + ArrowRight, + TrendingUp, + Globe, + MoreHorizontal } 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 +// ============================================================================ + interface Listing { id: number domain: string @@ -60,6 +126,10 @@ interface VerificationInfo { status: string } +// ============================================================================ +// MAIN PAGE +// ============================================================================ + export default function MyListingsPage() { const { subscription } = useStore() const searchParams = useSearchParams() @@ -68,7 +138,7 @@ export default function MyListingsPage() { const [listings, setListings] = useState([]) const [loading, setLoading] = useState(true) - // Modals - auto-open if domain is prefilled + // Modals const [showCreateModal, setShowCreateModal] = useState(false) const [showVerifyModal, setShowVerifyModal] = useState(false) const [selectedListing, setSelectedListing] = useState(null) @@ -78,7 +148,7 @@ export default function MyListingsPage() { const [error, setError] = useState(null) const [success, setSuccess] = useState(null) - // Create form + // Create form state const [newListing, setNewListing] = useState({ domain: '', title: '', @@ -104,7 +174,6 @@ export default function MyListingsPage() { loadListings() }, [loadListings]) - // Auto-open create modal if domain is prefilled from portfolio useEffect(() => { if (prefillDomain) { setNewListing(prev => ({ ...prev, domain: prefillDomain })) @@ -208,6 +277,7 @@ export default function MyListingsPage() { const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text) setSuccess('Copied to clipboard!') + setTimeout(() => setSuccess(null), 2000) } const formatPrice = (price: number | null, currency: string) => { @@ -219,390 +289,443 @@ export default function MyListingsPage() { }).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} - } - - // Tier limits as per concept: Scout = 0 (blocked), Trader = 5, Tycoon = 50 + // Tier limits 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 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 ( - - - - Browse Marketplace - - + +
+ + {/* Ambient Background Glow */} +
+
+
- } - > - - {/* Scout Paywall */} - {!canList && ( -
- -

Upgrade to List Domains

-

- The Pounce marketplace is exclusive to Trader and Tycoon members. - List your domains, get verified, and sell directly to buyers with 0% commission. -

-
+ +
+ + {/* Header Section */} +
+
+
+
+

Portfolio

+
+

+ Manage your domain inventory, track performance, and process offers. +

+
+ +
- Upgrade to Trader • $9/mo + Marketplace +
- )} - {/* Messages */} - {error && ( -
- -

{error}

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

{success}

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

{error}

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

{success}

+ +
+ )} - {/* Stats - only show if can list */} - {canList && ( -
- - l.status === 'active').length} - icon={CheckCircle} - accent - /> - sum + l.view_count, 0)} - icon={Eye} - /> - sum + l.inquiry_count, 0)} - icon={MessageSquare} - /> -
- )} + {/* Paywall */} + {!canList && ( +
+
+
+ +

Unlock Portfolio Management

+

+ List your domains, verify ownership automatically, and sell directly to buyers with 0% commission on the Pounce Marketplace. +

+ + Upgrade to Trader + +
+
+ )} - {/* Listings */} - {canList && ( - loading ? ( -
- -
- ) : listings.length === 0 ? ( -
- -

No Listings Yet

-

- Create your first listing to sell a domain on the Pounce marketplace. -

- -
- ) : ( -
- {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 && ( - - )} - - {listing.is_verified && listing.status === 'draft' && ( - - )} - - {listing.status === 'active' && ( - - - View - - )} - - -
+ {/* Stats Grid */} + {canList && ( +
+ + + 0 ? 'up' : 'neutral'} + /> + 0 ? 'up' : 'neutral'} + /> +
+ )} + + {/* Listings Table */} + {canList && ( +
+ {/* Table Header */} +
+
Domain
+
Status
+
Price
+
Views
+
Actions
+
+ + {loading ? ( +
+
-
- ))} -
- ) - )} - - - {/* Create Modal */} - {showCreateModal && ( -
-
-

List Domain for Sale

- -
-
- - setNewListing({ ...newListing, domain: e.target.value })} - placeholder="example.com" - className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent" - /> -
- -
- - setNewListing({ ...newListing, title: e.target.value })} - placeholder="Perfect for AI startups" - className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent" - /> -
- -
- -