+
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 */}
-
-
- TLD Pricing
-
-
- .{details.tld}
-
-
- {/* Stats Grid - All info from table */}
-
-
-
-
-
-
-
-
- 0 ? '+' : ''}${details.price_change_1y.toFixed(0)}%`}
- icon={details.price_change_1y > 0 ? TrendingUp : details.price_change_1y < 0 ? TrendingDown : Minus}
- />
-
-
- 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%`}
- icon={BarChart3}
- />
-
+
+
+
+ {/* Ambient Background Glow */}
+
- {/* Risk Level */}
-
-
-
-
Risk Assessment
-
Based on renewal ratio, price volatility, and market trends
-
- {getRiskBadge()}
-
+
+
+ {/* Header Section */}
+
+
+ {/* Breadcrumb */}
+
+
+ Intelligence
+
+
+ .{details.tld}
+
- {/* Renewal Trap Warning */}
- {renewalInfo?.isTrap && (
-
-
-
-
Renewal Trap Detected
-
- The renewal price (${renewalInfo.renewal.toFixed(2)}) is {renewalInfo.ratio.toFixed(1)}x higher than the registration price (${renewalInfo.registration.toFixed(2)}).
- Consider the total cost of ownership before registering.
-
-
-
- )}
-
- {/* Price Chart */}
-
-
-
Price History
-
- {(['1M', '3M', '1Y', 'ALL'] as ChartPeriod[]).map((period) => (
- setChartPeriod(period)}
- className={clsx(
- "px-3 py-1.5 text-xs font-medium rounded-md transition-all",
- chartPeriod === period
- ? "bg-accent text-background"
- : "text-foreground-muted hover:text-foreground"
- )}
- >
- {period}
-
- ))}
-
-
-
-
-
- {/* Chart Stats */}
-
-
-
Period High
-
${chartStats.high.toFixed(2)}
-
-
-
Average
-
${chartStats.avg.toFixed(2)}
-
-
-
Period Low
-
${chartStats.low.toFixed(2)}
-
-
-
-
- {/* Registrar Comparison */}
-
-
Registrar Comparison
-
-
-
-
-
- Registrar
- Register
- Renew
- Transfer
-
-
-
-
- {details.registrars.map((registrar, idx) => {
- const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5
- const isBestValue = idx === 0 && !hasRenewalTrap
-
- return (
-
-
-
- {registrar.name}
- {isBestValue && (
-
- Best
-
- )}
- {idx === 0 && hasRenewalTrap && (
-
- Cheap Start
-
- )}
-
-
-
-
- ${registrar.registration_price.toFixed(2)}
-
-
-
-
-
- ${registrar.renewal_price.toFixed(2)}
-
- {hasRenewalTrap && (
-
-
-
- )}
-
-
-
-
- ${registrar.transfer_price.toFixed(2)}
-
-
-
-
- Visit
-
-
-
-
- )
- })}
-
-
-
-
-
- {/* Quick Domain Check */}
-
-
Quick Domain Check
-
- Check if a domain is available with .{tld}
-
-
-
-
- setDomainSearch(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && handleDomainCheck()}
- placeholder={`example or example.${tld}`}
- className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl
- text-sm text-foreground placeholder:text-foreground-subtle
- focus:outline-none focus:border-accent/50 transition-all"
- />
-
-
- {checkingDomain ? (
-
- ) : (
- 'Check'
- )}
-
-
-
- {/* Result */}
- {domainResult && (
-
-
- {domainResult.is_available ? (
-
- ) : (
-
- )}
+
+
-
{domainResult.domain}
-
- {domainResult.is_available ? 'Available for registration!' : 'Already registered'}
-
+
+ .{details.tld}
+ {getRiskBadge()}
+
+
+ {details.description}
+
-
- {domainResult.is_available && (
-
+
+
+
- Register at {details.cheapest_registrar}
-
-
- )}
-
- )}
-
-
- {/* TLD Info */}
-
-
TLD Information
-
-
-
-
-
- Type
-
-
{details.type}
-
-
-
-
- Registry
-
-
{details.registry}
-
-
-
-
- Introduced
-
-
{details.introduced || 'Unknown'}
-
-
-
-
- Registrars
-
-
{details.registrars.length} tracked
+
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"
+ />
+
+
+ {checkingDomain ? : 'Check'}
+
+
+
+
+ {/* 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) => (
+ setChartPeriod(period)}
+ className={clsx(
+ "px-3 py-1 text-[10px] font-bold rounded transition-all",
+ chartPeriod === period
+ ? "bg-zinc-800 text-white shadow-sm"
+ : "text-zinc-500 hover:text-zinc-300"
+ )}
+ >
+ {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
+
+
+
+
+
+
+ Registrar
+ Reg
+ Renew
+
+
+
+
+ {details.registrars.map((registrar, idx) => {
+ const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5
+ const isBest = idx === 0 && !hasRenewalTrap
+
+ return (
+
+
+ {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 (
+
+ )
+}
+
+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
-
- setShowCreateModal(true)}
- disabled={listings.length >= maxListings}
- className="flex items-center gap-2 px-4 py-2 bg-accent text-background text-sm font-medium rounded-lg
- hover:bg-accent-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
- List Domain
-
+
+
+
+ {/* 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 */}
+
+
+
+
+ Manage your domain inventory, track performance, and process offers.
+
+
+
+
- Upgrade to Trader • $9/mo
+
Marketplace
+
setShowCreateModal(true)}
+ disabled={listings.length >= maxListings}
+ className="px-4 py-2 bg-emerald-500 text-white font-medium rounded-lg hover:bg-emerald-400 transition-all shadow-lg shadow-emerald-500/20 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
+ >
+ New Listing
+
- )}
- {/* Messages */}
- {error && (
-
-
-
{error}
-
setError(null)}>
-
- )}
-
- {success && (
-
-
-
{success}
-
setSuccess(null)}>
-
- )}
+ {/* Messages */}
+ {error && (
+
+
+
{error}
+
setError(null)}>
+
+ )}
+
+ {success && (
+
+
+
{success}
+
setSuccess(null)}>
+
+ )}
- {/* 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.
-
-
setShowCreateModal(true)}
- className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
- >
-
- Create Listing
-
-
- ) : (
-
- {listings.map((listing) => (
-
-
- {/* Domain Info */}
-
-
-
{listing.domain}
- {getStatusBadge(listing.status, listing.is_verified)}
- {listing.is_verified && (
-
-
-
- )}
-
- {listing.title && (
-
{listing.title}
- )}
-
-
- {/* Price */}
-
-
- {formatPrice(listing.asking_price, listing.currency)}
-
- {listing.pounce_score && (
-
Score: {listing.pounce_score}
- )}
-
-
- {/* Stats */}
-
-
- {listing.view_count}
-
-
- {listing.inquiry_count}
-
-
-
- {/* Actions */}
-
- {!listing.is_verified && (
- handleStartVerification(listing)}
- disabled={verifying}
- className="flex items-center gap-1.5 px-3 py-2 bg-amber-500/10 text-amber-400 text-sm font-medium rounded-lg hover:bg-amber-500/20 transition-all"
- >
-
- Verify
-
- )}
-
- {listing.is_verified && listing.status === 'draft' && (
- handlePublish(listing)}
- className="flex items-center gap-1.5 px-3 py-2 bg-accent text-background text-sm font-medium rounded-lg hover:bg-accent-hover transition-all"
- >
-
- Publish
-
- )}
-
- {listing.status === 'active' && (
-
-
- View
-
- )}
-
- handleDelete(listing)}
- className="p-2 text-foreground-subtle hover:text-red-400 transition-colors"
- >
-
-
-
+ {/* 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
-
-
- )}
+ )}
- {/* Verification Modal */}
- {showVerifyModal && verificationInfo && selectedListing && (
-
-
-
Verify Domain Ownership
-
- Add a DNS TXT record to prove you own {selectedListing.domain}
-
-
-
-
-
Record Type
-
{verificationInfo.dns_record_type}
-
+ {/* Verify Modal */}
+ {showVerifyModal && verificationInfo && selectedListing && (
+
+
+
+
Verify Ownership
+
+ Add this DNS TXT record to {selectedListing.domain} to prove you own it.
+
+
-
-
Name / Host
-
-
{verificationInfo.dns_record_name}
-
copyToClipboard(verificationInfo.dns_record_name)}
- className="p-2 text-foreground-subtle hover:text-accent transition-colors"
- >
-
-
+
+
+
+
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.
+
-
-
Value
-
-
{verificationInfo.dns_record_value}
-
copyToClipboard(verificationInfo.dns_record_value)}
- className="p-2 text-foreground-subtle hover:text-accent transition-colors shrink-0"
- >
-
-
-
+
+ setShowVerifyModal(false)}
+ className="flex-1 px-4 py-3 border border-white/10 text-zinc-400 rounded-xl hover:bg-white/5 hover:text-white transition-all font-medium"
+ >
+ Close
+
+
+ {verifying ? : }
+ {verifying ? 'Verifying...' : 'Verify Now'}
+
-
-
-
- {verificationInfo.instructions}
-
-
-
-
-
- setShowVerifyModal(false)}
- className="flex-1 px-4 py-3 border border-border text-foreground-muted rounded-xl hover:bg-foreground/5 transition-all"
- >
- Close
-
-
- {verifying ? : }
- {verifying ? 'Checking...' : 'Check Verification'}
-
-
- )}
+ )}
+
)
}
+
+function InfoIcon(props: any) {
+ return (
+
+ )
+}
diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx
index 9283ccf..3c11f5b 100755
--- a/frontend/src/app/terminal/watchlist/page.tsx
+++ b/frontend/src/app/terminal/watchlist/page.tsx
@@ -4,17 +4,6 @@ import { useEffect, useState, useMemo, useCallback, memo } from 'react'
import { useStore } from '@/lib/store'
import { api, DomainHealthReport, HealthStatus } from '@/lib/api'
import { TerminalLayout } from '@/components/TerminalLayout'
-import {
- PremiumTable,
- Badge,
- StatCard,
- PageContainer,
- TableActionButton,
- SearchInput,
- TabBar,
- FilterBar,
- ActionButton,
-} from '@/components/PremiumTable'
import { Toast, useToast } from '@/components/Toast'
import {
Plus,
@@ -33,64 +22,130 @@ import {
AlertTriangle,
ShoppingCart,
HelpCircle,
+ Search,
+ Filter,
+ CheckCircle2,
+ Globe,
+ Clock,
+ Calendar,
+ MoreVertical,
+ ChevronDown,
+ ArrowRight
} from 'lucide-react'
import clsx from 'clsx'
import Link from 'next/link'
-// Health status badge colors and icons (Ampel-System as per concept)
-// 🟢 Online, 🟡 DNS Changed, 🔴 Offline/Error
+// ============================================================================
+// SHARED COMPONENTS
+// ============================================================================
+
+function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
+ return (
+
+ {children}
+
+ {content}
+ {/* Arrow */}
+
+
+
+ )
+}
+
+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'}
+
+ )}
+
+
+ )
+}
+
+// Health status badge configuration
const healthStatusConfig: Record
= {
healthy: {
label: 'Online',
- color: 'text-accent',
- bgColor: 'bg-accent/10 border-accent/20',
+ color: 'text-emerald-400',
icon: Activity,
- description: 'Domain is active and well-maintained',
- ampel: '🟢'
+ description: 'Domain is active and reachable',
+ dot: 'bg-emerald-400'
},
weakening: {
- label: 'DNS Changed',
+ label: 'Issues',
color: 'text-amber-400',
- bgColor: 'bg-amber-400/10 border-amber-400/20',
icon: AlertTriangle,
- description: 'Warning signs detected - DNS or config changed',
- ampel: '🟡'
+ description: 'Warning signs detected',
+ dot: 'bg-amber-400'
},
parked: {
- label: 'For Sale',
- color: 'text-orange-400',
- bgColor: 'bg-orange-400/10 border-orange-400/20',
+ label: 'Parked',
+ color: 'text-blue-400',
icon: ShoppingCart,
- description: 'Domain is parked and likely for sale',
- ampel: '🟡'
+ description: 'Domain is parked/for sale',
+ dot: 'bg-blue-400'
},
critical: {
label: 'Offline',
- color: 'text-red-400',
- bgColor: 'bg-red-400/10 border-red-400/20',
+ color: 'text-rose-400',
icon: AlertTriangle,
- description: 'Domain is offline or has critical errors',
- ampel: '🔴'
+ description: 'Domain is offline/error',
+ dot: 'bg-rose-400'
},
unknown: {
label: 'Unknown',
- color: 'text-foreground-muted',
- bgColor: 'bg-foreground/5 border-border/30',
+ color: 'text-zinc-400',
icon: HelpCircle,
- description: 'Could not determine status',
- ampel: '⚪'
+ description: 'Status unknown',
+ dot: 'bg-zinc-600'
},
}
type FilterStatus = 'watching' | 'portfolio' | 'available'
+// ============================================================================
+// MAIN PAGE
+// ============================================================================
+
export default function WatchlistPage() {
const { domains, addDomain, deleteDomain, refreshDomain, subscription } = useStore()
const { toast, showToast, hideToast } = useToast()
@@ -108,7 +163,7 @@ export default function WatchlistPage() {
const [loadingHealth, setLoadingHealth] = useState>({})
const [selectedHealthDomainId, setSelectedHealthDomainId] = useState(null)
- // Memoized stats - avoids recalculation on every render
+ // Memoized stats
const stats = useMemo(() => ({
availableCount: domains?.filter(d => d.is_available).length || 0,
watchingCount: domains?.filter(d => !d.is_available).length || 0,
@@ -127,20 +182,12 @@ export default function WatchlistPage() {
return false
}
if (filterStatus === 'available' && !domain.is_available) return false
- if (filterStatus === 'portfolio') return false // TODO: filter for verified own domains
- // 'watching' shows all domains
+ // 'portfolio' logic would go here
return true
})
}, [domains, searchQuery, filterStatus])
- // Memoized tabs config - as per concept: Watching + My Portfolio
- const tabs = useMemo(() => [
- { id: 'watching', label: 'Watching', icon: Eye, count: stats.domainsUsed },
- { id: 'portfolio', label: 'My Portfolio', icon: Shield, count: 0 }, // TODO: verified own domains
- { id: 'available', label: 'Available', icon: Sparkles, count: stats.availableCount, color: 'accent' as const },
- ], [stats])
-
- // Callbacks - prevent recreation on every render
+ // Callbacks
const handleAddDomain = useCallback(async (e: React.FormEvent) => {
e.preventDefault()
if (!newDomain.trim()) return
@@ -210,206 +257,351 @@ export default function WatchlistPage() {
}
}, [loadingHealth, showToast])
- // Dynamic subtitle
- const subtitle = useMemo(() => {
- if (stats.domainsUsed === 0) return 'Start tracking domains to monitor their availability'
- return `Monitoring ${stats.domainsUsed} domain${stats.domainsUsed !== 1 ? 's' : ''} • ${stats.domainLimit === -1 ? 'Unlimited' : `${stats.domainLimit - stats.domainsUsed} slots left`}`
- }, [stats])
-
- // Memoized columns config
- const columns = useMemo(() => [
- {
- key: 'domain',
- header: 'Domain',
- render: (domain: any) => (
-
-
-
- {domain.is_available && (
-
- )}
-
-
- {domain.name}
- {domain.is_available && (
- AVAILABLE
- )}
-
-
- ),
- },
- {
- key: 'health',
- header: 'Health',
- align: 'center' as const,
- width: '100px',
- hideOnMobile: true,
- render: (domain: any) => {
- const health = healthReports[domain.id]
- if (health) {
- const config = healthStatusConfig[health.status]
- return (
-
- {config.ampel}
- {config.label}
-
- )
- }
- return (
-
- {domain.is_available ? '🟢 Available' : '⚪ Checking...'}
-
- )
- },
- },
- {
- key: 'notifications',
- header: 'Alerts',
- align: 'center' as const,
- width: '80px',
- hideOnMobile: true,
- render: (domain: any) => (
- {
- e.stopPropagation()
- handleToggleNotify(domain.id, domain.notify_on_available)
- }}
- disabled={togglingNotifyId === domain.id}
- className={clsx(
- "p-2 rounded-lg transition-colors",
- domain.notify_on_available
- ? "bg-accent/10 text-accent hover:bg-accent/20"
- : "text-foreground-muted hover:bg-foreground/5"
- )}
- title={domain.notify_on_available ? "Disable alerts" : "Enable alerts"}
- >
- {togglingNotifyId === domain.id ? (
-
- ) : domain.notify_on_available ? (
-
- ) : (
-
- )}
-
- ),
- },
- {
- key: 'actions',
- header: '',
- align: 'right' as const,
- render: (domain: any) => (
-
- ),
- },
- ], [healthReports, togglingNotifyId, loadingHealth, refreshingId, deletingId, handleToggleNotify, handleHealthCheck, handleRefresh, handleDelete])
-
return (
-
- {toast && }
-
-
- {/* Stats Cards */}
-
-
-
-
-
+
+
+ {toast &&
}
+
+ {/* Ambient Background Glow */}
+
- {/* Add Domain Form */}
-
-
- handleAddDomain({} as React.FormEvent)}
- disabled={adding || !newDomain.trim() || !canAddMore}
- icon={adding ? Loader2 : Plus}
- >
- Add Domain
-
-
-
- {!canAddMore && (
-
-
- You've reached your domain limit. Upgrade to track more.
-
-
- Upgrade
-
+
+
+ {/* Header Section */}
+
+
+
+
+ Monitor availability, expiration dates, and health metrics for your critical domains.
+
+
+
+ {/* Quick Stats Pills */}
+
+
+
+ {stats.watchingCount} Active
+
+
+
+ {stats.availableCount} Available
+
+
- )}
- {/* Filters */}
-
- setFilterStatus(id as FilterStatus)}
- />
-
-
+ {/* Metric Grid */}
+
+
+ 0 ? 'up' : 'neutral'}
+ />
+
+ = stats.domainLimit ? 'down' : 'neutral'}
+ />
+
- {/* Domain Table */}
-
d.id}
- emptyIcon={ }
- emptyTitle={stats.domainsUsed === 0 ? "Your watchlist is empty" : "No domains match your filters"}
- emptyDescription={stats.domainsUsed === 0 ? "Add a domain above to start tracking" : "Try adjusting your filter criteria"}
- columns={columns}
- />
+ {/* Control Bar */}
+
+ {/* Filter Pills */}
+
+ {(['watching', 'available'] as const).map((tab) => (
+ setFilterStatus(tab)}
+ className={clsx(
+ "px-4 py-1.5 rounded-md text-xs font-medium transition-all",
+ filterStatus === tab
+ ? "bg-zinc-800 text-white shadow-sm"
+ : "text-zinc-400 hover:text-zinc-200 hover:bg-white/5"
+ )}
+ >
+ {tab.charAt(0).toUpperCase() + tab.slice(1)}
+
+ ))}
+
+
+ {/* Add Domain Input */}
+
+ setNewDomain(e.target.value)}
+ placeholder="Add domain to watch (e.g. apple.com)..."
+ className="w-full bg-black/50 border border-white/10 rounded-lg pl-10 pr-12 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all"
+ />
+
+
+ {adding ? : }
+
+
+
+ {/* Search Filter */}
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Filter watchlist..."
+ className="w-full bg-black/50 border border-white/10 rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-white/20 transition-all"
+ />
+
+
+
+ {/* Limit Warning */}
+ {!canAddMore && (
+
+
+
+
Limit reached. Upgrade plan to track more domains.
+
+
+ Upgrade
+
+
+ )}
+
+ {/* Data Grid */}
+
+ {/* Table Header */}
+
+
Domain
+
Status
+
Health
+
Alerts
+
Actions
+
+
+ {filteredDomains.length === 0 ? (
+
+
+
+
+
+ {searchQuery ? "No matches found" : "Watchlist is empty"}
+
+
+ {searchQuery ? "Try adjusting your filters." : "Start by adding domains you want to track above."}
+
+ {!searchQuery && (
+
document.querySelector('input')?.focus()}
+ className="text-emerald-400 text-sm hover:text-emerald-300 transition-colors flex items-center gap-2"
+ >
+ Add first domain
+
+ )}
+
+ ) : (
+
+ {filteredDomains.map((domain) => {
+ const health = healthReports[domain.id]
+ const healthConfig = health ? healthStatusConfig[health.status] : null
+
+ return (
+
+
+ {/* Mobile Layout (Visible only on mobile) */}
+
+
+
+
{domain.name}
+
+
+ {domain.is_available ? 'AVAILABLE' : 'TAKEN'}
+
+
+
+
handleDelete(domain.id, domain.name)}
+ className="p-2 text-zinc-500 hover:text-rose-400 transition-colors"
+ >
+
+
+
+
+
+
handleHealthCheck(domain.id)}
+ className="text-xs text-zinc-400 flex items-center gap-1.5 hover:text-white"
+ >
+
+ Check Health
+
+
handleToggleNotify(domain.id, domain.notify_on_available)}
+ className={clsx(
+ "text-xs flex items-center gap-1.5",
+ domain.notify_on_available ? "text-emerald-400" : "text-zinc-500"
+ )}
+ >
+ {domain.notify_on_available ? : }
+ {domain.notify_on_available ? 'Alerts On' : 'Alerts Off'}
+
+
+
+
+ {/* Desktop Layout */}
+ {/* Domain */}
+
+
+
+
+ {domain.is_available &&
}
+
+
{domain.name}
+
+
+
+ {/* Status */}
+
+
+ {domain.is_available ? 'Available' : 'Registered'}
+
+
+
+ {/* Health */}
+
+ {healthConfig ? (
+
+ setSelectedHealthDomainId(domain.id)}
+ className={clsx(
+ "flex items-center gap-2 px-2 py-1 rounded hover:bg-white/5 transition-colors",
+ healthConfig.color
+ )}
+ >
+
+ {healthConfig.label}
+
+
+ ) : (
+
+ handleHealthCheck(domain.id)}
+ disabled={loadingHealth[domain.id]}
+ className="text-zinc-600 hover:text-zinc-400 transition-colors"
+ >
+ {loadingHealth[domain.id] ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+
+ {/* Alerts */}
+
+
+ handleToggleNotify(domain.id, domain.notify_on_available)}
+ disabled={togglingNotifyId === domain.id}
+ className={clsx(
+ "p-1.5 rounded-lg transition-all",
+ domain.notify_on_available
+ ? "text-emerald-400 bg-emerald-400/10 hover:bg-emerald-400/20"
+ : "text-zinc-600 hover:text-zinc-400 hover:bg-white/5"
+ )}
+ >
+ {togglingNotifyId === domain.id ? (
+
+ ) : domain.notify_on_available ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Actions */}
+
+
+ handleRefresh(domain.id)}
+ className={clsx(
+ "p-1.5 rounded-lg text-zinc-500 hover:text-white hover:bg-white/10 transition-colors",
+ refreshingId === domain.id && "animate-spin text-emerald-400"
+ )}
+ >
+
+
+
+
+
+ handleDelete(domain.id, domain.name)}
+ className="p-1.5 rounded-lg text-zinc-500 hover:text-rose-400 hover:bg-rose-400/10 transition-colors"
+ >
+
+
+
+
+ {domain.is_available && (
+
+
+ Buy
+
+
+ )}
+
+
+ )
+ })}
+
+ )}
+
+
{/* Health Report Modal */}
{selectedHealthDomainId && healthReports[selectedHealthDomainId] && (
@@ -418,7 +610,7 @@ export default function WatchlistPage() {
onClose={() => setSelectedHealthDomainId(null)}
/>
)}
-
+
)
}
@@ -436,190 +628,130 @@ const HealthReportModal = memo(function HealthReportModal({
return (
e.stopPropagation()}
>
{/* Header */}
-
+
-
+
-
{report.domain}
-
{config.description}
+
{report.domain}
+
{config.description}
-
+
{/* Score */}
-
-
-
Health Score
-
-
-
= 70 ? "bg-accent" :
- report.score >= 40 ? "bg-amber-400" : "bg-red-400"
- )}
- style={{ width: `${report.score}%` }}
- />
-
-
= 70 ? "text-accent" :
- report.score >= 40 ? "text-amber-400" : "text-red-400"
- )}>
- {report.score}/100
-
-
+
+
+ Health Score
+ = 70 ? "text-emerald-400" :
+ report.score >= 40 ? "text-amber-400" : "text-rose-400"
+ )}>
+ {report.score}/100
+
+
+
+
= 70 ? "bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.5)]" :
+ report.score >= 40 ? "bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.5)]" : "bg-rose-500 shadow-[0_0_10px_rgba(244,63,94,0.5)]"
+ )}
+ style={{ width: `${report.score}%` }}
+ />
{/* Check Results */}
-
- {/* DNS */}
- {report.dns && (
-
-
-
- DNS Infrastructure
-
-
-
-
- {report.dns.has_ns ? '✓' : '✗'}
-
-
Nameservers
+
+
+ {/* Section: Infrastructure */}
+
+
+ Infrastructure
+
+
+
+
DNS Status
+
+
+ {report.dns?.has_ns ? '● Active' : '○ Missing'}
+
+
-
-
- {report.dns.has_a ? '✓' : '✗'}
-
-
A Record
+
+
Web Server
+
+
+ {report.http?.is_reachable ? `● HTTP ${report.http?.status_code}` : '○ Unreachable'}
+
+
-
-
- {report.dns.has_mx ? '✓' : '—'}
-
- MX Record
-
-
- {report.dns.is_parked && (
-
⚠ Parked at {report.dns.parking_provider || 'unknown provider'}
- )}
-
- )}
+
+
- {/* HTTP */}
- {report.http && (
-
-
-
- Website Status
-
-
-
- {report.http.is_reachable ? 'Reachable' : 'Unreachable'}
-
- {report.http.status_code && (
-
HTTP {report.http.status_code}
+ {/* Section: Security */}
+
+
+ Security
+
+
+
+ SSL Certificate
+
+ {report.ssl?.is_valid ? 'SECURE' : 'INSECURE'}
+
+
+ {report.ssl?.days_until_expiry && (
+
+ Expires in {report.ssl.days_until_expiry} days
+
)}
-
- {report.http.is_parked && (
-
⚠ Parking page detected
- )}
-
- )}
-
- {/* SSL */}
- {report.ssl && (
-
-
-
- SSL Certificate
-
-
- {report.ssl.has_certificate ? (
-
-
- {report.ssl.is_valid ? '✓ Valid certificate' : '✗ Certificate invalid/expired'}
-
- {report.ssl.days_until_expiry !== undefined && (
-
30 ? "text-foreground-muted" :
- report.ssl.days_until_expiry > 7 ? "text-amber-400" : "text-red-400"
- )}>
- Expires in {report.ssl.days_until_expiry} days
-
- )}
-
- ) : (
-
No SSL certificate
- )}
-
-
- )}
+
+
{/* Signals & Recommendations */}
{((report.signals?.length || 0) > 0 || (report.recommendations?.length || 0) > 0) && (
-
+
{(report.signals?.length || 0) > 0 && (
-
Signals
-
+ Signals
+
{report.signals?.map((signal, i) => (
-
- •
+
+
{signal}
))}
)}
- {(report.recommendations?.length || 0) > 0 && (
-
-
Recommendations
-
- {report.recommendations?.map((rec, i) => (
-
- →
- {rec}
-
- ))}
-
-
- )}
)}
{/* Footer */}
-
-
- Checked at {new Date(report.checked_at).toLocaleString()}
+
+
+ LAST CHECK: {new Date(report.checked_at).toLocaleString().toUpperCase()}