From b7fa3632bf002d9ced20f7e28a733450079818b8 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Fri, 12 Dec 2025 22:45:55 +0100 Subject: [PATCH] Intel, Sniper, Yield pages redesign --- frontend/src/app/terminal/intel/page.tsx | 767 +++++++------------ frontend/src/app/terminal/portfolio/page.tsx | 264 +++---- frontend/src/app/terminal/sniper/page.tsx | 700 +++++++---------- frontend/src/app/terminal/yield/page.tsx | 509 ++++++------ 4 files changed, 959 insertions(+), 1281 deletions(-) diff --git a/frontend/src/app/terminal/intel/page.tsx b/frontend/src/app/terminal/intel/page.tsx index 29ccb40..8e4e6a9 100755 --- a/frontend/src/app/terminal/intel/page.tsx +++ b/frontend/src/app/terminal/intel/page.tsx @@ -1,11 +1,10 @@ 'use client' -import { useEffect, useState, useMemo, useCallback, memo } from 'react' +import { useEffect, useState, useMemo, useCallback } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' -import { TerminalLayout } from '@/components/TerminalLayout' +import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { - ExternalLink, Loader2, TrendingUp, TrendingDown, @@ -16,28 +15,39 @@ import { Search, ChevronDown, ChevronUp, - Info, ArrowRight, Lock, Sparkles, BarChart3, - Activity, Zap, - Filter, - Check, - Eye, - ShieldCheck, - Diamond, Minus } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' // ============================================================================ -// TIER ACCESS LEVELS +// TYPES // ============================================================================ type UserTier = 'scout' | 'trader' | 'tycoon' +type SortField = 'tld' | 'price' | 'renewal' | 'change' | 'change3y' | 'risk' | 'popularity' +type SortDirection = 'asc' | 'desc' + +interface TLDData { + tld: string + min_price: number + avg_price: number + max_price: number + min_renewal_price: number + avg_renewal_price: number + price_change_7d: number + price_change_1y: number + price_change_3y: number + risk_level: 'low' | 'medium' | 'high' + risk_reason: string + popularity_rank?: number + type?: string +} function getTierLevel(tier: UserTier): number { switch (tier) { @@ -48,175 +58,7 @@ function getTierLevel(tier: UserTier): number { } } -// ============================================================================ -// SHARED COMPONENTS -// ============================================================================ - -const Tooltip = memo(({ children, content }: { children: React.ReactNode; content: string }) => ( -
- {children} -
- {content} -
-
-
-)) -Tooltip.displayName = 'Tooltip' - -const LockedFeature = memo(({ requiredTier, currentTier }: { requiredTier: UserTier; currentTier: UserTier }) => { - const tierNames = { scout: 'Scout', trader: 'Trader', tycoon: 'Tycoon' } - return ( - -
- - Locked -
-
- ) -}) -LockedFeature.displayName = 'LockedFeature' - -const StatCard = memo(({ - label, - value, - subValue, - icon: Icon, - highlight, - locked = false, - lockTooltip -}: { - label: string - value: string | number - subValue?: string - icon: any - highlight?: boolean - locked?: boolean - lockTooltip?: string -}) => ( -
-
- -
-
-
- - {label} -
- - {locked ? ( - -
- - -
-
- ) : ( -
- {value} - {subValue && {subValue}} -
- )} - - {highlight && ( -
- ● LIVE -
- )} -
-
-)) -StatCard.displayName = 'StatCard' - -const FilterToggle = memo(({ active, onClick, label, icon: Icon }: { - active: boolean - onClick: () => void - label: string - icon?: any -}) => ( - -)) -FilterToggle.displayName = 'FilterToggle' - -type SortField = 'tld' | 'price' | 'renewal' | 'change' | 'change3y' | 'risk' | 'popularity' -type SortDirection = 'asc' | 'desc' - -const SortableHeader = memo(({ - label, field, currentSort, currentDirection, onSort, align = 'left', tooltip, locked = false, lockTooltip -}: { - label: string; field: SortField; currentSort: SortField; currentDirection: SortDirection; onSort: (field: SortField) => void; align?: 'left'|'center'|'right'; tooltip?: string; locked?: boolean; lockTooltip?: string -}) => { - const isActive = currentSort === field - return ( -
- - {tooltip && !locked && ( - - - - )} -
- ) -}) -SortableHeader.displayName = 'SortableHeader' - -// ============================================================================ -// TYPES -// ============================================================================ - -interface TLDData { - tld: string - min_price: number - avg_price: number - max_price: number - min_renewal_price: number - avg_renewal_price: number - cheapest_registrar?: string - cheapest_registrar_url?: string - price_change_7d: number - price_change_1y: number - price_change_3y: number - risk_level: 'low' | 'medium' | 'high' - risk_reason: string - popularity_rank?: number - type?: string -} +const formatPrice = (p: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p) // ============================================================================ // MAIN PAGE @@ -225,30 +67,23 @@ interface TLDData { export default function IntelPage() { const { subscription } = useStore() - // Determine user tier const userTier: UserTier = (subscription?.tier as UserTier) || 'scout' const tierLevel = getTierLevel(userTier) - // Feature access checks - const canSeeRenewal = tierLevel >= 2 // Trader+ - const canSee3yTrend = tierLevel >= 3 // Tycoon only - const canSeeFullHistory = tierLevel >= 3 // Tycoon only + const canSeeRenewal = tierLevel >= 2 + const canSee3yTrend = tierLevel >= 3 - // Data const [tldData, setTldData] = useState([]) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [total, setTotal] = useState(0) - // Filters const [searchQuery, setSearchQuery] = useState('') const [filterType, setFilterType] = useState<'all' | 'tech' | 'geo' | 'budget'>('all') - // Sort const [sortField, setSortField] = useState('popularity') const [sortDirection, setSortDirection] = useState('asc') - // Load Data const loadData = useCallback(async () => { setLoading(true) try { @@ -296,18 +131,12 @@ export default function IntelPage() { } }, [sortField, canSeeRenewal, canSee3yTrend]) - // Transform & Filter const filteredData = useMemo(() => { let data = tldData - // Tech filter: common tech-related TLDs const techTlds = ['ai', 'io', 'app', 'dev', 'tech', 'cloud', 'digital', 'software', 'code', 'systems', 'network', 'data', 'cyber', 'online', 'web', 'api', 'hosting'] if (filterType === 'tech') data = data.filter(t => techTlds.includes(t.tld) || t.tld === 'io' || t.tld === 'ai') - - // Geo filter: all country-code TLDs (ccTLD type from backend) if (filterType === 'geo') data = data.filter(t => t.type === 'ccTLD') - - // Budget filter: registration under $10 if (filterType === 'budget') data = data.filter(t => t.min_price < 10) if (searchQuery) { @@ -341,295 +170,277 @@ export default function IntelPage() { return { lowest, hottest, traps, avgRenewal } }, [tldData]) - const formatPrice = (p: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p) - return ( - -
- - {/* Ambient Background Glow */} -
-
-
-
- -
- - {/* Header Section */} -
-
-
-
-

TLD Intelligence

-
-

- Inflation Monitor & Pricing Analytics across 800+ TLDs. -

+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* HEADER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+
+ + Pricing Analytics
- {/* Quick Stats Pills */} -
-
- - {userTier === 'tycoon' ? 'Tycoon Access' : userTier === 'trader' ? 'Trader Access' : 'Scout Access'} -
-
- - {total} Tracked -
-
-
- - {/* Metric Grid */} -
- - - - -
- - {/* Control Bar */} -
- {/* Filter Pills */} -
- setFilterType('all')} label="All TLDs" /> - setFilterType('tech')} label="Tech" icon={Zap} /> - setFilterType('geo')} label="Geo / National" icon={Globe} /> - setFilterType('budget')} label="Budget <$10" icon={DollarSign} /> -
- - {/* Refresh Button (Mobile) */} - - - {/* Search Filter */} -
- - setSearchQuery(e.target.value)} - placeholder="Search TLDs..." - 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" - /> -
-
- - {/* DATA GRID */} -
- - {/* Unified Table Header - Use a wrapper with min-width to force scrolling instead of breaking */} -
-
{/* Force minimum width */} -
-
- -
-
- -
-
- -
-
- -
-
- {canSee3yTrend ? ( - - ) : ( - Trend (3y) - )} -
-
- -
-
Action
-
- - {/* Rows */} - {loading ? ( -
- -

Analyzing registry data...

-
- ) : filteredData.length === 0 ? ( -
-
- -
-

No TLDs found

-

- Try adjusting your filters or search query. -

-
- ) : ( -
- {filteredData.map((tld) => { - const isTrap = tld.min_renewal_price > tld.min_price * 1.5 - const trend = tld.price_change_1y || 0 - const trend3y = tld.price_change_3y || 0 - - return ( -
- {/* TLD */} -
- - .{tld.tld} - -
- - {/* Price */} -
- {formatPrice(tld.min_price)} -
- - {/* Renewal (Trader+) */} -
- {canSeeRenewal ? ( - <> - - {formatPrice(tld.min_renewal_price)} - - {isTrap && ( - - - - )} - - ) : ( - - )} -
- - {/* Trend 1y */} -
-
5 ? "text-orange-400 bg-orange-400/5 border border-orange-400/20" : - trend < -5 ? "text-emerald-400 bg-emerald-400/5 border border-emerald-400/20" : - "text-zinc-400 bg-zinc-800/50 border border-zinc-700" - )}> - {trend > 0 ? : trend < 0 ? : } - {Math.abs(trend)}% -
-
- - {/* Trend 3y */} -
- {canSee3yTrend ? ( -
10 ? "text-orange-400 bg-orange-400/5 border border-orange-400/20" : - trend3y < -10 ? "text-emerald-400 bg-emerald-400/5 border border-emerald-400/20" : - "text-zinc-400 bg-zinc-800/50 border border-zinc-700" - )}> - {trend3y > 0 ? : trend3y < 0 ? : } - {Math.abs(trend3y)}% -
- ) : ( - - )} -
- - {/* Risk */} -
- -
-
-
- -
- - {/* Action */} -
- - - -
-
- ) - })} -
- )} -
-
+

+ TLD Intel + {total} +

- {/* Upgrade CTA for Scout users */} - {userTier === 'scout' && ( -
-
- -
-

Unlock Full TLD Intelligence

-

- See renewal prices, identify renewal traps, and access detailed price history charts with Trader or Tycoon. -

- - - Upgrade Now - +
+
+
{formatPrice(stats.lowest)}
+
Lowest Entry
- )} +
+
{stats.traps}
+
High Risk
+
+ {canSeeRenewal && ( +
+
{formatPrice(stats.avgRenewal)}
+
Avg Renewal
+
+ )} +
-
- +
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* FILTERS */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+ + + + + +
+ +
+ + setSearchQuery(e.target.value)} + placeholder="Search TLDs..." + className="bg-[#050505] border border-white/10 pl-9 pr-4 py-2 text-sm text-white placeholder:text-white/25 outline-none focus:border-accent/40 w-48 lg:w-64" + /> +
+ + +
+
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* TABLE */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+ {loading ? ( +
+ +
+ ) : filteredData.length === 0 ? ( +
+
+ +
+

No TLDs found

+
+ ) : ( +
+ + + + + + + + + + + + + + {filteredData.map((tld) => { + const isTrap = tld.min_renewal_price > tld.min_price * 1.5 + const trend = tld.price_change_1y || 0 + const trend3y = tld.price_change_3y || 0 + + return ( + + + + + + + + + + ) + })} + +
+ + + + + + + + + + Risk
+ + .{tld.tld} + + + {formatPrice(tld.min_price)} + + {canSeeRenewal ? ( +
+ + {formatPrice(tld.min_renewal_price)} + + {isTrap && } +
+ ) : ( + + + Trader+ + + )} +
+ 5 ? "text-orange-400 bg-orange-400/10" : + trend < -5 ? "text-accent bg-accent/10" : + "text-white/40 bg-white/5" + )}> + {trend > 0 ? : trend < 0 ? : } + {Math.abs(trend)}% + + + {canSee3yTrend ? ( + 10 ? "text-orange-400 bg-orange-400/10" : + trend3y < -10 ? "text-accent bg-accent/10" : + "text-white/40 bg-white/5" + )}> + {trend3y > 0 ? : trend3y < 0 ? : } + {Math.abs(trend3y)}% + + ) : ( + + )} + +
+
+
+
+ + + +
+
+ )} +
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* UPGRADE CTA */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {userTier === 'scout' && ( +
+
+ +

Unlock Full Intel

+

+ See renewal prices, identify traps, and access 3-year price history with Trader or Tycoon. +

+ + Upgrade Now + +
+
+ )} +
) } diff --git a/frontend/src/app/terminal/portfolio/page.tsx b/frontend/src/app/terminal/portfolio/page.tsx index 39b6dbb..c649799 100755 --- a/frontend/src/app/terminal/portfolio/page.tsx +++ b/frontend/src/app/terminal/portfolio/page.tsx @@ -164,7 +164,7 @@ export default function PortfolioPage() { notes: '', tags: '', }) - + const [sellData, setSellData] = useState({ sale_date: new Date().toISOString().split('T')[0], sale_price: '', @@ -415,7 +415,7 @@ export default function PortfolioPage() {

{error}

-
+
)} {success && ( @@ -435,16 +435,16 @@ export default function PortfolioPage() {

Unlock Portfolio Management

Track your domain investments, monitor valuations, and calculate ROI. Know exactly how your portfolio is performing. -

- + + > Upgrade to Trader - +
-
- )} +
+ )} {/* Stats Grid */} {canUsePortfolio && summary && ( @@ -494,7 +494,7 @@ export default function PortfolioPage() {
ROI
Status
Actions
-
+
{loading ? (
@@ -547,8 +547,8 @@ export default function PortfolioPage() {
{formatCurrency(domain.purchase_price)}
{domain.purchase_date && (
{formatDate(domain.purchase_date)}
- )} -
+ )} +
{/* Value */}
@@ -592,30 +592,30 @@ export default function PortfolioPage() {
{!domain.is_sold && ( <> - - - + > + + )} - + > + +
))} - )} - + )} + )} - {/* Add Modal */} - {showAddModal && ( + {/* Add Modal */} + {showAddModal && (
@@ -650,55 +650,55 @@ export default function PortfolioPage() {
-
+
- setFormData({ ...formData, domain: e.target.value })} placeholder="example.com" className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-blue-500/50 transition-all font-mono" - /> -
+ /> +
-
-
+
+
- setFormData({ ...formData, purchase_date: e.target.value })} className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all" - /> -
-
+ /> +
+
- setFormData({ ...formData, purchase_price: e.target.value })} placeholder="0.00" 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-blue-500/50 transition-all font-mono" - /> -
-
+ /> +
+
-
+
- setFormData({ ...formData, registrar: e.target.value })} placeholder="Namecheap, GoDaddy..." 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-blue-500/50 transition-all" - /> -
+ /> +
- - + -
- + +
+
- )} + )} - {/* Edit Modal */} - {showEditModal && selectedDomain && ( + {/* Edit Modal */} + {showEditModal && selectedDomain && (
@@ -753,8 +753,8 @@ export default function PortfolioPage() {
-
-
+
+
Purchase Price
- setFormData({ ...formData, purchase_price: e.target.value })} className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all font-mono" - /> -
+ /> +
-
+
- setFormData({ ...formData, registrar: e.target.value })} className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all" - /> -
+ /> +
setFormData({ ...formData, renewal_date: e.target.value })} className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all" /> -
+
@@ -810,29 +810,29 @@ export default function PortfolioPage() {
- - + -
-
+ +
+
- )} + )} {/* Sell Modal */} - {showSellModal && selectedDomain && ( + {showSellModal && selectedDomain && (
@@ -843,25 +843,25 @@ export default function PortfolioPage() {

Congratulations on selling {selectedDomain.domain}!

-
+
-
+
- setSellData({ ...sellData, sale_date: e.target.value })} className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-emerald-500/50 transition-all" - /> -
+ /> +
-
+
- setSellData({ ...sellData, sale_price: e.target.value })} placeholder="0.00" 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 text-lg" - /> -
+ /> +
{selectedDomain.purchase_price && sellData.sale_price && (
selectedDomain.purchase_price ? '📈' : '📉'} {' '}ROI: {(((parseFloat(sellData.sale_price) - selectedDomain.purchase_price) / selectedDomain.purchase_price) * 100).toFixed(1)}% {' '}(${(parseFloat(sellData.sale_price) - selectedDomain.purchase_price).toLocaleString()} profit) -
+
)}
- - + -
- + - + + + )} {/* List for Sale Modal */} @@ -917,8 +917,8 @@ export default function PortfolioPage() {

Put {selectedDomain.domain} on the marketplace

- - + +
@@ -931,14 +931,14 @@ export default function PortfolioPage() { onChange={(e) => setListData({ ...listData, asking_price: e.target.value })} placeholder="Leave empty for '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-amber-500/50 transition-all font-mono text-lg" - /> -
+ /> + {selectedDomain.estimated_value && (

Estimated value: {formatCurrency(selectedDomain.estimated_value)} -

- )} - +

+ )} +
@@ -951,13 +951,13 @@ export default function PortfolioPage() { -
- + +

💡 After creating the listing, you'll need to verify domain ownership via DNS before it goes live on the marketplace. -

-
+

+
-
+ +
- - - )} + + )} + ) } diff --git a/frontend/src/app/terminal/sniper/page.tsx b/frontend/src/app/terminal/sniper/page.tsx index f00e227..e75c725 100644 --- a/frontend/src/app/terminal/sniper/page.tsx +++ b/frontend/src/app/terminal/sniper/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, useCallback } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' -import { TerminalLayout } from '@/components/TerminalLayout' +import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { Plus, Target, @@ -12,62 +12,21 @@ import { Trash2, Power, PowerOff, - Eye, Bell, MessageSquare, Loader2, X, AlertCircle, CheckCircle, - TrendingUp, - Filter, Clock, DollarSign, Hash, - Tag, Crown, Activity } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' -// ============================================================================ -// SHARED COMPONENTS -// ============================================================================ - -const StatCard = ({ 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 -
- )} -
-
-) - // ============================================================================ // INTERFACES // ============================================================================ @@ -111,19 +70,16 @@ export default function SniperAlertsPage() { const [deletingId, setDeletingId] = useState(null) const [togglingId, setTogglingId] = useState(null) - // Tier-based limits const tier = subscription?.tier || 'scout' const alertLimits: Record = { scout: 2, trader: 10, tycoon: 50 } const maxAlerts = alertLimits[tier] || 2 const canAddMore = alerts.length < maxAlerts const isTycoon = tier === 'tycoon' - // Stats const activeAlerts = alerts.filter(a => a.is_active).length const totalMatches = alerts.reduce((sum, a) => sum + a.matches_count, 0) const totalNotifications = alerts.reduce((sum, a) => sum + a.notifications_sent, 0) - // Load alerts const loadAlerts = useCallback(async () => { setLoading(true) try { @@ -140,7 +96,6 @@ export default function SniperAlertsPage() { loadAlerts() }, [loadAlerts]) - // Toggle alert active status const handleToggle = async (id: number, currentStatus: boolean) => { setTogglingId(id) try { @@ -156,7 +111,6 @@ export default function SniperAlertsPage() { } } - // Delete alert const handleDelete = async (id: number, name: string) => { if (!confirm(`Delete alert "${name}"?`)) return @@ -172,293 +126,276 @@ export default function SniperAlertsPage() { } return ( - -
- {/* Ambient Background Glow */} -
-
-
-
- - {/* Content */} -
- {/* Header */} -
-
-
-
-

Sniper Alerts

-
-

- Get notified when domains matching your exact criteria hit the market. Set it, forget it, and pounce when the time is right. -

+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* HEADER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+
+ + Automated Alerts
- + +

+ Sniper + {alerts.length}/{maxAlerts} +

+ +

+ Get notified when domains matching your criteria hit the market +

+
+ +
+
+
+
{activeAlerts}
+
Active
+
+
+
{totalMatches}
+
Matches
+
+
+
{totalNotifications}
+
Sent
+
+
+
+
+
- {/* Stats */} -
- 0} - /> - 0 ? 'up' : 'neutral'} - /> - 0 ? 'up' : 'neutral'} - /> - + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* ALERTS LIST */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+ {loading ? ( +
+
- - {/* Alerts List */} -
- {/* Header */} -
-

- - Your Alerts -

- {alerts.length} / {maxAlerts} + ) : alerts.length === 0 ? ( +
+
+
- - {/* Loading */} - {loading && ( -
- -
- )} - - {/* Empty State */} - {!loading && alerts.length === 0 && ( -
-
- -
-

No Alerts Yet

-

- Create your first sniper alert to get notified when domains matching your criteria appear in auctions. -

- -
- )} - - {/* Alerts Grid */} - {!loading && alerts.length > 0 && ( -
- {alerts.map((alert) => ( -
-
- {/* Alert Info */} -
-
-

{alert.name}

- {alert.is_active ? ( - -
- ACTIVE - - ) : ( - - PAUSED - - )} - {isTycoon && alert.notify_sms && ( - - - SMS - - )} -
- - {alert.description && ( -

{alert.description}

+

No alerts yet

+

+ Create your first sniper alert to get notified when matching domains appear +

+ +
+ ) : ( +
+ {alerts.map((alert) => ( +
+
+
+
+
+

{alert.name}

+ {alert.is_active ? ( + +
+ Active + + ) : ( + + Paused + )} - - {/* Criteria Pills */} -
- {alert.tlds && ( - - {alert.tlds} - - )} - {alert.keywords && ( - - +{alert.keywords} - - )} - {alert.exclude_keywords && ( - - -{alert.exclude_keywords} - - )} - {(alert.min_length || alert.max_length) && ( - - - {alert.min_length || 1}-{alert.max_length || 63} chars - - )} - {(alert.min_price || alert.max_price) && ( - - - {alert.min_price ? `$${alert.min_price}+` : ''}{alert.max_price ? ` - $${alert.max_price}` : ''} - - )} - {alert.no_numbers && ( - - No numbers - - )} - {alert.no_hyphens && ( - - No hyphens - - )} -
- - {/* Stats */} -
- - - {alert.matches_count} matches + {isTycoon && alert.notify_sms && ( + + + SMS - - - {alert.notifications_sent} sent + )} +
+ + {alert.description && ( +

{alert.description}

+ )} + +
+ {alert.tlds && ( + + {alert.tlds} - {alert.last_matched_at && ( - - - Last: {new Date(alert.last_matched_at).toLocaleDateString()} - - )} -
+ )} + {alert.keywords && ( + + +{alert.keywords} + + )} + {alert.exclude_keywords && ( + + -{alert.exclude_keywords} + + )} + {(alert.min_length || alert.max_length) && ( + + + {alert.min_length || 1}-{alert.max_length || 63} + + )} + {(alert.min_price || alert.max_price) && ( + + + {alert.min_price ? `$${alert.min_price}` : ''}{alert.max_price ? ` - $${alert.max_price}` : '+'} + + )} + {alert.no_numbers && ( + + No digits + + )} + {alert.no_hyphens && ( + + No hyphens + + )}
- {/* Actions */} -
- - - - - +
+ + + {alert.matches_count} matches + + + + {alert.notifications_sent} sent + + {alert.last_matched_at && ( + + + {new Date(alert.last_matched_at).toLocaleDateString()} + + )}
+ +
+ + + + + +
- ))} +
- )} + ))}
- - {/* Upgrade CTA */} - {!canAddMore && ( -
- -

Alert Limit Reached

-

- You've created {maxAlerts} alerts. Upgrade to add more. -

-
-
- Trader: 10 alerts -
-
- Tycoon: 50 alerts + SMS -
-
- - Upgrade Now - -
- )} -
- - {/* Create/Edit Modal */} - {(showCreateModal || editingAlert) && ( - { - setShowCreateModal(false) - setEditingAlert(null) - }} - onSuccess={() => { - loadAlerts() - setShowCreateModal(false) - setEditingAlert(null) - }} - isTycoon={isTycoon} - /> )} -
- +
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* UPGRADE CTA */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {!canAddMore && ( +
+
+ +

Alert Limit Reached

+

+ You've created {maxAlerts} alerts. Upgrade for more. +

+
+
+ Trader: 10 alerts +
+
+ Tycoon: 50 + SMS +
+
+ + Upgrade Now + +
+
+ )} + + {/* Create/Edit Modal */} + {(showCreateModal || editingAlert) && ( + { + setShowCreateModal(false) + setEditingAlert(null) + }} + onSuccess={() => { + loadAlerts() + setShowCreateModal(false) + setEditingAlert(null) + }} + isTycoon={isTycoon} + /> + )} + ) } @@ -548,28 +485,26 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: { onClick={onClose} >
e.stopPropagation()} > {/* Header */} -
+
-
- -
+
-

{isEditing ? 'Edit Alert' : 'Create Sniper Alert'}

-

Set precise criteria for domain matching

+

{isEditing ? 'Edit Alert' : 'Create Sniper Alert'}

+

Set criteria for domain matching

-
{error && ( -
+

{error}

@@ -577,85 +512,85 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: { {/* Basic Info */}
-

Basic Info

+

Basic Info

- + setForm({ ...form, name: e.target.value })} placeholder="e.g. Premium 4L .com domains" required - 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" + className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 transition-all" />
- +