-
- {/* Market Feed */}
-
-
- {/* Alerts Feed */}
-
-
-
-
- {recentAlerts.length > 0 ? (
- recentAlerts.slice(0, 5).map((alert, idx) => (
-
-
- {alert.type === 'available' ? (
-
- ) : alert.type === 'expiring' ? (
-
- ) : (
-
- )}
-
-
{alert.domain.name}
-
- {alert.type === 'available' && "Status: Available"}
- {alert.type === 'expiring' && `Expiring: ${new Date(alert.domain.expiration_date!).toLocaleDateString()}`}
- {alert.type === 'checked' && "Regular Scan Completed"}
-
-
-
- {alert.type === 'available' && (
-
- Action Req
-
- )}
+
+ {/* Quick Actions */}
+
+
+
+
+
+
+
Quick Access
+
Navigation
- ))
- ) : (
-
- WATCHLIST EMPTY
- )}
+
+
+ {[
+ { label: 'Watchlist', desc: 'Your targets', href: '/terminal/watchlist', icon: Eye },
+ { label: 'Market', desc: 'All auctions', href: '/terminal/market', icon: Gavel },
+ { label: 'Intel', desc: 'TLD pricing', href: '/terminal/intel', icon: Globe },
+ ].map((item) => (
+
+
+
+
{item.label}
+
{item.desc}
+
+
+
+ ))}
+
+
+ {/* System Status */}
+
+
System Status
+
+
+
+
All Systems Operational
+
+
99.9%
+
+
+
+
+
-
+
+
)
}
\ No newline at end of file
diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx
index e90bf25..f4d3f80 100755
--- a/frontend/src/app/terminal/watchlist/page.tsx
+++ b/frontend/src/app/terminal/watchlist/page.tsx
@@ -3,7 +3,7 @@
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 { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { Toast, useToast } from '@/components/Toast'
import {
Plus,
@@ -14,159 +14,24 @@ import {
BellOff,
Eye,
Sparkles,
- ArrowUpRight,
X,
Activity,
Shield,
AlertTriangle,
- ShoppingCart,
- HelpCircle,
- Search,
- Globe,
Clock,
- Calendar,
ArrowRight,
CheckCircle2,
XCircle,
- Wifi,
- Lock,
- TrendingDown,
Zap,
- Diamond,
- Tag,
- Crown,
- DollarSign,
- Copy,
- Check
+ Target,
+ Radio,
+ Crosshair,
+ Globe,
+ ExternalLink,
+ Calendar
} from 'lucide-react'
import clsx from 'clsx'
import Link from 'next/link'
-import { useRouter } from 'next/navigation'
-
-// ============================================================================
-// SHARED COMPONENTS (Matched to Market/Intel/Radar)
-// ============================================================================
-
-const Tooltip = memo(({ children, content }: { children: React.ReactNode; content: string }) => (
-
-))
-Tooltip.displayName = 'Tooltip'
-
-const StatCard = memo(({
- label,
- value,
- subValue,
- icon: Icon,
- highlight,
- trend
-}: {
- label: string
- value: string | number
- subValue?: string
- icon: any
- highlight?: boolean
- trend?: 'up' | 'down' | 'neutral' | 'active'
-}) => (
-
-
-
-
-
-
-
- {label}
-
-
- {value}
- {subValue && {subValue}}
-
- {highlight && (
-
- ● LIVE
-
- )}
-
-
-))
-StatCard.displayName = 'StatCard'
-
-// Health status badge configuration
-const healthStatusConfig: Record
= {
- healthy: {
- label: 'Online',
- color: 'text-emerald-400',
- bgColor: 'bg-emerald-500/10 border-emerald-500/20',
- icon: Activity,
- description: 'Domain is active and reachable',
- },
- weakening: {
- label: 'Issues',
- color: 'text-amber-400',
- bgColor: 'bg-amber-500/10 border-amber-500/20',
- icon: AlertTriangle,
- description: 'Warning signs detected',
- },
- parked: {
- label: 'Parked',
- color: 'text-blue-400',
- bgColor: 'bg-blue-500/10 border-blue-500/20',
- icon: ShoppingCart,
- description: 'Domain is parked/for sale',
- },
- critical: {
- label: 'Critical',
- color: 'text-rose-400',
- bgColor: 'bg-rose-500/10 border-rose-500/20',
- icon: AlertTriangle,
- description: 'Domain may be dropping soon',
- },
- unknown: {
- label: 'Unknown',
- color: 'text-zinc-400',
- bgColor: 'bg-zinc-800 border-zinc-700',
- icon: HelpCircle,
- description: 'Health check pending',
- },
-}
-
-type MainTab = 'watching' | 'portfolio'
-type FilterTab = 'all' | 'available' | 'expiring' | 'critical'
-
-// Portfolio Domain type - not used anymore since we use domains from store
-// Keeping for potential future use
-interface PortfolioDomain {
- id: number
- domain: string
- purchase_date: string | null
- purchase_price: number | null
- registrar: string | null
- renewal_date: string | null
- renewal_cost: number | null
- status: string
- is_verified: boolean
- verification_code: string | null
- last_checked: string | null
- last_change: string | null
- notify_on_expiry: boolean
- notify_on_change: boolean
- notes: string | null
- created_at: string
-}
// ============================================================================
// HELPER FUNCTIONS
@@ -182,11 +47,7 @@ function getDaysUntilExpiry(expirationDate: string | null): number | null {
function formatExpiryDate(expirationDate: string | null): string {
if (!expirationDate) return '—'
- return new Date(expirationDate).toLocaleDateString('en-US', {
- month: 'short',
- day: 'numeric',
- year: 'numeric'
- })
+ return new Date(expirationDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
function getTimeAgo(date: string | null): string {
@@ -197,97 +58,40 @@ function getTimeAgo(date: string | null): string {
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
-
if (diffMins < 1) return 'Just now'
if (diffMins < 60) return `${diffMins}m ago`
if (diffHours < 24) return `${diffHours}h ago`
- if (diffDays < 7) return `${diffDays}d ago`
- return formatExpiryDate(date)
+ return `${diffDays}d ago`
+}
+
+// Health config
+const healthConfig: Record = {
+ healthy: { label: 'ONLINE', color: 'text-accent', bg: 'bg-accent/10 border-accent/20' },
+ weakening: { label: 'WARNING', color: 'text-amber-400', bg: 'bg-amber-500/10 border-amber-500/20' },
+ parked: { label: 'PARKED', color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/20' },
+ critical: { label: 'CRITICAL', color: 'text-rose-400', bg: 'bg-rose-500/10 border-rose-500/20' },
+ unknown: { label: 'UNKNOWN', color: 'text-white/40', bg: 'bg-white/5 border-white/10' },
}
// ============================================================================
// MAIN PAGE
// ============================================================================
-// Listing interface for counting active listings
-interface Listing {
- id: number
- domain: string
- status: string
-}
-
export default function WatchlistPage() {
const { domains, addDomain, deleteDomain, refreshDomain, updateDomain, subscription } = useStore()
const { toast, showToast, hideToast } = useToast()
- const router = useRouter()
-
- // Main tab state (Watching vs My Portfolio)
- const [mainTab, setMainTab] = useState('watching')
const [newDomain, setNewDomain] = useState('')
const [adding, setAdding] = useState(false)
const [refreshingId, setRefreshingId] = useState(null)
const [deletingId, setDeletingId] = useState(null)
const [togglingNotifyId, setTogglingNotifyId] = useState(null)
- const [filterTab, setFilterTab] = useState('all')
- const [searchQuery, setSearchQuery] = useState('')
-
- // Portfolio state - uses same domains from store but filtered/extended
- const [newPortfolioDomain, setNewPortfolioDomain] = useState('')
- const [addingPortfolio, setAddingPortfolio] = useState(false)
- const [showVerifyModal, setShowVerifyModal] = useState(false)
- const [verifyingDomainId, setVerifyingDomainId] = useState(null)
- const [verifying, setVerifying] = useState(false)
- const [showEditModal, setShowEditModal] = useState(false)
- const [editingDomainId, setEditingDomainId] = useState(null)
- const [editForm, setEditForm] = useState({ registrar: '', notes: '' })
- const [savingEdit, setSavingEdit] = useState(false)
-
- // Health check state
const [healthReports, setHealthReports] = useState>({})
const [loadingHealth, setLoadingHealth] = useState>({})
- const [selectedHealthDomainId, setSelectedHealthDomainId] = useState(null)
-
- // Listing state for tier limits
- const [listings, setListings] = useState([])
- const [loadingListings, setLoadingListings] = useState(false)
-
- // Sell Modal state (Wizard)
- const [showSellModal, setShowSellModal] = useState(false)
- const [sellStep, setSellStep] = useState<1 | 2 | 3>(1) // 1: Details, 2: DNS Verify, 3: Published
- const [sellDomainName, setSellDomainName] = useState('')
- const [sellForm, setSellForm] = useState({
- price: '',
- priceType: 'negotiable',
- allowOffers: true,
- title: '',
- })
- const [sellLoading, setSellLoading] = useState(false)
- const [sellListingId, setSellListingId] = useState(null)
- const [sellVerificationInfo, setSellVerificationInfo] = useState<{
- verification_code: string
- dns_record_name: string
- dns_record_value: string
- } | null>(null)
- const [copiedField, setCopiedField] = useState(null)
-
- // Tier-based limits (from pounce_pricing.md)
- const tier = subscription?.tier || 'scout'
-
- // Watchlist limits: Scout=5, Trader=50, Tycoon=500
- const watchlistLimits: Record = { scout: 5, trader: 50, tycoon: 500 }
- const maxWatchlist = watchlistLimits[tier] || 5
-
- // Listing limits: Scout=0, Trader=5, Tycoon=50
- const listingLimits: Record = { scout: 0, trader: 5, tycoon: 50 }
- const maxListings = listingLimits[tier] || 0
- const canSell = tier !== 'scout'
- const isTycoon = tier === 'tycoon'
- const currentListingCount = listings.length
- const canCreateListing = canSell && currentListingCount < maxListings
-
+ const [selectedDomain, setSelectedDomain] = useState(null)
+ const [filter, setFilter] = useState<'all' | 'available' | 'expiring'>('all')
- // Memoized stats
+ // Stats
const stats = useMemo(() => {
const available = domains?.filter(d => d.is_available) || []
const expiringSoon = domains?.filter(d => {
@@ -295,71 +99,37 @@ export default function WatchlistPage() {
const days = getDaysUntilExpiry(d.expiration_date)
return days !== null && days <= 30 && days > 0
}) || []
- const critical = Object.values(healthReports).filter(h => h.status === 'critical').length
-
- return {
- total: domains?.length || 0,
- available: available.length,
- expiringSoon: expiringSoon.length,
- critical,
- limit: maxWatchlist,
- }
- }, [domains, maxWatchlist, healthReports])
+ return { total: domains?.length || 0, available: available.length, expiring: expiringSoon.length }
+ }, [domains])
- const canAddMore = stats.total < stats.limit
-
- // Memoized filtered domains
+ // Filtered
const filteredDomains = useMemo(() => {
if (!domains) return []
-
- return domains.filter(domain => {
- // Search filter
- if (searchQuery && !domain.name.toLowerCase().includes(searchQuery.toLowerCase())) {
- return false
+ return domains.filter(d => {
+ if (filter === 'available') return d.is_available
+ if (filter === 'expiring') {
+ const days = getDaysUntilExpiry(d.expiration_date)
+ return days !== null && days <= 30 && days > 0
}
-
- // Tab filter
- switch (filterTab) {
- case 'available':
- return domain.is_available
- case 'expiring':
- if (domain.is_available || !domain.expiration_date) return false
- const days = getDaysUntilExpiry(domain.expiration_date)
- return days !== null && days <= 30 && days > 0
- case 'critical':
- const health = healthReports[domain.id]
- return health?.status === 'critical' || health?.status === 'weakening'
- default:
return true
- }
}).sort((a, b) => {
- // Sort available first, then by expiry date
if (a.is_available && !b.is_available) return -1
if (!a.is_available && b.is_available) return 1
-
- // Then by expiry (soonest first)
- const daysA = getDaysUntilExpiry(a.expiration_date)
- const daysB = getDaysUntilExpiry(b.expiration_date)
- if (daysA !== null && daysB !== null) return daysA - daysB
- if (daysA !== null) return -1
- if (daysB !== null) return 1
-
return a.name.localeCompare(b.name)
})
- }, [domains, searchQuery, filterTab, healthReports])
+ }, [domains, filter])
- // Callbacks
- const handleAddDomain = useCallback(async (e: React.FormEvent) => {
+ // Handlers
+ const handleAdd = useCallback(async (e: React.FormEvent) => {
e.preventDefault()
if (!newDomain.trim()) return
-
setAdding(true)
try {
await addDomain(newDomain.trim())
+ showToast(`Target locked: ${newDomain.trim()}`, 'success')
setNewDomain('')
- showToast(`Added ${newDomain.trim()} to watchlist`, 'success')
} catch (err: any) {
- showToast(err.message || 'Failed to add domain', 'error')
+ showToast(err.message || 'Failed', 'error')
} finally {
setAdding(false)
}
@@ -369,1659 +139,349 @@ export default function WatchlistPage() {
setRefreshingId(id)
try {
await refreshDomain(id)
- showToast('Domain status refreshed', 'success')
- } catch (err: any) {
- showToast(err.message || 'Failed to refresh', 'error')
- } finally {
- setRefreshingId(null)
- }
+ showToast('Intel updated', 'success')
+ } catch { showToast('Update failed', 'error') }
+ finally { setRefreshingId(null) }
}, [refreshDomain, showToast])
const handleDelete = useCallback(async (id: number, name: string) => {
- if (!confirm(`Remove ${name} from your watchlist?`)) return
-
+ if (!confirm(`Drop target: ${name}?`)) return
setDeletingId(id)
try {
await deleteDomain(id)
- showToast(`Removed ${name} from watchlist`, 'success')
- } catch (err: any) {
- showToast(err.message || 'Failed to remove', 'error')
- } finally {
- setDeletingId(null)
- }
+ showToast('Target dropped', 'success')
+ } catch { showToast('Failed', 'error') }
+ finally { setDeletingId(null) }
}, [deleteDomain, showToast])
- const handleToggleNotify = useCallback(async (id: number, currentState: boolean) => {
+ const handleToggleNotify = useCallback(async (id: number, current: boolean) => {
setTogglingNotifyId(id)
try {
- await api.updateDomainNotify(id, !currentState)
- // Instant optimistic update
- updateDomain(id, { notify_on_available: !currentState })
- showToast(!currentState ? 'Notifications enabled' : 'Notifications disabled', 'success')
- } catch (err: any) {
- showToast(err.message || 'Failed to update', 'error')
- } finally {
- setTogglingNotifyId(null)
- }
- }, [showToast, updateDomain])
+ await api.updateDomainNotify(id, !current)
+ updateDomain(id, { notify_on_available: !current })
+ showToast(!current ? 'Alerts armed' : 'Alerts disarmed', 'success')
+ } catch { showToast('Failed', 'error') }
+ finally { setTogglingNotifyId(null) }
+ }, [updateDomain, showToast])
- const handleHealthCheck = useCallback(async (domainId: number) => {
- if (loadingHealth[domainId]) return
-
- setLoadingHealth(prev => ({ ...prev, [domainId]: true }))
+ const handleHealthCheck = useCallback(async (id: number) => {
+ if (loadingHealth[id]) return
+ setLoadingHealth(prev => ({ ...prev, [id]: true }))
try {
- // Force a live refresh when user explicitly triggers a check
- const report = await api.getDomainHealth(domainId, { refresh: true })
- setHealthReports(prev => ({ ...prev, [domainId]: report }))
- setSelectedHealthDomainId(domainId)
- } catch (err: any) {
- showToast(err.message || 'Health check failed', 'error')
- } finally {
- setLoadingHealth(prev => ({ ...prev, [domainId]: false }))
- }
- }, [loadingHealth, showToast])
+ const report = await api.getDomainHealth(id, { refresh: true })
+ setHealthReports(prev => ({ ...prev, [id]: report }))
+ } catch {}
+ finally { setLoadingHealth(prev => ({ ...prev, [id]: false })) }
+ }, [loadingHealth])
-
- // Load health data for all domains on mount
+ // Load health
useEffect(() => {
- const loadHealthData = async () => {
- if (!domains || domains.length === 0) return
-
- // Load cached health for all domains in one request (fast path)
+ const load = async () => {
+ if (!domains?.length) return
try {
const data = await api.getDomainsHealthCache()
- if (data?.reports) {
- // API returns string keys; JS handles number indexing transparently.
- setHealthReports(prev => ({ ...(prev as any), ...(data.reports as any) }))
- }
- } catch {
- // Silently fail - health data is optional
- }
+ if (data?.reports) setHealthReports(prev => ({ ...prev, ...data.reports }))
+ } catch {}
}
-
- loadHealthData()
+ load()
}, [domains])
-
- // Load user's listings to check limits
- useEffect(() => {
- const loadListings = async () => {
- if (!canSell) return // Scout users can't list, no need to load
-
- setLoadingListings(true)
- try {
- const data = await api.request('/listings/my')
- setListings(data)
- } catch (err) {
- console.error('Failed to load listings:', err)
- } finally {
- setLoadingListings(false)
- }
- }
-
- loadListings()
- }, [canSell])
-
- // Handle "Sell" button click - open wizard modal
- const handleSellDomain = useCallback((domainName: string) => {
- if (!canSell) {
- showToast('Upgrade to Trader or Tycoon to sell domains', 'error')
- return
- }
-
- if (!canCreateListing) {
- showToast(`Listing limit reached (${currentListingCount}/${maxListings}). Upgrade to list more.`, 'error')
- return
- }
-
- // Open sell wizard modal
- setSellDomainName(domainName)
- setSellStep(1)
- setSellForm({ price: '', priceType: 'negotiable', allowOffers: true, title: '' })
- setSellListingId(null)
- setSellVerificationInfo(null)
- setShowSellModal(true)
- }, [canSell, canCreateListing, currentListingCount, maxListings, showToast])
-
- // Step 1: Create listing
- const handleCreateListing = useCallback(async () => {
- setSellLoading(true)
- try {
- const response = await api.request<{ id: number }>('/listings', {
- method: 'POST',
- body: JSON.stringify({
- domain: sellDomainName,
- title: sellForm.title || null,
- asking_price: sellForm.price ? parseFloat(sellForm.price) : null,
- price_type: sellForm.priceType,
- allow_offers: sellForm.allowOffers,
- }),
- })
- setSellListingId(response.id)
-
- // Start DNS verification
- const verifyResponse = await api.request<{
- verification_code: string
- dns_record_name: string
- dns_record_value: string
- }>(`/listings/${response.id}/verify-dns`, { method: 'POST' })
-
- setSellVerificationInfo(verifyResponse)
- setSellStep(2)
- showToast('Listing created! Now verify ownership.', 'success')
- } catch (err: any) {
- showToast(err.message || 'Failed to create listing', 'error')
- } finally {
- setSellLoading(false)
- }
- }, [sellDomainName, sellForm, showToast])
-
- // Step 2: Check DNS verification
- const handleCheckVerification = useCallback(async () => {
- if (!sellListingId) return
- setSellLoading(true)
- try {
- const result = await api.request<{ verified: boolean; message: string }>(
- `/listings/${sellListingId}/verify-dns/check`
- )
-
- if (result.verified) {
- // Publish the listing
- await api.request(`/listings/${sellListingId}`, {
- method: 'PUT',
- body: JSON.stringify({ status: 'active' }),
- })
- setSellStep(3)
- showToast('Domain verified and published!', 'success')
- // Reload listings
- const data = await api.request('/listings/my')
- setListings(data)
- } else {
- showToast(result.message || 'Verification pending. Check your DNS settings.', 'error')
- }
- } catch (err: any) {
- showToast(err.message || 'Verification failed', 'error')
- } finally {
- setSellLoading(false)
- }
- }, [sellListingId, showToast])
-
- // Copy to clipboard helper
- const copyToClipboard = useCallback((text: string, field: string) => {
- navigator.clipboard.writeText(text)
- setCopiedField(field)
- setTimeout(() => setCopiedField(null), 2000)
- }, [])
-
- // Portfolio uses the SAME domains as Watchlist - just a different view
- // This ensures consistent monitoring behavior
-
- // Portfolio domains = all non-available domains (domains user "owns")
- const portfolioDomains = useMemo(() => {
- if (!domains) return []
- // Show all domains that are NOT available (i.e., registered/owned)
- return domains.filter(d => !d.is_available).sort((a, b) => {
- // Sort by expiry date (soonest first)
- const daysA = getDaysUntilExpiry(a.expiration_date)
- const daysB = getDaysUntilExpiry(b.expiration_date)
- if (daysA !== null && daysB !== null) return daysA - daysB
- if (daysA !== null) return -1
- if (daysB !== null) return 1
- return a.name.localeCompare(b.name)
- })
- }, [domains])
-
- // Add domain to portfolio (uses same addDomain as Watchlist)
- const handleAddPortfolioDomain = useCallback(async (e: React.FormEvent) => {
- e.preventDefault()
- if (!newPortfolioDomain.trim()) return
-
- setAddingPortfolio(true)
- try {
- await addDomain(newPortfolioDomain.trim())
- setNewPortfolioDomain('')
- showToast(`Added ${newPortfolioDomain.trim()} to your portfolio`, 'success')
- } catch (err: any) {
- showToast(err.message || 'Failed to add domain', 'error')
- } finally {
- setAddingPortfolio(false)
- }
- }, [newPortfolioDomain, addDomain, showToast])
-
- // Verify domain ownership via DNS (placeholder - would need backend support)
- const handleVerifyDomain = useCallback(async () => {
- if (verifyingDomainId === null) return
- const domain = domains?.find(d => d.id === verifyingDomainId)
- if (!domain) return
-
- setVerifying(true)
- try {
- // For now, just show success - real verification would check DNS TXT record
- await api.request(`/domains/${verifyingDomainId}/verify`, { method: 'POST' }).catch(() => {})
- showToast(`${domain.name} ownership noted! ✓`, 'success')
- setShowVerifyModal(false)
- setVerifyingDomainId(null)
- } catch (err: any) {
- showToast(err.message || 'Verification noted', 'success')
- setShowVerifyModal(false)
- setVerifyingDomainId(null)
- } finally {
- setVerifying(false)
- }
- }, [verifyingDomainId, domains, showToast])
-
- // Edit portfolio domain (updates notes via store)
- const handleEditPortfolioDomain = useCallback(async (e: React.FormEvent) => {
- e.preventDefault()
- if (editingDomainId === null) return
-
- setSavingEdit(true)
- try {
- // Update via API if available, otherwise just close
- await api.request(`/domains/${editingDomainId}`, {
- method: 'PATCH',
- body: JSON.stringify({ notes: editForm.notes }),
- }).catch(() => {})
- showToast('Domain updated', 'success')
- setShowEditModal(false)
- setEditingDomainId(null)
- } catch (err: any) {
- showToast('Updated locally', 'success')
- setShowEditModal(false)
- setEditingDomainId(null)
- } finally {
- setSavingEdit(false)
- }
- }, [editingDomainId, editForm, showToast])
-
- // Open edit modal for portfolio domain
- const openEditModal = useCallback((domain: typeof domains[0]) => {
- setEditingDomainId(domain.id)
- setEditForm({
- registrar: domain.registrar || '',
- notes: '',
- })
- setShowEditModal(true)
- }, [])
return (
-
-
- {toast &&
}
+
+ {toast && }
+
+
- {/* Ambient Background Glow */}
-
-
-
-
- {/* Header Section */}
-
-
-
-
-
Watchlist
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* HERO */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
+
+
+
+ {/* Left */}
+
+
+
+
+ Active Surveillance
+
+
+
+
+ Target
+ Acquisition.
+
+
+
+ Monitor high-value domains. Get instant alerts when they become available.
+
- {/* Main Tab Switcher (Watching | My Portfolio) */}
-
-
-
-
-
-
- {/* Quick Stats Pills */}
-
- {mainTab === 'watching' && stats.available > 0 && (
-
-
- {stats.available} Available!
+ {/* Right: Big Numbers */}
+
+
+
{stats.total}
+
Tracking
+
+
+
{stats.available}
+
Available
+
+
+
{stats.expiring}
+
Expiring
- )}
-
-
- {mainTab === 'watching' ? 'Auto-Monitoring' : 'Verified Domains'}
+
- {/* ══════════════════════════════════════════════════════════════════ */}
- {/* WATCHING TAB CONTENT */}
- {/* ══════════════════════════════════════════════════════════════════ */}
- {mainTab === 'watching' && (
- <>
- {/* Metric Grid */}
-
-
0}
- trend={stats.available > 0 ? 'up' : 'active'}
- />
- 0 ? 'up' : 'neutral'}
- />
- 0 ? 'down' : 'neutral'}
- />
- 0 ? 'down' : 'neutral'}
- />
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* ADD DOMAIN */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
- {/* Control Bar */}
-
- {/* Filter Tabs */}
-
- {([
- { key: 'all', label: 'All', count: stats.total },
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* FILTERS */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
+
+
+ {[
+ { key: 'all', label: 'All Targets', count: stats.total },
{ key: 'available', label: 'Available', count: stats.available },
- { key: 'expiring', label: 'Expiring', count: stats.expiringSoon },
- { key: 'critical', label: 'Issues', count: stats.critical },
- ] as const).map((tab) => (
+ { key: 'expiring', label: 'Expiring', count: stats.expiring },
+ ].map((f) => (
))}
-
- {/* Add Domain Input */}
-
-
- {/* Search Filter */}
-
-
-
setSearchQuery(e.target.value)}
- placeholder="Filter domains..."
- 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"
- />
+
+
+ Auto-refresh: {subscription?.tier === 'tycoon' ? '10min' : 'Hourly'}
+
- {/* Limit Warning */}
- {!canAddMore && (
-
-
-
-
Limit reached ({stats.total}/{stats.limit}). Upgrade to track more.
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* DATA TABLE */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
+
+
+ {filteredDomains.length === 0 ? (
+
+
+
+
+
No Targets
+
Add domains above to start tracking
-
- Upgrade
-
-
- )}
-
- {/* Data Grid */}
-
- {/* Table Header */}
-
-
-
-
Domain
+ ) : (
+
+ {/* Header */}
+
+
Target
Status
Health
Expires
-
Alerts
Actions
-
-
- {filteredDomains.length === 0 ? (
-
-
-
-
- {searchQuery ? "No matches found" : filterTab !== 'all' ? `No ${filterTab} domains` : "Watchlist is empty"}
-
-
- {searchQuery ? "Try adjusting your filter." : "Start by adding domains you want to track."}
-
- {!searchQuery && filterTab === 'all' && (
-
- )}
-
- ) : (
-
- {filteredDomains.map((domain) => {
- const health = healthReports[domain.id]
- const healthConfig = health ? healthStatusConfig[health.status] : healthStatusConfig.unknown
- const daysUntilExpiry = getDaysUntilExpiry(domain.expiration_date)
- const isExpiringSoon = daysUntilExpiry !== null && daysUntilExpiry <= 30 && daysUntilExpiry > 0
- const isExpired = daysUntilExpiry !== null && daysUntilExpiry <= 0
-
- return (
-
-
- {/* Domain */}
-
-
-
-
- {domain.is_available && (
-
- )}
+
+ {/* Rows */}
+
+ {filteredDomains.map((domain) => {
+ const health = healthReports[domain.id]
+ const hConfig = health ? healthConfig[health.status] : healthConfig.unknown
+ const days = getDaysUntilExpiry(domain.expiration_date)
+ const expiringSoon = days !== null && days <= 30 && days > 0
+
+ return (
+
+ {/* Domain */}
+
+
+
+
{domain.name}
+
+ {domain.registrar || 'Unknown registrar'}
+
-
-
{domain.name}
- {domain.registrar && (
-
- {domain.registrar}
-
- )}
-
-
-
- {/* Status */}
-
- {domain.is_available ? (
-
-
- Available
+
+ {/* Status */}
+
+
+ {domain.is_available ? 'OPEN' : 'LOCKED'}
- ) : (
-
-
- Registered
-
- )}
-
-
- {/* Health */}
-
- {domain.is_available ? (
-
—
- ) : health ? (
-
-
-
- ) : (
-
-
-
- )
- })}
+ )
+ })}
+
- )}
-
-
-
-
- {/* Subtle Footer Info */}
-
-
-
- Checks: {subscription?.tier === 'tycoon' ? '10min' : subscription?.tier === 'trader' ? 'hourly' : 'daily'}
-
- {subscription?.tier !== 'tycoon' && (
-
- Upgrade
-
)}
+
- >
- )}
+
- {/* ══════════════════════════════════════════════════════════════════ */}
- {/* MY PORTFOLIO TAB CONTENT */}
- {/* ══════════════════════════════════════════════════════════════════ */}
- {mainTab === 'portfolio' && (
- <>
- {/* Portfolio Metric Grid - uses same data as Watching */}
-
-
- h.status === 'healthy').length}
- subValue="Healthy"
- icon={CheckCircle2}
- trend="up"
- />
- h.status === 'critical' || h.status === 'weakening').length}
- subValue="Need attention"
- icon={AlertTriangle}
- trend={Object.values(healthReports).filter(h => h.status === 'critical' || h.status === 'weakening').length > 0 ? 'down' : 'neutral'}
- />
- {
- const days = getDaysUntilExpiry(d.expiration_date)
- return days !== null && days <= 30 && days > 0
- }).length}
- subValue="< 30 days"
- icon={Calendar}
- trend={portfolioDomains.filter(d => {
- const days = getDaysUntilExpiry(d.expiration_date)
- return days !== null && days <= 30 && days > 0
- }).length > 0 ? 'down' : 'neutral'}
- />
-
-
- {/* Control Bar - Add Domain */}
-
-
-
- Add domains you own to track expiry & health
-
-
- {/* Add Domain Input */}
-
-
-
- {/* Portfolio Data Grid - uses same domains/health as Watching */}
-
- {portfolioDomains.length === 0 ? (
-
-
-
-
-
No domains in portfolio
-
- Add domains you own to track renewals, health status, and changes.
-
-
document.querySelector('input[placeholder*="mydomain"]')?.focus()}
- className="text-emerald-400 text-sm hover:text-emerald-300 transition-colors flex items-center gap-2"
- >
- Add your first domain
-
-
- ) : (
- <>
- {/* Desktop Table - IDENTICAL to Watching tab */}
-
-
- {/* Table Header */}
-
-
Domain
-
Health
-
Expiry
-
Last Update
-
Alerts
-
Actions
-
-
- {/* Table Rows - uses same data/logic as Watching */}
-
- {portfolioDomains.map((domain) => {
- const expiryDays = getDaysUntilExpiry(domain.expiration_date)
- const isExpiringSoon = expiryDays !== null && expiryDays <= 30 && expiryDays > 0
- const health = healthReports[domain.id]
- const healthConfig = health ? healthStatusConfig[health.status] : null
-
- return (
-
- {/* Domain */}
-
-
-
-
-
-
-
{domain.name}
-
- {domain.registrar || 'Unknown registrar'} • Added {getTimeAgo(domain.created_at)}
-
-
-
-
-
- {/* Health - uses SAME healthReports as Watching */}
-
-
- handleHealthCheck(domain.id)}
- disabled={loadingHealth[domain.id]}
- className={clsx(
- "w-8 h-8 rounded-full flex items-center justify-center border transition-all",
- loadingHealth[domain.id] && "animate-pulse",
- healthConfig
- ? healthConfig.bgColor
- : "bg-zinc-800 border-zinc-700 hover:border-zinc-600"
- )}
- >
- {loadingHealth[domain.id] ? (
-
- ) : healthConfig ? (
-
- ) : (
-
- )}
-
-
-
-
- {/* Expiry - uses expiration_date like Watching */}
-
- {domain.expiration_date ? (
-
-
-
- {expiryDays !== null ? `${expiryDays}d` : formatExpiryDate(domain.expiration_date)}
-
-
- ) : (
-
Unknown
- )}
-
-
- {/* Last Update */}
-
-
- {domain.last_checked ? getTimeAgo(domain.last_checked) : 'Never'}
-
-
-
- {/* Alerts - uses SAME handleToggleNotify as Watching */}
-
-
- handleToggleNotify(domain.id, domain.notify_on_available)}
- disabled={togglingNotifyId === domain.id}
- className={clsx(
- "p-1.5 rounded border transition-all",
- domain.notify_on_available
- ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
- : "bg-zinc-800/50 border-zinc-700 text-zinc-500 hover:text-zinc-400"
- )}
- >
- {togglingNotifyId === domain.id
- ?
- : domain.notify_on_available
- ?
- :
- }
-
-
-
-
- {/* Actions - uses SAME handleRefresh/handleDelete as Watching */}
-
-
- handleRefresh(domain.id)}
- disabled={refreshingId === domain.id}
- className="p-1.5 rounded border bg-zinc-800/50 border-zinc-700 text-zinc-400 hover:text-white hover:border-zinc-600 transition-all disabled:opacity-50"
- >
- {refreshingId === domain.id
- ?
- :
- }
-
-
-
- handleDelete(domain.id, domain.name)}
- disabled={deletingId === domain.id}
- className="p-1.5 rounded border bg-zinc-800/50 border-zinc-700 text-zinc-400 hover:text-rose-400 hover:border-rose-500/30 transition-all disabled:opacity-50"
- >
- {deletingId === domain.id
- ?
- :
- }
-
-
-
- {/* Sell Button - Tier-based */}
- {canSell ? (
-
- handleSellDomain(domain.name)}
- disabled={!canCreateListing}
- className={clsx(
- "px-3 py-1.5 rounded-lg text-[11px] font-medium flex items-center gap-1.5 transition-colors",
- canCreateListing
- ? "bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 hover:bg-emerald-500/20"
- : "bg-zinc-800/50 border border-zinc-700 text-zinc-500 cursor-not-allowed"
- )}
- >
-
- Sell
- {isTycoon && canCreateListing && (
-
- )}
-
-
- ) : (
-
-
-
- Upgrade
-
-
- )}
-
-
- )
- })}
-
-
-
-
- {/* Mobile Cards - IDENTICAL logic to Watching */}
-
- {portfolioDomains.map((domain) => {
- const expiryDays = getDaysUntilExpiry(domain.expiration_date)
- const isExpiringSoon = expiryDays !== null && expiryDays <= 30 && expiryDays > 0
- const health = healthReports[domain.id]
- const healthConfig = health ? healthStatusConfig[health.status] : null
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
-
{domain.name}
-
{domain.registrar || 'Unknown registrar'}
-
-
-
-
- {/* Info Grid - uses same data as Watching */}
-
-
-
Health
-
handleHealthCheck(domain.id)}
- className={clsx(
- "w-8 h-8 rounded-full flex items-center justify-center border mx-auto",
- healthConfig ? healthConfig.bgColor : "bg-zinc-800 border-zinc-700"
- )}
- >
- {loadingHealth[domain.id] ? (
-
- ) : healthConfig ? (
-
- ) : (
-
- )}
-
-
-
-
Expiry
-
- {expiryDays !== null ? `${expiryDays}d` : '—'}
-
-
-
-
Checked
-
- {domain.last_checked ? getTimeAgo(domain.last_checked) : '—'}
-
-
-
-
- {/* Actions - uses SAME functions as Watching */}
-
-
- handleToggleNotify(domain.id, domain.notify_on_available)}
- disabled={togglingNotifyId === domain.id}
- className={clsx(
- "p-2 rounded border text-xs flex items-center gap-1.5",
- domain.notify_on_available
- ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
- : "bg-zinc-800/50 border-zinc-700 text-zinc-500"
- )}
- >
- {togglingNotifyId === domain.id
- ?
- :
- }
-
- handleRefresh(domain.id)}
- disabled={refreshingId === domain.id}
- className="p-2 rounded border bg-zinc-800/50 border-zinc-700 text-zinc-400"
- >
- {refreshingId === domain.id
- ?
- :
- }
-
- handleDelete(domain.id, domain.name)}
- disabled={deletingId === domain.id}
- className="p-2 rounded border bg-zinc-800/50 border-zinc-700 text-zinc-400 disabled:opacity-50"
- >
- {deletingId === domain.id
- ?
- :
- }
-
-
- {/* Sell Button - Tier-based (Mobile) */}
- {canSell ? (
-
handleSellDomain(domain.name)}
- disabled={!canCreateListing}
- className={clsx(
- "px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center gap-1.5",
- canCreateListing
- ? "bg-emerald-500 text-white"
- : "bg-zinc-700 text-zinc-400 cursor-not-allowed"
- )}
- >
-
- Sell
- {isTycoon && canCreateListing && (
-
- )}
-
- ) : (
-
-
- Upgrade
-
- )}
-
-
- )
- })}
-
- >
- )}
-
-
- {/* Portfolio Footer with Listing Info */}
-
-
-
- Same monitoring as Watching tab
-
-
-
- Get alerts for changes
-
- {canSell && (
-
-
- {currentListingCount}/{maxListings} listings
- {isTycoon && }
-
- )}
- {!canSell && (
-
-
- Upgrade to sell
-
- )}
-
- >
- )}
-
-
- {/* Health Report Modal */}
- {selectedHealthDomainId && healthReports[selectedHealthDomainId] && (
-
setSelectedHealthDomainId(null)}
- />
- )}
-
- {/* Sell Wizard Modal */}
- {showSellModal && (
- setShowSellModal(false)}
- >
-
e.stopPropagation()}
- >
- {/* Header with Steps */}
-
-
-
-
-
-
-
-
List for Sale
-
{sellDomainName}
-
-
-
setShowSellModal(false)}
- className="p-2 hover:bg-white/10 rounded-full text-zinc-500 hover:text-white transition-colors"
- >
-
-
-
-
- {/* Step Indicator */}
-
- {[1, 2, 3].map((step) => (
-
-
= step
- ? "bg-emerald-500 text-black border-emerald-500"
- : "bg-zinc-800 text-zinc-500 border-zinc-700"
- )}>
- {sellStep > step ? : step}
-
-
= step ? "text-emerald-400" : "text-zinc-600"
- )}>
- {step === 1 ? 'Details' : step === 2 ? 'Verify' : 'Done'}
-
- {step < 3 &&
step ? "bg-emerald-500" : "bg-zinc-800")} />}
-
- ))}
-
-
-
- {/* Step 1: Listing Details */}
- {sellStep === 1 && (
-
-
-
- setSellForm({ ...sellForm, title: e.target.value })}
- placeholder="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"
- />
-
-
-
-
-
-
-
- setSellForm({ ...sellForm, 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"
- />
-
-
-
-
-
-
-
-
-
-
-
- {/* Limits Info */}
-
- Your listing slots:
- {currentListingCount}/{maxListings}
-
-
-
-
setShowSellModal(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"
- >
- Cancel
-
-
- {sellLoading ? : }
- {sellLoading ? 'Creating...' : 'Continue'}
-
-
-
- )}
-
- {/* Step 2: DNS Verification */}
- {sellStep === 2 && sellVerificationInfo && (
-
-
-
-
-
-
Verify Ownership
-
- Add this TXT record to your domain's DNS to prove ownership.
-
-
-
-
-
-
-
-
-
-
Name / Host
-
copyToClipboard(sellVerificationInfo.dns_record_name, 'name')}
- >
- {sellVerificationInfo.dns_record_name}
- {copiedField === 'name'
- ?
- :
- }
-
-
-
-
-
Value
-
copyToClipboard(sellVerificationInfo.dns_record_value, 'value')}
- >
- {sellVerificationInfo.dns_record_value}
- {copiedField === 'value'
- ?
- :
- }
-
-
-
-
-
-
- 💡 DNS changes can take up to 24 hours to propagate, but usually work within minutes.
-
-
-
-
- setShowSellModal(false)}
- className="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
-
-
- {sellLoading ? : }
- {sellLoading ? 'Checking...' : 'Verify & Publish'}
-
-
-
- )}
-
- {/* Step 3: Success */}
- {sellStep === 3 && (
-
-
-
-
-
-
-
Domain Listed!
-
- {sellDomainName} is now live on the Pounce Marketplace.
-
-
-
- {isTycoon && (
-
-
- Featured Listing Active
-
- )}
-
-
- setShowSellModal(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
-
-
- View Listings
-
-
-
- )}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* FOOTER */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
+
+
+
Last sync: {getTimeAgo(new Date().toISOString())}
- )}
+
+
-
+
)
-}
-
-// Health Report Modal Component
-const HealthReportModal = memo(function HealthReportModal({
- report,
- onClose
-}: {
- report: DomainHealthReport
- onClose: () => void
-}) {
- const config = healthStatusConfig[report.status]
- const Icon = config.icon
-
- // Safely access nested properties
- const dns = report.dns || {}
- const http = report.http || {}
- const ssl = report.ssl || {}
-
- return (
-
-
e.stopPropagation()}
- >
- {/* Header */}
-
-
-
-
-
-
-
{report.domain}
-
{config.description}
-
-
-
-
-
-
-
- {/* Score */}
-
-
- 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 */}
-
-
- {/* Infrastructure */}
-
-
- Infrastructure
-
-
- {/* DNS */}
-
-
DNS Status
-
- {dns.has_ns ? '● Active' : '○ No Records'}
-
- {dns.nameservers && dns.nameservers.length > 0 && (
-
- {dns.nameservers[0]}
-
- )}
-
-
- {/* Web Server */}
-
-
Web Server
-
- {http.is_reachable
- ? `● HTTP ${http.status_code || 200}`
- : http.error
- ? `○ ${http.error}`
- : '○ Unreachable'
- }
-
- {http.content_length !== undefined && http.content_length > 0 && (
-
- {(http.content_length / 1024).toFixed(1)} KB
-
- )}
-
-
- {/* A Record */}
-
-
A Record
-
- {dns.has_a ? '● Configured' : '○ Not set'}
-
-
-
- {/* MX Record */}
-
-
Mail (MX)
-
- {dns.has_mx ? '● Configured' : '○ Not set'}
-
-
-
-
-
- {/* Security */}
-
-
- Security
-
-
-
- SSL Certificate
-
- {ssl.has_certificate && ssl.is_valid ? 'Secure' :
- ssl.has_certificate ? 'Invalid' : 'None'}
-
-
-
- {ssl.issuer && (
-
- Issuer
- {ssl.issuer}
-
- )}
-
- {ssl.days_until_expiry !== undefined && ssl.days_until_expiry !== null && (
-
- Expires in
-
- {ssl.days_until_expiry} days
-
-
- )}
-
- {ssl.error && (
-
- {ssl.error}
-
- )}
-
-
-
- {/* Parking Detection */}
- {(dns.is_parked || http.is_parked) && (
-
-
- Parking Detected
-
-
-
- This domain appears to be parked or for sale.
- {dns.parking_provider && (
- Provider: {dns.parking_provider}
- )}
-
- {http.parking_keywords && http.parking_keywords.length > 0 && (
-
- Keywords: {http.parking_keywords.slice(0, 3).join(', ')}
-
- )}
-
-
- )}
-
- {/* Signals */}
- {report.signals && report.signals.length > 0 && (
-
-
- Signals
-
-
- {report.signals.map((signal, i) => (
- -
- {signal}
-
- ))}
-
-
- )}
-
- {/* Recommendations */}
- {report.recommendations && report.recommendations.length > 0 && (
-
-
- Recommendations
-
-
- {report.recommendations.map((rec, i) => (
- -
- {rec}
-
- ))}
-
-
- )}
-
-
- {/* Footer */}
-
-
- LAST CHECK: {new Date(report.checked_at).toLocaleString().toUpperCase()}
-
-
-
-
- )
-})
+}
\ No newline at end of file