From 5b200a21bcc353162b44b3c635b80f3dde358e07 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Wed, 10 Dec 2025 21:59:56 +0100 Subject: [PATCH] cleanup: Remove old Command Center files and fix all references - Removed old folders: dashboard, pricing, auctions, marketplace, portfolio, alerts, seo - Removed CommandCenterLayout.tsx (replaced by TerminalLayout) - Fixed all internal links to use new terminal routes - Updated keyboard shortcuts for new module names - Fixed welcome page next steps - Fixed landing page feature links - Fixed radar page stat cards and links --- frontend/src/app/page.tsx | 4 +- frontend/src/app/terminal/alerts/page.tsx | 597 ----------- frontend/src/app/terminal/auctions/page.tsx | 578 ----------- .../src/app/terminal/marketplace/page.tsx | 302 ------ frontend/src/app/terminal/portfolio/page.tsx | 951 ------------------ frontend/src/app/terminal/radar/page.tsx | 8 +- frontend/src/app/terminal/seo/page.tsx | 508 ---------- frontend/src/app/terminal/welcome/page.tsx | 8 +- frontend/src/hooks/useKeyboardShortcuts.tsx | 4 +- 9 files changed, 12 insertions(+), 2948 deletions(-) delete mode 100644 frontend/src/app/terminal/alerts/page.tsx delete mode 100644 frontend/src/app/terminal/auctions/page.tsx delete mode 100644 frontend/src/app/terminal/marketplace/page.tsx delete mode 100644 frontend/src/app/terminal/portfolio/page.tsx delete mode 100644 frontend/src/app/terminal/seo/page.tsx diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index d51d93b..383548d 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -489,7 +489,7 @@ export default function HomePage() { Set Up @@ -536,7 +536,7 @@ export default function HomePage() { Manage diff --git a/frontend/src/app/terminal/alerts/page.tsx b/frontend/src/app/terminal/alerts/page.tsx deleted file mode 100644 index 4e658c7..0000000 --- a/frontend/src/app/terminal/alerts/page.tsx +++ /dev/null @@ -1,597 +0,0 @@ -'use client' - -import { useEffect, useState, useMemo, useCallback, memo } from 'react' -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, - Bell, - Target, - Zap, - Loader2, - Trash2, - CheckCircle, - AlertCircle, - X, - Play, - Pause, - Mail, - Settings, - TestTube, - ChevronDown, - ChevronUp, -} from 'lucide-react' -import clsx from 'clsx' - -interface SniperAlert { - id: number - name: string - description: string | null - tlds: string | null - keywords: string | null - exclude_keywords: string | null - max_length: number | null - min_length: number | null - max_price: number | null - min_price: number | null - max_bids: number | null - ending_within_hours: number | null - platforms: string | null - no_numbers: boolean - no_hyphens: boolean - exclude_chars: string | null - notify_email: boolean - notify_sms: boolean - is_active: boolean - matches_count: number - notifications_sent: number - last_matched_at: string | null - created_at: string -} - -interface TestResult { - alert_name: string - auctions_checked: number - matches_found: number - matches: Array<{ - domain: string - platform: string - current_bid: number - num_bids: number - end_time: string - }> - message: string -} - -export default function SniperAlertsPage() { - const { subscription } = useStore() - - const [alerts, setAlerts] = useState([]) - const [loading, setLoading] = useState(true) - const [showCreateModal, setShowCreateModal] = useState(false) - const [creating, setCreating] = useState(false) - const [testing, setTesting] = useState(null) - const [testResult, setTestResult] = useState(null) - const [expandedAlert, setExpandedAlert] = useState(null) - const [error, setError] = useState(null) - const [success, setSuccess] = useState(null) - - // Create form - const [newAlert, setNewAlert] = useState({ - name: '', - description: '', - tlds: '', - keywords: '', - exclude_keywords: '', - max_length: '', - min_length: '', - max_price: '', - min_price: '', - max_bids: '', - no_numbers: false, - no_hyphens: false, - exclude_chars: '', - notify_email: true, - }) - - const loadAlerts = useCallback(async () => { - setLoading(true) - try { - const data = await api.request('/sniper-alerts') - setAlerts(data) - } catch (err: any) { - setError(err.message) - } finally { - setLoading(false) - } - }, []) - - useEffect(() => { - loadAlerts() - }, [loadAlerts]) - - const handleCreate = useCallback(async (e: React.FormEvent) => { - e.preventDefault() - setCreating(true) - setError(null) - - try { - await api.request('/sniper-alerts', { - method: 'POST', - body: JSON.stringify({ - name: newAlert.name, - description: newAlert.description || null, - tlds: newAlert.tlds || null, - keywords: newAlert.keywords || null, - exclude_keywords: newAlert.exclude_keywords || null, - max_length: newAlert.max_length ? parseInt(newAlert.max_length) : null, - min_length: newAlert.min_length ? parseInt(newAlert.min_length) : null, - max_price: newAlert.max_price ? parseFloat(newAlert.max_price) : null, - min_price: newAlert.min_price ? parseFloat(newAlert.min_price) : null, - max_bids: newAlert.max_bids ? parseInt(newAlert.max_bids) : null, - no_numbers: newAlert.no_numbers, - no_hyphens: newAlert.no_hyphens, - exclude_chars: newAlert.exclude_chars || null, - notify_email: newAlert.notify_email, - }), - }) - setSuccess('Sniper Alert created!') - setShowCreateModal(false) - setNewAlert({ - name: '', description: '', tlds: '', keywords: '', exclude_keywords: '', - max_length: '', min_length: '', max_price: '', min_price: '', max_bids: '', - no_numbers: false, no_hyphens: false, exclude_chars: '', notify_email: true, - }) - loadAlerts() - } catch (err: any) { - setError(err.message) - } finally { - setCreating(false) - } - }, [newAlert, loadAlerts]) - - const handleToggle = useCallback(async (alert: SniperAlert) => { - try { - await api.request(`/sniper-alerts/${alert.id}`, { - method: 'PUT', - body: JSON.stringify({ is_active: !alert.is_active }), - }) - loadAlerts() - } catch (err: any) { - setError(err.message) - } - }, [loadAlerts]) - - const handleDelete = useCallback(async (alert: SniperAlert) => { - if (!confirm(`Delete alert "${alert.name}"?`)) return - - try { - await api.request(`/sniper-alerts/${alert.id}`, { method: 'DELETE' }) - setSuccess('Alert deleted') - loadAlerts() - } catch (err: any) { - setError(err.message) - } - }, [loadAlerts]) - - const handleTest = useCallback(async (alert: SniperAlert) => { - setTesting(alert.id) - setTestResult(null) - - try { - const result = await api.request(`/sniper-alerts/${alert.id}/test`, { - method: 'POST', - }) - setTestResult(result) - setExpandedAlert(alert.id) - } catch (err: any) { - setError(err.message) - } finally { - setTesting(null) - } - }, []) - - // Memoized stats - const stats = useMemo(() => ({ - activeAlerts: alerts.filter(a => a.is_active).length, - totalMatches: alerts.reduce((sum, a) => sum + a.matches_count, 0), - notificationsSent: alerts.reduce((sum, a) => sum + a.notifications_sent, 0), - }), [alerts]) - - const tier = subscription?.tier || 'scout' - const limits = { scout: 2, trader: 10, tycoon: 50 } - const maxAlerts = limits[tier as keyof typeof limits] || 2 - - return ( - setShowCreateModal(true)} disabled={alerts.length >= maxAlerts} icon={Plus}> - New Alert - - } - > - - {/* Messages */} - {error && ( -
- -

{error}

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

{success}

- -
- )} - - {/* Stats */} -
- - - - -
- - {/* Alerts List */} - {loading ? ( -
- -
- ) : alerts.length === 0 ? ( -
- -

No Sniper Alerts

-

- Create alerts to get notified when domains matching your criteria appear in auctions. -

- -
- ) : ( -
- {alerts.map((alert) => ( -
- {/* Header */} -
-
-
-
-

{alert.name}

- - {alert.is_active ? 'Active' : 'Paused'} - -
- {alert.description && ( -

{alert.description}

- )} -
- - {/* Stats */} -
-
-

{alert.matches_count}

-

Matches

-
-
-

{alert.notifications_sent}

-

Notified

-
-
- - {/* Actions */} -
- - - - - - - -
-
- - {/* Filter Summary */} -
- {alert.tlds && ( - - TLDs: {alert.tlds} - - )} - {alert.max_length && ( - - Max {alert.max_length} chars - - )} - {alert.max_price && ( - - Max ${alert.max_price} - - )} - {alert.no_numbers && ( - - No numbers - - )} - {alert.no_hyphens && ( - - No hyphens - - )} - {alert.notify_email && ( - - Email - - )} -
-
- - {/* Test Results */} - {expandedAlert === alert.id && testResult && testResult.alert_name === alert.name && ( -
-
-
-

Test Results

-

- Checked {testResult.auctions_checked} auctions -

-
- - {testResult.matches_found === 0 ? ( -

{testResult.message}

- ) : ( -
-

- Found {testResult.matches_found} matching domains! -

-
- {testResult.matches.map((match, idx) => ( -
- {match.domain} - ${match.current_bid} -
- ))} -
-
- )} -
-
- )} -
- ))} -
- )} -
- - {/* Create Modal */} - {showCreateModal && ( -
-
-

Create Sniper Alert

-

- Get notified when domains matching your criteria appear in auctions. -

- -
-
- - setNewAlert({ ...newAlert, name: e.target.value })} - placeholder="4-letter .com without numbers" - 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" - /> -
- -
- - setNewAlert({ ...newAlert, description: e.target.value })} - placeholder="Optional description" - 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" - /> -
- -
-
- - setNewAlert({ ...newAlert, tlds: e.target.value })} - placeholder="com,io,ai" - 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" - /> -
- -
- - setNewAlert({ ...newAlert, keywords: e.target.value })} - placeholder="ai,tech,crypto" - 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" - /> -
-
- -
-
- - setNewAlert({ ...newAlert, min_length: e.target.value })} - placeholder="3" - 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" - /> -
- -
- - setNewAlert({ ...newAlert, max_length: e.target.value })} - placeholder="6" - 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" - /> -
-
- -
-
- - setNewAlert({ ...newAlert, max_price: e.target.value })} - placeholder="500" - 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" - /> -
- -
- - setNewAlert({ ...newAlert, max_bids: e.target.value })} - placeholder="5" - 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" - /> -
-
- -
- - setNewAlert({ ...newAlert, exclude_chars: e.target.value })} - placeholder="q,x,z" - 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" - /> -
- -
- - - - - -
-
- -
- - -
-
-
- )} -
- ) -} - diff --git a/frontend/src/app/terminal/auctions/page.tsx b/frontend/src/app/terminal/auctions/page.tsx deleted file mode 100644 index 3dd1a91..0000000 --- a/frontend/src/app/terminal/auctions/page.tsx +++ /dev/null @@ -1,578 +0,0 @@ -'use client' - -import { useEffect, useState, useMemo, useCallback, memo } from 'react' -import { useStore } from '@/lib/store' -import { api } from '@/lib/api' -import { TerminalLayout } from '@/components/TerminalLayout' -import { - PremiumTable, - Badge, - PlatformBadge, - StatCard, - PageContainer, - SearchInput, - TabBar, - FilterBar, - SelectDropdown, - ActionButton, -} from '@/components/PremiumTable' -import { - Clock, - ExternalLink, - Flame, - Timer, - Gavel, - DollarSign, - RefreshCw, - Target, - Loader2, - Sparkles, - Eye, - Zap, - Crown, - Plus, - Check, -} from 'lucide-react' -import Link from 'next/link' -import clsx from 'clsx' - -interface Auction { - domain: string - platform: string - platform_url: string - current_bid: number - currency: string - num_bids: number - end_time: string - time_remaining: string - buy_now_price: number | null - reserve_met: boolean | null - traffic: number | null - age_years: number | null - tld: string - affiliate_url: string -} - -interface Opportunity { - auction: Auction - analysis: { - opportunity_score: number - urgency?: string - competition?: string - price_range?: string - recommendation: string - reasoning?: string - } -} - -type TabType = 'all' | 'ending' | 'hot' | 'opportunities' -type SortField = 'ending' | 'bid_asc' | 'bid_desc' | 'bids' | 'score' -type FilterPreset = 'all' | 'no-trash' | 'short' | 'high-value' | 'low-competition' - -const PLATFORMS = [ - { value: 'All', label: 'All Sources' }, - { value: 'GoDaddy', label: 'GoDaddy' }, - { value: 'Sedo', label: 'Sedo' }, - { value: 'NameJet', label: 'NameJet' }, - { value: 'DropCatch', label: 'DropCatch' }, -] - -const FILTER_PRESETS: { id: FilterPreset, label: string, icon: typeof Gavel, description: string, proOnly?: boolean }[] = [ - { id: 'all', label: 'All', icon: Gavel, description: 'Show all auctions' }, - { id: 'no-trash', label: 'No Trash', icon: Sparkles, description: 'Clean domains only (no spam)', proOnly: true }, - { id: 'short', label: 'Short', icon: Zap, description: '4-letter domains or less' }, - { id: 'high-value', label: 'High Value', icon: Crown, description: 'Premium TLDs with high activity', proOnly: true }, - { id: 'low-competition', label: 'Low Competition', icon: Target, description: 'Under 5 bids' }, -] - -const PREMIUM_TLDS = ['com', 'io', 'ai', 'co', 'net', 'org', 'app', 'dev'] - -// Pure functions (no hooks needed) -function isCleanDomain(auction: Auction): boolean { - const name = auction.domain.split('.')[0] - if (name.includes('-')) return false - if (name.length > 4 && /\d/.test(name)) return false - if (name.length > 12) return false - if (!PREMIUM_TLDS.includes(auction.tld)) return false - return true -} - -function calculateDealScore(auction: Auction): number { - let score = 50 - const name = auction.domain.split('.')[0] - if (name.length <= 4) score += 25 - else if (name.length <= 6) score += 15 - else if (name.length <= 8) score += 5 - if (['com', 'io', 'ai'].includes(auction.tld)) score += 15 - else if (['co', 'net', 'org'].includes(auction.tld)) score += 5 - if (auction.age_years && auction.age_years > 10) score += 15 - else if (auction.age_years && auction.age_years > 5) score += 10 - if (auction.num_bids >= 20) score += 10 - else if (auction.num_bids >= 10) score += 5 - if (isCleanDomain(auction)) score += 10 - return Math.min(score, 100) -} - -function getTimeColor(timeRemaining: string): string { - if (timeRemaining.includes('m') && !timeRemaining.includes('h') && !timeRemaining.includes('d')) return 'text-red-400' - if (timeRemaining.includes('h') && parseInt(timeRemaining) < 2) return 'text-amber-400' - return 'text-foreground-muted' -} - -const formatCurrency = (value: number) => { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(value) -} - -export default function AuctionsPage() { - const { isAuthenticated, subscription } = useStore() - - const [allAuctions, setAllAuctions] = useState([]) - const [endingSoon, setEndingSoon] = useState([]) - const [hotAuctions, setHotAuctions] = useState([]) - const [opportunities, setOpportunities] = useState([]) - const [loading, setLoading] = useState(true) - const [refreshing, setRefreshing] = useState(false) - const [activeTab, setActiveTab] = useState('all') - const [sortBy, setSortBy] = useState('ending') - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') - - // Filters - const [searchQuery, setSearchQuery] = useState('') - const [selectedPlatform, setSelectedPlatform] = useState('All') - const [maxBid, setMaxBid] = useState('') - const [filterPreset, setFilterPreset] = useState('all') - const [trackedDomains, setTrackedDomains] = useState>(new Set()) - const [trackingInProgress, setTrackingInProgress] = useState(null) - - const isPaidUser = subscription?.tier === 'trader' || subscription?.tier === 'tycoon' - - // Data loading - const loadData = useCallback(async () => { - setLoading(true) - try { - const [auctionsData, hotData, endingData] = await Promise.all([ - api.getAuctions(), - api.getHotAuctions(50), - api.getEndingSoonAuctions(24, 50), - ]) - - setAllAuctions(auctionsData.auctions || []) - setHotAuctions(hotData || []) - setEndingSoon(endingData || []) - } catch (error) { - console.error('Failed to load auction data:', error) - } finally { - setLoading(false) - } - }, []) - - const loadOpportunities = useCallback(async () => { - try { - const oppData = await api.getAuctionOpportunities() - setOpportunities(oppData.opportunities || []) - } catch (e) { - console.error('Failed to load opportunities:', e) - } - }, []) - - useEffect(() => { - loadData() - }, [loadData]) - - useEffect(() => { - if (isAuthenticated && opportunities.length === 0) { - loadOpportunities() - } - }, [isAuthenticated, opportunities.length, loadOpportunities]) - - const handleRefresh = useCallback(async () => { - setRefreshing(true) - await loadData() - if (isAuthenticated) await loadOpportunities() - setRefreshing(false) - }, [loadData, loadOpportunities, isAuthenticated]) - - const handleTrackDomain = useCallback(async (domain: string) => { - if (trackedDomains.has(domain)) return - - setTrackingInProgress(domain) - try { - await api.addDomain(domain) - setTrackedDomains(prev => new Set([...Array.from(prev), domain])) - } catch (error) { - console.error('Failed to track domain:', error) - } finally { - setTrackingInProgress(null) - } - }, [trackedDomains]) - - const handleSort = useCallback((field: string) => { - const f = field as SortField - if (sortBy === f) { - setSortDirection(d => d === 'asc' ? 'desc' : 'asc') - } else { - setSortBy(f) - setSortDirection('asc') - } - }, [sortBy]) - - // Memoized tabs - const tabs = useMemo(() => [ - { id: 'all', label: 'All', icon: Gavel, count: allAuctions.length }, - { id: 'ending', label: 'Ending Soon', icon: Timer, count: endingSoon.length, color: 'warning' as const }, - { id: 'hot', label: 'Hot', icon: Flame, count: hotAuctions.length }, - { id: 'opportunities', label: 'Opportunities', icon: Target, count: opportunities.length }, - ], [allAuctions.length, endingSoon.length, hotAuctions.length, opportunities.length]) - - // Filter and sort auctions - const sortedAuctions = useMemo(() => { - // Get base auctions for current tab - let auctions: Auction[] = [] - switch (activeTab) { - case 'ending': auctions = [...endingSoon]; break - case 'hot': auctions = [...hotAuctions]; break - case 'opportunities': auctions = opportunities.map(o => o.auction); break - default: auctions = [...allAuctions] - } - - // Apply preset filter - const baseFilter = filterPreset === 'all' && isPaidUser ? 'no-trash' : filterPreset - switch (baseFilter) { - case 'no-trash': auctions = auctions.filter(isCleanDomain); break - case 'short': auctions = auctions.filter(a => a.domain.split('.')[0].length <= 4); break - case 'high-value': auctions = auctions.filter(a => - PREMIUM_TLDS.slice(0, 3).includes(a.tld) && a.num_bids >= 5 && calculateDealScore(a) >= 70 - ); break - case 'low-competition': auctions = auctions.filter(a => a.num_bids < 5); break - } - - // Apply search - if (searchQuery) { - const q = searchQuery.toLowerCase() - auctions = auctions.filter(a => a.domain.toLowerCase().includes(q)) - } - - // Apply platform filter - if (selectedPlatform !== 'All') { - auctions = auctions.filter(a => a.platform === selectedPlatform) - } - - // Apply max bid - if (maxBid) { - const max = parseFloat(maxBid) - auctions = auctions.filter(a => a.current_bid <= max) - } - - // Sort (skip for opportunities - already sorted by score) - if (activeTab !== 'opportunities') { - const mult = sortDirection === 'asc' ? 1 : -1 - auctions.sort((a, b) => { - switch (sortBy) { - case 'ending': return mult * (new Date(a.end_time).getTime() - new Date(b.end_time).getTime()) - case 'bid_asc': - case 'bid_desc': return mult * (a.current_bid - b.current_bid) - case 'bids': return mult * (b.num_bids - a.num_bids) - case 'score': return mult * (calculateDealScore(b) - calculateDealScore(a)) - default: return 0 - } - }) - } - - return auctions - }, [activeTab, allAuctions, endingSoon, hotAuctions, opportunities, filterPreset, isPaidUser, searchQuery, selectedPlatform, maxBid, sortBy, sortDirection]) - - // Subtitle - const subtitle = useMemo(() => { - if (loading) return 'Loading live auctions...' - const total = allAuctions.length - if (total === 0) return 'No active auctions found' - return `${sortedAuctions.length.toLocaleString()} auctions ${sortedAuctions.length < total ? `(filtered from ${total.toLocaleString()})` : 'across 4 platforms'}` - }, [loading, allAuctions.length, sortedAuctions.length]) - - // Get opportunity data helper - const getOpportunityData = useCallback((domain: string) => { - if (activeTab !== 'opportunities') return null - return opportunities.find(o => o.auction.domain === domain)?.analysis - }, [activeTab, opportunities]) - - // Table columns - memoized - const columns = useMemo(() => [ - { - key: 'domain', - header: 'Domain', - sortable: true, - render: (a: Auction) => ( -
- - {a.domain} - -
- - {a.age_years && {a.age_years}y} -
-
- ), - }, - { - key: 'platform', - header: 'Platform', - hideOnMobile: true, - render: (a: Auction) => ( -
- - {a.age_years && ( - - {a.age_years}y - - )} -
- ), - }, - { - key: 'bid_asc', - header: 'Bid', - sortable: true, - align: 'right' as const, - render: (a: Auction) => ( -
- {formatCurrency(a.current_bid)} - {a.buy_now_price && ( -

Buy: {formatCurrency(a.buy_now_price)}

- )} -
- ), - }, - { - key: 'score', - header: 'Deal Score', - sortable: true, - align: 'center' as const, - hideOnMobile: true, - render: (a: Auction) => { - if (activeTab === 'opportunities') { - const oppData = getOpportunityData(a.domain) - if (oppData) { - return ( - - {oppData.opportunity_score} - - ) - } - } - - if (!isPaidUser) { - return ( - - - - ) - } - - const score = calculateDealScore(a) - return ( -
- = 75 ? "bg-accent/20 text-accent" : - score >= 50 ? "bg-amber-500/20 text-amber-400" : - "bg-foreground/10 text-foreground-muted" - )}> - {score} - - {score >= 75 && Undervalued} -
- ) - }, - }, - { - key: 'bids', - header: 'Bids', - sortable: true, - align: 'right' as const, - hideOnMobile: true, - render: (a: Auction) => ( - = 20 ? "text-accent" : a.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted" - )}> - {a.num_bids} - {a.num_bids >= 20 && } - - ), - }, - { - key: 'ending', - header: 'Time Left', - sortable: true, - align: 'right' as const, - hideOnMobile: true, - render: (a: Auction) => ( - - {a.time_remaining} - - ), - }, - { - key: 'actions', - header: '', - align: 'right' as const, - render: (a: Auction) => ( -
- - - Bid - -
- ), - }, - ], [activeTab, isPaidUser, trackedDomains, trackingInProgress, handleTrackDomain, getOpportunityData]) - - return ( - - {refreshing ? '' : 'Refresh'} - - } - > - - {/* Stats */} -
- - - - -
- - {/* Tabs */} - setActiveTab(id as TabType)} /> - - {/* Smart Filter Presets */} -
- {FILTER_PRESETS.map((preset) => { - const isDisabled = preset.proOnly && !isPaidUser - const isActive = filterPreset === preset.id - const Icon = preset.icon - return ( - - ) - })} -
- - {/* Tier notification for Scout users */} - {!isPaidUser && ( -
-
- -
-
-

You're seeing the raw auction feed

-

- Upgrade to Trader for spam-free listings, Deal Scores, and Smart Filters. -

-
- - Upgrade - -
- )} - - {/* Filters */} - - - -
- - setMaxBid(e.target.value)} - className="w-28 h-10 pl-9 pr-3 bg-background-secondary/50 border border-border/40 rounded-xl - text-sm text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:border-accent/50 transition-all" - /> -
-
- - {/* Table */} - `${a.domain}-${a.platform}`} - loading={loading} - sortBy={sortBy} - sortDirection={sortDirection} - onSort={handleSort} - emptyIcon={} - emptyTitle={searchQuery ? `No auctions matching "${searchQuery}"` : "No auctions found"} - emptyDescription="Try adjusting your filters or check back later" - columns={columns} - /> -
-
- ) -} diff --git a/frontend/src/app/terminal/marketplace/page.tsx b/frontend/src/app/terminal/marketplace/page.tsx deleted file mode 100644 index bf5af59..0000000 --- a/frontend/src/app/terminal/marketplace/page.tsx +++ /dev/null @@ -1,302 +0,0 @@ -'use client' - -import { useEffect, useState, useMemo, useCallback } from 'react' -import { api } from '@/lib/api' -import { TerminalLayout } from '@/components/TerminalLayout' -import { - PageContainer, - StatCard, - Badge, - SearchInput, - FilterBar, - SelectDropdown, - ActionButton, -} from '@/components/PremiumTable' -import { - Search, - Shield, - Loader2, - ExternalLink, - Store, - Tag, - DollarSign, - Filter, -} from 'lucide-react' -import Link from 'next/link' -import clsx from 'clsx' - -interface Listing { - domain: string - slug: string - title: string | null - description: string | null - asking_price: number | null - currency: string - price_type: string - pounce_score: number | null - estimated_value: number | null - is_verified: boolean - allow_offers: boolean - public_url: string - seller_verified: boolean -} - -type SortOption = 'newest' | 'price_asc' | 'price_desc' | 'score' - -export default function CommandMarketplacePage() { - const [listings, setListings] = useState([]) - const [loading, setLoading] = useState(true) - const [searchQuery, setSearchQuery] = useState('') - const [minPrice, setMinPrice] = useState('') - const [maxPrice, setMaxPrice] = useState('') - const [verifiedOnly, setVerifiedOnly] = useState(false) - const [sortBy, setSortBy] = useState('newest') - const [showFilters, setShowFilters] = useState(false) - - const loadListings = useCallback(async () => { - setLoading(true) - try { - const params = new URLSearchParams() - params.set('limit', '100') - if (sortBy === 'price_asc') params.set('sort', 'price_asc') - if (sortBy === 'price_desc') params.set('sort', 'price_desc') - if (verifiedOnly) params.set('verified_only', 'true') - - const data = await api.request(`/listings?${params.toString()}`) - setListings(data) - } catch (err) { - console.error('Failed to load listings:', err) - } finally { - setLoading(false) - } - }, [sortBy, verifiedOnly]) - - useEffect(() => { - loadListings() - }, [loadListings]) - - const formatPrice = (price: number | null, currency: string) => { - if (!price) return 'Make Offer' - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency, - minimumFractionDigits: 0, - }).format(price) - } - - // Memoized filtered and sorted listings - const sortedListings = useMemo(() => { - let result = listings.filter(listing => { - if (searchQuery && !listing.domain.toLowerCase().includes(searchQuery.toLowerCase())) return false - if (minPrice && listing.asking_price && listing.asking_price < parseFloat(minPrice)) return false - if (maxPrice && listing.asking_price && listing.asking_price > parseFloat(maxPrice)) return false - return true - }) - - return result.sort((a, b) => { - switch (sortBy) { - case 'price_asc': return (a.asking_price || 0) - (b.asking_price || 0) - case 'price_desc': return (b.asking_price || 0) - (a.asking_price || 0) - case 'score': return (b.pounce_score || 0) - (a.pounce_score || 0) - default: return 0 - } - }) - }, [listings, searchQuery, minPrice, maxPrice, sortBy]) - - // Memoized stats - const stats = useMemo(() => { - const verifiedCount = listings.filter(l => l.is_verified).length - const pricesWithValue = listings.filter(l => l.asking_price) - const avgPrice = pricesWithValue.length > 0 - ? pricesWithValue.reduce((sum, l) => sum + (l.asking_price || 0), 0) / pricesWithValue.length - : 0 - return { verifiedCount, avgPrice } - }, [listings]) - - return ( - - My Listings - - } - > - - {/* Stats */} -
- - - 0 ? `$${Math.round(stats.avgPrice).toLocaleString()}` : '—'} - icon={DollarSign} - /> - -
- - {/* Search & Filters */} -
-
- {/* Search */} -
- - setSearchQuery(e.target.value)} - className="w-full pl-12 pr-4 py-3 bg-background border border-border rounded-xl - text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent" - /> -
- - {/* Sort */} - - - {/* Filter Toggle */} - -
- - {/* Expanded Filters */} - {showFilters && ( -
-
- Price: - setMinPrice(e.target.value)} - className="w-24 px-3 py-2 bg-background border border-border rounded-lg text-sm text-foreground - placeholder:text-foreground-subtle focus:outline-none focus:border-accent" - /> - - setMaxPrice(e.target.value)} - className="w-24 px-3 py-2 bg-background border border-border rounded-lg text-sm text-foreground - placeholder:text-foreground-subtle focus:outline-none focus:border-accent" - /> -
- - -
- )} -
- - {/* Listings Grid */} - {loading ? ( -
- -
- ) : sortedListings.length === 0 ? ( -
- -

No Domains Found

-

- {searchQuery || minPrice || maxPrice - ? 'Try adjusting your filters' - : 'No domains are currently listed for sale'} -

- - - List Your Domain - -
- ) : ( -
- {sortedListings.map((listing) => ( - -
-
-

- {listing.domain} -

- {listing.title && ( -

{listing.title}

- )} -
- {listing.is_verified && ( -
- -
- )} -
- - {listing.description && ( -

- {listing.description} -

- )} - -
-
- {listing.pounce_score && ( -
- {listing.pounce_score} -
- )} - {listing.allow_offers && ( - Offers - )} -
-
-

- {formatPrice(listing.asking_price, listing.currency)} -

- {listing.price_type === 'negotiable' && ( -

Negotiable

- )} -
-
- - ))} -
- )} -
-
- ) -} - diff --git a/frontend/src/app/terminal/portfolio/page.tsx b/frontend/src/app/terminal/portfolio/page.tsx deleted file mode 100644 index 3f4e4a0..0000000 --- a/frontend/src/app/terminal/portfolio/page.tsx +++ /dev/null @@ -1,951 +0,0 @@ -'use client' - -import { useEffect, useState, useMemo, useCallback, memo } from 'react' -import { useStore } from '@/lib/store' -import { api, PortfolioDomain, PortfolioSummary, DomainValuation, DomainHealthReport, HealthStatus } from '@/lib/api' -import { TerminalLayout } from '@/components/TerminalLayout' -import { PremiumTable, StatCard, PageContainer, ActionButton } from '@/components/PremiumTable' -import { Toast, useToast } from '@/components/Toast' -import { - Plus, - Trash2, - Edit2, - DollarSign, - Calendar, - Building, - Loader2, - ArrowUpRight, - X, - Briefcase, - ShoppingCart, - Activity, - Shield, - AlertTriangle, - Tag, - MoreVertical, - ExternalLink, -} from 'lucide-react' -import clsx from 'clsx' -import Link from 'next/link' - -// Health status configuration -const healthStatusConfig: Record = { - healthy: { label: 'Healthy', color: 'text-accent', bgColor: 'bg-accent/10', icon: Activity }, - weakening: { label: 'Weak', color: 'text-amber-400', bgColor: 'bg-amber-400/10', icon: AlertTriangle }, - parked: { label: 'Parked', color: 'text-orange-400', bgColor: 'bg-orange-400/10', icon: ShoppingCart }, - critical: { label: 'Critical', color: 'text-red-400', bgColor: 'bg-red-400/10', icon: AlertTriangle }, - unknown: { label: 'Unknown', color: 'text-foreground-muted', bgColor: 'bg-foreground/5', icon: Activity }, -} - -export default function PortfolioPage() { - const { subscription } = useStore() - const { toast, showToast, hideToast } = useToast() - - const [portfolio, setPortfolio] = useState([]) - const [summary, setSummary] = useState(null) - const [loading, setLoading] = useState(true) - const [showAddModal, setShowAddModal] = useState(false) - const [showEditModal, setShowEditModal] = useState(false) - const [showSellModal, setShowSellModal] = useState(false) - const [showValuationModal, setShowValuationModal] = useState(false) - const [selectedDomain, setSelectedDomain] = useState(null) - const [valuation, setValuation] = useState(null) - const [valuatingDomain, setValuatingDomain] = useState('') - const [addingDomain, setAddingDomain] = useState(false) - const [savingEdit, setSavingEdit] = useState(false) - const [processingSale, setProcessingSale] = useState(false) - const [refreshingId, setRefreshingId] = useState(null) - - // Health monitoring state - const [healthReports, setHealthReports] = useState>({}) - const [loadingHealth, setLoadingHealth] = useState>({}) - const [selectedHealthDomain, setSelectedHealthDomain] = useState(null) - - // Dropdown menu state - const [openMenuId, setOpenMenuId] = useState(null) - - const [addForm, setAddForm] = useState({ - domain: '', - purchase_price: '', - purchase_date: '', - registrar: '', - renewal_date: '', - renewal_cost: '', - notes: '', - }) - - const [editForm, setEditForm] = useState({ - purchase_price: '', - purchase_date: '', - registrar: '', - renewal_date: '', - renewal_cost: '', - notes: '', - }) - - const [sellForm, setSellForm] = useState({ - sale_date: new Date().toISOString().split('T')[0], - sale_price: '', - }) - - const loadPortfolio = useCallback(async () => { - setLoading(true) - try { - const [portfolioData, summaryData] = await Promise.all([ - api.getPortfolio(), - api.getPortfolioSummary(), - ]) - setPortfolio(portfolioData) - setSummary(summaryData) - } catch (error) { - console.error('Failed to load portfolio:', error) - } finally { - setLoading(false) - } - }, []) - - useEffect(() => { - loadPortfolio() - }, [loadPortfolio]) - - const handleAddDomain = useCallback(async (e: React.FormEvent) => { - e.preventDefault() - if (!addForm.domain.trim()) return - - setAddingDomain(true) - try { - await api.addPortfolioDomain({ - domain: addForm.domain.trim(), - purchase_price: addForm.purchase_price ? parseFloat(addForm.purchase_price) : undefined, - purchase_date: addForm.purchase_date || undefined, - registrar: addForm.registrar || undefined, - renewal_date: addForm.renewal_date || undefined, - renewal_cost: addForm.renewal_cost ? parseFloat(addForm.renewal_cost) : undefined, - notes: addForm.notes || undefined, - }) - showToast(`Added ${addForm.domain} to portfolio`, 'success') - setAddForm({ domain: '', purchase_price: '', purchase_date: '', registrar: '', renewal_date: '', renewal_cost: '', notes: '' }) - setShowAddModal(false) - loadPortfolio() - } catch (err: any) { - showToast(err.message || 'Failed to add domain', 'error') - } finally { - setAddingDomain(false) - } - }, [addForm, loadPortfolio, showToast]) - - const handleEditDomain = useCallback(async (e: React.FormEvent) => { - e.preventDefault() - if (!selectedDomain) return - - setSavingEdit(true) - try { - await api.updatePortfolioDomain(selectedDomain.id, { - purchase_price: editForm.purchase_price ? parseFloat(editForm.purchase_price) : undefined, - purchase_date: editForm.purchase_date || undefined, - registrar: editForm.registrar || undefined, - renewal_date: editForm.renewal_date || undefined, - renewal_cost: editForm.renewal_cost ? parseFloat(editForm.renewal_cost) : undefined, - notes: editForm.notes || undefined, - }) - showToast('Domain updated', 'success') - setShowEditModal(false) - loadPortfolio() - } catch (err: any) { - showToast(err.message || 'Failed to update', 'error') - } finally { - setSavingEdit(false) - } - }, [selectedDomain, editForm, loadPortfolio, showToast]) - - const handleSellDomain = useCallback(async (e: React.FormEvent) => { - e.preventDefault() - if (!selectedDomain || !sellForm.sale_price) return - - setProcessingSale(true) - try { - await api.markDomainSold(selectedDomain.id, sellForm.sale_date, parseFloat(sellForm.sale_price)) - showToast(`Marked ${selectedDomain.domain} as sold`, 'success') - setShowSellModal(false) - loadPortfolio() - } catch (err: any) { - showToast(err.message || 'Failed to process sale', 'error') - } finally { - setProcessingSale(false) - } - }, [selectedDomain, sellForm, loadPortfolio, showToast]) - - const handleValuate = useCallback(async (domain: PortfolioDomain) => { - setValuatingDomain(domain.domain) - setShowValuationModal(true) - try { - const result = await api.getDomainValuation(domain.domain) - setValuation(result) - } catch (err: any) { - showToast(err.message || 'Failed to get valuation', 'error') - setShowValuationModal(false) - } finally { - setValuatingDomain('') - } - }, [showToast]) - - const handleRefresh = useCallback(async (domain: PortfolioDomain) => { - setRefreshingId(domain.id) - try { - await api.refreshDomainValue(domain.id) - showToast('Valuation refreshed', 'success') - loadPortfolio() - } catch (err: any) { - showToast(err.message || 'Failed to refresh', 'error') - } finally { - setRefreshingId(null) - } - }, [loadPortfolio, showToast]) - - const handleHealthCheck = useCallback(async (domainName: string) => { - if (loadingHealth[domainName]) return - - setLoadingHealth(prev => ({ ...prev, [domainName]: true })) - try { - const report = await api.quickHealthCheck(domainName) - setHealthReports(prev => ({ ...prev, [domainName]: report })) - setSelectedHealthDomain(domainName) - } catch (err: any) { - showToast(err.message || 'Health check failed', 'error') - } finally { - setLoadingHealth(prev => ({ ...prev, [domainName]: false })) - } - }, [loadingHealth, showToast]) - - const handleDelete = useCallback(async (domain: PortfolioDomain) => { - if (!confirm(`Remove ${domain.domain} from your portfolio?`)) return - - try { - await api.deletePortfolioDomain(domain.id) - showToast(`Removed ${domain.domain}`, 'success') - loadPortfolio() - } catch (err: any) { - showToast(err.message || 'Failed to remove', 'error') - } - }, [loadPortfolio, showToast]) - - const openEditModal = useCallback((domain: PortfolioDomain) => { - setSelectedDomain(domain) - setEditForm({ - purchase_price: domain.purchase_price?.toString() || '', - purchase_date: domain.purchase_date || '', - registrar: domain.registrar || '', - renewal_date: domain.renewal_date || '', - renewal_cost: domain.renewal_cost?.toString() || '', - notes: domain.notes || '', - }) - setShowEditModal(true) - }, []) - - const openSellModal = useCallback((domain: PortfolioDomain) => { - setSelectedDomain(domain) - setSellForm({ - sale_date: new Date().toISOString().split('T')[0], - sale_price: '', - }) - setShowSellModal(true) - }, []) - - const portfolioLimit = subscription?.portfolio_limit || 0 - const canAddMore = portfolioLimit === -1 || portfolio.length < portfolioLimit - - // Memoized stats and subtitle - const { expiringSoonCount, subtitle } = useMemo(() => { - const expiring = portfolio.filter(d => { - if (!d.renewal_date) return false - const days = Math.ceil((new Date(d.renewal_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) - return days <= 30 && days > 0 - }).length - - let sub = '' - if (loading) sub = 'Loading your portfolio...' - else if (portfolio.length === 0) sub = 'Start tracking your domains' - else if (expiring > 0) sub = `${portfolio.length} domain${portfolio.length !== 1 ? 's' : ''} • ${expiring} expiring soon` - else sub = `Managing ${portfolio.length} domain${portfolio.length !== 1 ? 's' : ''}` - - return { expiringSoonCount: expiring, subtitle: sub } - }, [portfolio, loading]) - - return ( - setShowAddModal(true)} disabled={!canAddMore} icon={Plus}> - Add Domain - - } - > - {toast && } - - - {/* Summary Stats - Only reliable data */} -
- - - r.status !== 'healthy').length} - icon={AlertTriangle} - /> - -
- - {!canAddMore && ( -
-

- You've reached your portfolio limit. Upgrade to add more. -

- - Upgrade - -
- )} - - {/* Portfolio Table */} - d.id} - loading={loading} - emptyIcon={} - emptyTitle="Your portfolio is empty" - emptyDescription="Add your first domain to start tracking investments" - columns={[ - { - key: 'domain', - header: 'Domain', - render: (domain) => ( -
- {domain.domain} - {domain.registrar && ( -

- {domain.registrar} -

- )} -
- ), - }, - { - key: 'added', - header: 'Added', - hideOnMobile: true, - hideOnTablet: true, - render: (domain) => ( - - {domain.purchase_date - ? new Date(domain.purchase_date).toLocaleDateString() - : new Date(domain.created_at).toLocaleDateString() - } - - ), - }, - { - key: 'renewal', - header: 'Expires', - hideOnMobile: true, - render: (domain) => { - if (!domain.renewal_date) { - return - } - const days = Math.ceil((new Date(domain.renewal_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) - const isExpiringSoon = days <= 30 && days > 0 - const isExpired = days <= 0 - return ( -
- - {new Date(domain.renewal_date).toLocaleDateString()} - - {isExpiringSoon && ( - - {days}d - - )} - {isExpired && ( - - EXPIRED - - )} -
- ) - }, - }, - { - key: 'health', - header: 'Health', - hideOnMobile: true, - render: (domain) => { - const report = healthReports[domain.domain] - if (loadingHealth[domain.domain]) { - return - } - if (report) { - const config = healthStatusConfig[report.status] - const Icon = config.icon - return ( - - ) - } - return ( - - ) - }, - }, - { - key: 'actions', - header: '', - align: 'right', - render: (domain) => ( -
- - - {openMenuId === domain.id && ( - <> - {/* Backdrop */} -
setOpenMenuId(null)} - /> - {/* Menu - opens downward */} -
- - -
- setOpenMenuId(null)} - className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-accent hover:bg-accent/5 transition-colors" - > - - List for Sale - - setOpenMenuId(null)} - className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 transition-colors" - > - - Visit Website - -
- - -
- - )} -
- ), - }, - ]} - /> - - - {/* Add Modal */} - {showAddModal && ( - setShowAddModal(false)}> -
-
- - setAddForm({ ...addForm, domain: e.target.value })} - placeholder="example.com" - className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground - focus:outline-none focus:border-accent/50 transition-all" - required - /> -
-
-
- - setAddForm({ ...addForm, purchase_price: e.target.value })} - placeholder="100" - className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground - focus:outline-none focus:border-accent/50" - /> -
-
- - setAddForm({ ...addForm, purchase_date: e.target.value })} - className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground - focus:outline-none focus:border-accent/50" - /> -
-
-
- - setAddForm({ ...addForm, registrar: e.target.value })} - placeholder="Namecheap" - className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground - focus:outline-none focus:border-accent/50" - /> -
-
- - -
-
-
- )} - - {/* Edit Modal */} - {showEditModal && selectedDomain && ( - setShowEditModal(false)}> -
-
-
- - setEditForm({ ...editForm, purchase_price: e.target.value })} - className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground - focus:outline-none focus:border-accent/50" - /> -
-
- - setEditForm({ ...editForm, registrar: e.target.value })} - className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground - focus:outline-none focus:border-accent/50" - /> -
-
-
- - -
-
-
- )} - - {/* Record Sale Modal - for tracking completed sales */} - {showSellModal && selectedDomain && ( - setShowSellModal(false)}> -
-
-

Record a completed sale to track your profit/loss. This will mark the domain as sold in your portfolio.

-

Want to list it for sale instead? Use the "List" button.

-
-
-
- - setSellForm({ ...sellForm, sale_price: e.target.value })} - placeholder="1000" - className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground - focus:outline-none focus:border-accent/50" - required - /> -
-
- - setSellForm({ ...sellForm, sale_date: e.target.value })} - className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground - focus:outline-none focus:border-accent/50" - /> -
-
-
- - -
-
-
- )} - - {/* Valuation Modal */} - {showValuationModal && ( - { setShowValuationModal(false); setValuation(null); }}> - {valuatingDomain ? ( -
- -
- ) : valuation ? ( -
-
-

${valuation.estimated_value.toLocaleString()}

-

Pounce Score Estimate

-
-
-
- Confidence Level - - {valuation.confidence} - -
-
-

This is an algorithmic estimate based on domain length, TLD, and market patterns. Actual market value may vary.

-
-
-
- ) : null} -
- )} - - {/* Health Report Modal */} - {selectedHealthDomain && healthReports[selectedHealthDomain] && ( - setSelectedHealthDomain(null)} - /> - )} - - ) -} - -// Health Report Modal Component -function HealthReportModal({ report, onClose }: { report: DomainHealthReport; onClose: () => void }) { - const config = healthStatusConfig[report.status] - const Icon = config.icon - - return ( -
-
e.stopPropagation()} - > - {/* Header */} -
-
-
- -
-
-

{report.domain}

-

{config.label}

-
-
- -
- - {/* 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 - -
-
-
- - {/* Check Results */} -
- {/* DNS */} - {report.dns && ( -
-

- - DNS Infrastructure -

-
-
- - {report.dns.has_ns ? '✓' : '✗'} - - Nameservers -
-
- - {report.dns.has_a ? '✓' : '✗'} - - A Record -
-
- - {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} - - )} -
- {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

-
    - {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()} -

-
-
-
- ) -} - -// Modal Component -function Modal({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) { - return ( -
-
e.stopPropagation()} - > -
-

{title}

- -
-
- {children} -
-
-
- ) -} diff --git a/frontend/src/app/terminal/radar/page.tsx b/frontend/src/app/terminal/radar/page.tsx index 62f8262..3e87ce5 100644 --- a/frontend/src/app/terminal/radar/page.tsx +++ b/frontend/src/app/terminal/radar/page.tsx @@ -200,10 +200,10 @@ export default function DashboardPage() { accent={availableDomains.length > 0} /> - + @@ -291,7 +291,7 @@ export default function DashboardPage() { icon={Gavel} compact action={ - + View all → } diff --git a/frontend/src/app/terminal/seo/page.tsx b/frontend/src/app/terminal/seo/page.tsx deleted file mode 100644 index 4f96841..0000000 --- a/frontend/src/app/terminal/seo/page.tsx +++ /dev/null @@ -1,508 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { useStore } from '@/lib/store' -import { api } from '@/lib/api' -import { TerminalLayout } from '@/components/TerminalLayout' -import { PageContainer, StatCard, Badge } from '@/components/PremiumTable' -import { - Search, - Link2, - Globe, - Shield, - TrendingUp, - Loader2, - AlertCircle, - X, - ExternalLink, - Crown, - CheckCircle, - Sparkles, - BookOpen, - Building, - GraduationCap, - Newspaper, - Lock, - Star, -} from 'lucide-react' -import Link from 'next/link' -import clsx from 'clsx' - -interface SEOData { - domain: string - seo_score: number - value_category: string - metrics: { - domain_authority: number | null - page_authority: number | null - spam_score: number | null - total_backlinks: number | null - referring_domains: number | null - } - notable_links: { - has_wikipedia: boolean - has_gov: boolean - has_edu: boolean - has_news: boolean - notable_domains: string[] - } - top_backlinks: Array<{ - domain: string - authority: number - page: string - }> - estimated_value: number | null - data_source: string - last_updated: string | null - is_estimated: boolean -} - -export default function SEOPage() { - const { subscription } = useStore() - - const [domain, setDomain] = useState('') - const [loading, setLoading] = useState(false) - const [seoData, setSeoData] = useState(null) - const [error, setError] = useState(null) - const [recentSearches, setRecentSearches] = useState([]) - - const tier = subscription?.tier?.toLowerCase() || 'scout' - const isTycoon = tier === 'tycoon' - - useEffect(() => { - // Load recent searches from localStorage - const saved = localStorage.getItem('seo-recent-searches') - if (saved) { - setRecentSearches(JSON.parse(saved)) - } - }, []) - - const saveRecentSearch = (domain: string) => { - const updated = [domain, ...recentSearches.filter(d => d !== domain)].slice(0, 5) - setRecentSearches(updated) - localStorage.setItem('seo-recent-searches', JSON.stringify(updated)) - } - - const cleanDomain = (d: string): string => { - // Remove whitespace, protocol, www, and trailing slashes - return d.trim() - .toLowerCase() - .replace(/\s+/g, '') - .replace(/^https?:\/\//, '') - .replace(/^www\./, '') - .replace(/\/.*$/, '') - } - - const handleSearch = async (e: React.FormEvent) => { - e.preventDefault() - const cleanedDomain = cleanDomain(domain) - if (!cleanedDomain) return - - setLoading(true) - setError(null) - setSeoData(null) - - try { - const data = await api.request(`/seo/${encodeURIComponent(cleanedDomain)}`) - setSeoData(data) - saveRecentSearch(cleanedDomain) - } catch (err: any) { - setError(err.message || 'Failed to analyze domain') - } finally { - setLoading(false) - } - } - - const handleQuickSearch = async (searchDomain: string) => { - const cleanedDomain = cleanDomain(searchDomain) - setDomain(cleanedDomain) - setLoading(true) - setError(null) - setSeoData(null) - - try { - const data = await api.request(`/seo/${encodeURIComponent(cleanedDomain)}`) - setSeoData(data) - } catch (err: any) { - setError(err.message || 'Failed to analyze domain') - } finally { - setLoading(false) - } - } - - const getScoreColor = (score: number) => { - if (score >= 60) return 'text-accent' - if (score >= 40) return 'text-amber-400' - if (score >= 20) return 'text-orange-400' - return 'text-foreground-muted' - } - - const getScoreBg = (score: number) => { - if (score >= 60) return 'bg-accent/10 border-accent/30' - if (score >= 40) return 'bg-amber-500/10 border-amber-500/30' - if (score >= 20) return 'bg-orange-500/10 border-orange-500/30' - return 'bg-foreground/5 border-border' - } - - const formatNumber = (num: number | null) => { - if (num === null) return '-' - if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M` - if (num >= 1000) return `${(num / 1000).toFixed(1)}K` - return num.toString() - } - - // Show upgrade prompt for non-Tycoon users - if (!isTycoon) { - return ( - - -
-
- -
-

Tycoon Feature

-

- SEO Juice Detector is a premium feature for serious domain investors. - Analyze backlinks, domain authority, and find hidden gems that SEO agencies pay - $100-$500 for — even if the name is "ugly". -

- -
-
- -

Backlink Analysis

-

Top referring domains

-
-
- -

Domain Authority

-

Moz DA/PA scores

-
-
- -

Notable Links

-

Wikipedia, .gov, .edu

-
-
- - - - Upgrade to Tycoon - -
-
-
- ) - } - - return ( - - - {/* Error Message */} - {error && ( -
- -

{error}

- -
- )} - - {/* Search Form */} -
-
-
- - setDomain(e.target.value)} - placeholder="Enter domain to analyze (e.g., example.com)" - className="w-full pl-12 pr-4 py-3 bg-background border border-border rounded-xl - text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent" - /> -
- -
- - {/* Recent Searches */} - {recentSearches.length > 0 && !seoData && ( -
- Recent: - {recentSearches.map((d) => ( - - ))} -
- )} -
- - {/* Loading State */} - {loading && ( -
- -

Analyzing backlinks & authority...

-
- )} - - {/* Results */} - {seoData && !loading && ( -
- {/* Header with Score */} -
-
-
-

- {seoData.domain} -

-
- - {seoData.data_source === 'moz' ? 'Moz Data' : 'Estimated'} - - {seoData.value_category} -
-
-
- - {seoData.seo_score} - - SEO Score -
-
- - {/* Estimated Value */} - {seoData.estimated_value && ( -
-

Estimated SEO Value

-

- ${seoData.estimated_value.toLocaleString()} -

-

- Based on domain authority & backlink profile -

-
- )} -
- - {/* Metrics Grid */} -
- - - - - 30 ? '⚠️ High' : '✓ Low'} - /> -
- - {/* Notable Links */} -
-

Notable Backlinks

-
-
- -
-

Wikipedia

-

- {seoData.notable_links.has_wikipedia ? '✓ Found' : 'Not found'} -

-
-
- -
- -
-

.gov Links

-

- {seoData.notable_links.has_gov ? '✓ Found' : 'Not found'} -

-
-
- -
- -
-

.edu Links

-

- {seoData.notable_links.has_edu ? '✓ Found' : 'Not found'} -

-
-
- -
- -
-

News Sites

-

- {seoData.notable_links.has_news ? '✓ Found' : 'Not found'} -

-
-
-
- - {/* Notable Domains List */} - {seoData.notable_links.notable_domains.length > 0 && ( -
-

High-authority referring domains:

-
- {seoData.notable_links.notable_domains.map((d) => ( - - {d} - - ))} -
-
- )} -
- - {/* Top Backlinks */} - {seoData.top_backlinks.length > 0 && ( -
-

Top Backlinks

-
- {seoData.top_backlinks.map((link, idx) => ( -
-
-
= 60 ? "bg-accent/10 text-accent" : - link.authority >= 40 ? "bg-amber-500/10 text-amber-400" : - "bg-foreground/5 text-foreground-muted" - )}> - {link.authority} -
-
-

{link.domain}

- {link.page && ( -

{link.page}

- )} -
-
- - - -
- ))} -
-
- )} - - {/* Data Source Note */} - {seoData.is_estimated && ( -
-

- - This data is estimated based on domain characteristics. - For live Moz data, configure MOZ_ACCESS_ID and MOZ_SECRET_KEY in the backend. -

-
- )} -
- )} - - {/* Empty State */} - {!seoData && !loading && !error && ( -
- -

SEO Juice Detector

-

- Enter a domain above to analyze its backlink profile, domain authority, - and find hidden SEO value that others miss. -

-
- )} -
-
- ) -} - diff --git a/frontend/src/app/terminal/welcome/page.tsx b/frontend/src/app/terminal/welcome/page.tsx index d4f3752..07414af 100644 --- a/frontend/src/app/terminal/welcome/page.tsx +++ b/frontend/src/app/terminal/welcome/page.tsx @@ -35,8 +35,8 @@ const planDetails = { ], nextSteps: [ { href: '/terminal/watchlist', label: 'Add domains to watchlist', icon: Eye }, - { href: '/terminal/alerts', label: 'Set up Sniper Alerts', icon: Bell }, - { href: '/terminal/portfolio', label: 'Track your portfolio', icon: BarChart3 }, + { href: '/terminal/market', label: 'Browse the market', icon: Store }, + { href: '/terminal/intel', label: 'Check TLD pricing', icon: BarChart3 }, ], }, tycoon: { @@ -53,8 +53,8 @@ const planDetails = { ], nextSteps: [ { href: '/terminal/watchlist', label: 'Add domains to watchlist', icon: Eye }, - { href: '/terminal/seo', label: 'Analyze SEO metrics', icon: Sparkles }, - { href: '/terminal/alerts', label: 'Create Sniper Alerts', icon: Bell }, + { href: '/terminal/market', label: 'Browse the market', icon: Store }, + { href: '/terminal/listing', label: 'List your domains', icon: Sparkles }, ], }, } diff --git a/frontend/src/hooks/useKeyboardShortcuts.tsx b/frontend/src/hooks/useKeyboardShortcuts.tsx index 7902f40..5fe5a65 100644 --- a/frontend/src/hooks/useKeyboardShortcuts.tsx +++ b/frontend/src/hooks/useKeyboardShortcuts.tsx @@ -241,8 +241,8 @@ export function useUserShortcuts() { // Navigation { key: 'g', label: 'Go to Dashboard', description: 'Navigate to dashboard', action: () => router.push('/terminal/radar'), category: 'navigation' }, { key: 'w', label: 'Go to Watchlist', description: 'Navigate to watchlist', action: () => router.push('/terminal/watchlist'), category: 'navigation' }, - { key: 'p', label: 'Go to Portfolio', description: 'Navigate to portfolio', action: () => router.push('/terminal/portfolio'), category: 'navigation' }, - { key: 'a', label: 'Go to Auctions', description: 'Navigate to auctions', action: () => router.push('/terminal/auctions'), category: 'navigation' }, + { key: 'p', label: 'Go to Watchlist', description: 'Navigate to watchlist', action: () => router.push('/terminal/watchlist'), category: 'navigation' }, + { key: 'a', label: 'Go to Market', description: 'Navigate to market', action: () => router.push('/terminal/market'), category: 'navigation' }, { key: 't', label: 'Go to TLD Pricing', description: 'Navigate to TLD pricing', action: () => router.push('/terminal/intel'), category: 'navigation' }, { key: 's', label: 'Go to Settings', description: 'Navigate to settings', action: () => router.push('/terminal/settings'), category: 'navigation' }, // Actions