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 }) => (
-
-))
-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 */}
-
-
-
-
- Inflation Monitor & Pricing Analytics across 800+ TLDs.
-
+
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* HEADER */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
+
+
+
+
+ Pricing Analytics
- {/* Quick Stats Pills */}
-
-
-
- {userTier === 'tycoon' ? 'Tycoon Access' : userTier === 'trader' ? 'Trader Access' : 'Scout Access'}
-
-
-
-
-
- {/* 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 ? (
+
+ ) : (
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ Risk |
+ |
+
+
+
+ {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}
+
+ |
+
+ {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() {
- )}
+ )}
- {/* Edit Modal */}
- {showEditModal && selectedDomain && (
+ {/* Edit Modal */}
+ {showEditModal && selectedDomain && (
@@ -753,8 +753,8 @@ export default function PortfolioPage() {
+
+
+
- )}
+ )}
{/* Sell Modal */}
- {showSellModal && selectedDomain && (
+ {showSellModal && selectedDomain && (
@@ -843,25 +843,25 @@ export default function PortfolioPage() {
Congratulations on selling {selectedDomain.domain}!
-
+
- setShowSellModal(false)}
+ 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
-
-
+ Cancel
+
+
+ >
{saving ? : }
{saving ? 'Saving...' : 'Record Sale'}
-
-
-
+
-
+
+
+
)}
{/* List for Sale Modal */}
@@ -917,8 +917,8 @@ export default function PortfolioPage() {
Put {selectedDomain.domain} on the marketplace
-
-
+
+
+ )}
+
@@ -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.
-
-
+
+
{saving ? : }
{saving ? 'Creating...' : 'Create Listing'}
-
-
+
+
-
-
- )}
+
+ )}
+
)
}
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 */}
-
-
-
-
- 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
+
+
+
setShowCreateModal(true)}
disabled={!canAddMore}
- className="px-4 py-2 bg-emerald-500 text-white font-medium rounded-lg hover:bg-emerald-400 transition-all shadow-lg shadow-emerald-500/20 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 whitespace-nowrap"
+ className={clsx(
+ "flex items-center gap-2 px-4 py-2 text-sm font-semibold transition-colors",
+ canAddMore
+ ? "bg-accent text-black hover:bg-white"
+ : "bg-white/10 text-white/40 cursor-not-allowed"
+ )}
>
- New Alert
+
+ New Alert
+
+
- {/* 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.
-
-
setShowCreateModal(true)}
- className="px-4 py-2 bg-emerald-500 text-white font-medium rounded-lg hover:bg-emerald-400 transition-all shadow-lg shadow-emerald-500/20 flex items-center gap-2"
- >
- Create First Alert
-
-
- )}
-
- {/* 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
+
+
setShowCreateModal(true)}
+ className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors"
+ >
+
+ Create First Alert
+
+
+ ) : (
+
+ {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 */}
-
-
handleToggle(alert.id, alert.is_active)}
- disabled={togglingId === alert.id}
- className={clsx(
- "p-2 rounded-lg border transition-all",
- alert.is_active
- ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400 hover:bg-emerald-500/20"
- : "bg-zinc-800/50 border-zinc-700 text-zinc-500 hover:bg-zinc-800"
- )}
- title={alert.is_active ? "Pause" : "Activate"}
- >
- {togglingId === alert.id ? (
-
- ) : alert.is_active ? (
-
- ) : (
-
- )}
-
-
-
setEditingAlert(alert)}
- className="p-2 rounded-lg border bg-zinc-800/50 border-zinc-700 text-zinc-400 hover:text-white hover:bg-zinc-800 transition-all"
- title="Edit"
- >
-
-
-
-
handleDelete(alert.id, alert.name)}
- disabled={deletingId === alert.id}
- className="p-2 rounded-lg 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"
- title="Delete"
- >
- {deletingId === alert.id ? (
-
- ) : (
-
- )}
-
+
+
+
+ {alert.matches_count} matches
+
+
+
+ {alert.notifications_sent} sent
+
+ {alert.last_matched_at && (
+
+
+ {new Date(alert.last_matched_at).toLocaleDateString()}
+
+ )}
+
+
+
handleToggle(alert.id, alert.is_active)}
+ disabled={togglingId === alert.id}
+ className={clsx(
+ "w-8 h-8 flex items-center justify-center border transition-colors",
+ alert.is_active
+ ? "bg-accent/10 border-accent/20 text-accent hover:bg-accent/20"
+ : "bg-white/5 border-white/10 text-white/40 hover:bg-white/10"
+ )}
+ >
+ {togglingId === alert.id ? (
+
+ ) : alert.is_active ? (
+
+ ) : (
+
+ )}
+
+
+
setEditingAlert(alert)}
+ className="w-8 h-8 flex items-center justify-center border bg-white/5 border-white/10 text-white/40 hover:text-white hover:bg-white/10 transition-colors"
+ >
+
+
+
+
handleDelete(alert.id, alert.name)}
+ disabled={deletingId === alert.id}
+ className="w-8 h-8 flex items-center justify-center border bg-white/5 border-white/10 text-white/40 hover:text-rose-400 hover:border-rose-400/30 transition-colors"
+ >
+ {deletingId === alert.id ? (
+
+ ) : (
+
+ )}
+
+
- ))}
+
- )}
+ ))}
-
- {/* 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
-
+