Trends & Forge v2: clearer AI integration, unified layout, auto-expand keywords
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

This commit is contained in:
2025-12-17 16:05:40 +01:00
parent 0618d8517d
commit 129716ad1d
5 changed files with 570 additions and 569 deletions

View File

@ -101,11 +101,50 @@ function getStatusStyle(status: string) {
}
}
const SECTION_CONFIG: Record<string, { icon: any; color: string; label: string }> = {
authority: { icon: Shield, color: 'blue', label: 'Authority' },
market: { icon: TrendingUp, color: 'emerald', label: 'Market' },
risk: { icon: AlertTriangle, color: 'amber', label: 'Risk' },
value: { icon: DollarSign, color: 'violet', label: 'Value' },
const SECTION_CONFIG: Record<string, { icon: any; color: string; label: string; description: string }> = {
authority: {
icon: Shield,
color: 'blue',
label: 'Authority',
description: 'Domain trust signals: age, backlinks, brand memorability'
},
market: {
icon: TrendingUp,
color: 'emerald',
label: 'Market',
description: 'Search demand, competition, and commercial value indicators'
},
risk: {
icon: AlertTriangle,
color: 'amber',
label: 'Risk',
description: 'Legal, reputation and technical risks to consider'
},
value: {
icon: DollarSign,
color: 'violet',
label: 'Value',
description: 'Estimated worth and comparable sales data'
},
}
// Tooltips for each analysis item
const ITEM_TOOLTIPS: Record<string, string> = {
availability: 'Is this domain currently available for registration?',
radio_test: 'Can this domain be easily spelled when heard? Good brandables score high.',
age: 'Older domains often have more trust with search engines.',
backlinks: 'Number of websites linking to this domain. More = higher authority.',
trust_flow: 'Quality score of the backlink profile (0-100). Higher is better.',
search_volume: 'Monthly Google searches for this keyword. Higher = more traffic potential.',
cpc: 'Cost-per-click for ads on this keyword. Higher CPC = more commercial intent.',
competition: 'How competitive the keyword is for SEO and ads.',
tld_matrix: 'Availability of this name across different domain extensions.',
blacklist: 'Is this domain flagged for spam, malware, or phishing?',
trademark: 'Does this domain potentially infringe on known trademarks?',
archive: 'Historical data from the Wayback Machine - what was hosted here before?',
valuation: 'Estimated market value based on comparable sales and metrics.',
tld_cheapest_register_usd: 'Lowest registration price available from major registrars.',
tld_cheapest_renew_usd: 'Annual renewal cost - factor this into your ROI calculations.',
}
async function copyToClipboard(text: string) {
@ -302,7 +341,10 @@ export function AnalyzePanel() {
<div className="px-5 pb-4">
<div className="flex gap-3">
{/* Pounce Score */}
<div className={clsx("flex-1 p-4 border", getScoreBg(pounceScore))}>
<div
className={clsx("flex-1 p-4 border cursor-help", getScoreBg(pounceScore))}
title="Pounce Score: Combined rating based on authority, market potential, and risk factors. 80+ is excellent, 60+ is good, below 40 needs caution."
>
<div className="flex items-center justify-between mb-2">
<span className="text-[9px] font-mono text-white/40 uppercase tracking-wider">Pounce Score</span>
<Sparkles className={clsx("w-4 h-4", getScoreColor(pounceScore))} />
@ -311,12 +353,21 @@ export function AnalyzePanel() {
{pounceScore}
</div>
<div className="text-[10px] font-mono text-white/30 mt-1">
{pounceScore >= 80 ? 'Excellent' : pounceScore >= 60 ? 'Good' : pounceScore >= 40 ? 'Fair' : 'Poor'}
{pounceScore >= 80 ? 'Excellent investment' : pounceScore >= 60 ? 'Good opportunity' : pounceScore >= 40 ? 'Proceed with caution' : 'High risk'}
</div>
</div>
{/* Recommendation Badge */}
<div className={clsx("w-32 p-4 border flex flex-col items-center justify-center", recommendation.color)}>
<div
className={clsx("w-32 p-4 border flex flex-col items-center justify-center cursor-help", recommendation.color)}
title={
recommendation.label === 'BUY' ? 'Strong buy signal - this domain has excellent metrics' :
recommendation.label === 'CONSIDER' ? 'Worth considering - do additional research' :
recommendation.label === 'RISKY' ? 'Trademark risk detected - legal issues possible' :
recommendation.label === 'TAKEN' ? 'Domain is not available for registration' :
'Not recommended for purchase at this time'
}
>
<recommendation.icon className="w-6 h-6 mb-2" />
<span className="text-sm font-bold uppercase tracking-wider">{recommendation.label}</span>
</div>
@ -384,6 +435,7 @@ export function AnalyzePanel() {
<button
key={section.key}
onClick={() => setActiveSection(section.key)}
title={config.description}
className={clsx(
"flex items-center gap-1.5 px-3 py-2 border transition-all shrink-0",
isActive ? colors.active : colors.inactive + ' hover:bg-white/[0.02]'
@ -424,23 +476,44 @@ export function AnalyzePanel() {
) : (
<div className="p-5">
{/* Active Section Items */}
{data.sections.filter(s => s.key === activeSection).map((section) => (
<div key={section.key} className="space-y-2">
{data.sections.filter(s => s.key === activeSection).map((section) => {
const sectionConfig = SECTION_CONFIG[section.key] || SECTION_CONFIG.authority
return (
<div key={section.key} className="space-y-3">
{/* Section Description */}
<div className="pb-3 mb-3 border-b border-white/[0.06]">
<div className="flex items-center gap-2 mb-1">
<sectionConfig.icon className="w-4 h-4 text-white/40" />
<span className="text-sm font-bold text-white">{sectionConfig.label}</span>
</div>
<p className="text-xs text-white/40 font-mono">{sectionConfig.description}</p>
</div>
{section.items.map((item) => {
const statusStyle = getStatusStyle(item.status)
const StatusIcon = statusStyle.icon
const tooltip = ITEM_TOOLTIPS[item.key] || ''
// Special handling for TLD Matrix
if (item.key === 'tld_matrix' && Array.isArray(item.value)) {
return (
<div key={item.key} className="p-4 bg-white/[0.02] border border-white/[0.06]">
<div className="text-xs font-mono text-white/40 uppercase tracking-wider mb-3">{item.label}</div>
<div key={item.key} className="p-4 bg-white/[0.02] border border-white/[0.06]" title={tooltip}>
<div className="flex items-center justify-between mb-3">
<div className="text-xs font-mono text-white/40 uppercase tracking-wider">{item.label}</div>
<div className="group relative">
<Info className="w-3.5 h-3.5 text-white/20 hover:text-white/40 cursor-help" />
<div className="absolute right-0 top-6 w-48 p-2 bg-black border border-white/20 text-[10px] text-white/60 hidden group-hover:block z-10">
{tooltip}
</div>
</div>
</div>
<div className="grid grid-cols-4 gap-1.5">
{(item.value as any[]).slice(0, 12).map((row: any) => (
<div
key={String(row.domain)}
title={row.status === 'available' ? `${row.domain} is available!` : `${row.domain} is taken`}
className={clsx(
"px-2 py-1.5 text-[10px] font-mono flex items-center justify-between border",
"px-2 py-1.5 text-[10px] font-mono flex items-center justify-between border cursor-default",
row.status === 'available'
? "border-accent/30 bg-accent/5 text-accent"
: "border-white/[0.06] bg-white/[0.01] text-white/30"
@ -459,24 +532,37 @@ export function AnalyzePanel() {
<div
key={item.key}
className={clsx(
"p-4 border transition-colors",
"p-4 border transition-colors group",
statusStyle.bg, statusStyle.border
)}
>
<div className="flex items-start gap-3">
{/* Status Icon */}
<div className={clsx(
"w-8 h-8 flex items-center justify-center shrink-0",
statusStyle.bg, "border", statusStyle.border
)}>
<div
className={clsx(
"w-8 h-8 flex items-center justify-center shrink-0",
statusStyle.bg, "border", statusStyle.border
)}
title={item.status === 'pass' ? 'Good' : item.status === 'warn' ? 'Warning' : item.status === 'fail' ? 'Issue' : 'Info'}
>
{StatusIcon && <StatusIcon className={clsx("w-4 h-4", statusStyle.text)} />}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2 mb-1">
<span className="text-sm font-medium text-white">{item.label}</span>
<span className="text-[9px] font-mono text-white/30 uppercase">{item.source}</span>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-white">{item.label}</span>
{tooltip && (
<div className="relative">
<Info className="w-3 h-3 text-white/20 hover:text-white/40 cursor-help" />
<div className="absolute left-0 top-5 w-56 p-2 bg-black border border-white/20 text-[10px] text-white/60 hidden group-hover:block z-10 font-mono">
{tooltip}
</div>
</div>
)}
</div>
<span className="text-[9px] font-mono text-white/30 uppercase" title="Data source">{item.source}</span>
</div>
<div className={clsx(
@ -494,22 +580,22 @@ export function AnalyzePanel() {
return (
<div className="mt-2 flex flex-wrap gap-2">
{d.syllables !== undefined && (
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50">
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50" title="Number of syllables - fewer is better for memorability">
{d.syllables} syllables
</span>
)}
{d.length !== undefined && (
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50">
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50" title="Character count - shorter domains are more valuable">
{d.length} chars
</span>
)}
{d.has_hyphen && (
<span className="px-2 py-0.5 bg-amber-500/10 border border-amber-500/20 text-[10px] font-mono text-amber-400">
<span className="px-2 py-0.5 bg-amber-500/10 border border-amber-500/20 text-[10px] font-mono text-amber-400" title="Hyphens reduce brandability and resale value">
has hyphen
</span>
)}
{d.has_digits && (
<span className="px-2 py-0.5 bg-amber-500/10 border border-amber-500/20 text-[10px] font-mono text-amber-400">
<span className="px-2 py-0.5 bg-amber-500/10 border border-amber-500/20 text-[10px] font-mono text-amber-400" title="Numbers can reduce memorability">
has digits
</span>
)}
@ -521,7 +607,7 @@ export function AnalyzePanel() {
{(item.key === 'tld_cheapest_register_usd' || item.key === 'tld_cheapest_renew_usd') && item.details && (() => {
const d = item.details as Record<string, any>
return d.registrar ? (
<div className="mt-1 text-[10px] font-mono text-white/30">
<div className="mt-1 text-[10px] font-mono text-white/30" title="Cheapest registrar offering this price">
via {d.registrar}
</div>
) : null
@ -532,7 +618,7 @@ export function AnalyzePanel() {
)
})}
</div>
))}
)})
{/* Quick Actions */}
<div className="mt-6 pt-4 border-t border-white/[0.06]">

View File

@ -438,23 +438,23 @@ export function AuctionsTab({ showToast }: AuctionsTabProps) {
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{/* Desktop Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_80px_100px_80px_120px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<button onClick={() => handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
<button onClick={() => handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left" title="Domain name being auctioned">
Domain
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('score')} className="flex items-center gap-1 justify-center hover:text-white/60">
<button onClick={() => handleSort('score')} className="flex items-center gap-1 justify-center hover:text-white/60" title="Pounce Score - our AI-powered quality rating (0-100)">
Score
{sortField === 'score' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('price')} className="flex items-center gap-1 justify-end hover:text-white/60">
<button onClick={() => handleSort('price')} className="flex items-center gap-1 justify-end hover:text-white/60" title="Current bid or buy-now price">
Price
{sortField === 'price' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60">
<button onClick={() => handleSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60" title="Time remaining until auction ends">
Time
{sortField === 'time' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div className="text-right">Actions</div>
<div className="text-right" title="Available actions: Analyze, Track, Buy">Actions</div>
</div>
{filteredItems.map((item) => {

View File

@ -13,9 +13,8 @@ import {
ShoppingCart,
Sparkles,
Lock,
Star,
Brain,
RefreshCw,
Plus,
} from 'lucide-react'
import Link from 'next/link'
import { api } from '@/lib/api'
@ -27,9 +26,9 @@ import { useStore } from '@/lib/store'
// ============================================================================
const PATTERNS = [
{ key: 'cvcvc', label: 'CVCVC', examples: ['Zalor', 'Mivex', 'Ronix'], desc: 'Classic 5-letter' },
{ key: 'cvccv', label: 'CVCCV', examples: ['Bento', 'Salvo'], desc: 'Punchy sound' },
{ key: 'human', label: 'Human', examples: ['Siri', 'Alexa', 'Levi'], desc: 'AI agent names' },
{ key: 'cvcvc', label: 'CVCVC', example: 'Zalor' },
{ key: 'cvccv', label: 'CVCCV', example: 'Bento' },
{ key: 'human', label: 'Human', example: 'Alexa' },
]
const TLDS = ['com', 'io', 'ai', 'co', 'net', 'app']
@ -45,26 +44,19 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type
const tier = (subscription?.tier || '').toLowerCase()
const hasAI = tier === 'trader' || tier === 'tycoon'
// Mode
const [mode, setMode] = useState<'pattern' | 'ai'>('pattern')
// Pattern Mode
// Config
const [pattern, setPattern] = useState('cvcvc')
const [tlds, setTlds] = useState(['com', 'io'])
// AI Mode
const [concept, setConcept] = useState('')
const [similarBrand, setSimilarBrand] = useState('')
const [aiNames, setAiNames] = useState<string[]>([])
// Shared
const [results, setResults] = useState<Array<{ domain: string; available: boolean }>>([])
// State
const [results, setResults] = useState<Array<{ domain: string }>>([])
const [loading, setLoading] = useState(false)
const [aiLoading, setAiLoading] = useState(false)
const [copied, setCopied] = useState<string | null>(null)
const [tracking, setTracking] = useState<string | null>(null)
// Generate Pattern-based
// Generate from pattern
const generatePattern = useCallback(async () => {
if (tlds.length === 0) {
showToast('Select at least one TLD', 'error')
@ -73,9 +65,9 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type
setLoading(true)
setResults([])
try {
const res = await api.huntBrandables({ pattern, tlds, limit: 30, max_checks: 400 })
setResults(res.items.map(i => ({ domain: i.domain, available: true })))
showToast(`Found ${res.items.length} brandable domains!`, 'success')
const res = await api.huntBrandables({ pattern, tlds, limit: 24, max_checks: 300 })
setResults(res.items.map(i => ({ domain: i.domain })))
showToast(`Found ${res.items.length} domains!`, 'success')
} catch (e) {
showToast('Generation failed', 'error')
} finally {
@ -83,54 +75,25 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type
}
}, [pattern, tlds, showToast])
// Generate AI-based
// Generate from AI concept
const generateFromConcept = useCallback(async () => {
if (!concept.trim() || !hasAI) return
setAiLoading(true)
setAiNames([])
setResults([])
try {
const res = await api.generateBrandableNames(concept.trim(), undefined, 15)
setAiNames(res.names || [])
showToast(`AI generated ${res.names?.length || 0} names!`, 'success')
const res = await api.generateBrandableNames(concept.trim(), undefined, 12)
if (res.names?.length) {
// Check availability
const checkRes = await api.huntKeywords({ keywords: res.names, tlds })
const available = checkRes.items.filter(i => i.status === 'available')
setResults(available.map(i => ({ domain: i.domain })))
showToast(`Found ${available.length} available from ${res.names.length} AI names!`, 'success')
}
} catch (e) {
showToast('AI generation failed', 'error')
} finally {
setAiLoading(false)
}
}, [concept, hasAI, showToast])
const generateFromBrand = useCallback(async () => {
if (!similarBrand.trim() || !hasAI) return
setAiLoading(true)
setAiNames([])
setResults([])
try {
const res = await api.generateSimilarNames(similarBrand.trim(), 12)
setAiNames(res.names || [])
showToast(`Found ${res.names?.length || 0} similar names!`, 'success')
} catch (e) {
showToast('AI generation failed', 'error')
} finally {
setAiLoading(false)
}
}, [similarBrand, hasAI, showToast])
// Check AI names availability
const checkAiNames = useCallback(async () => {
if (aiNames.length === 0 || tlds.length === 0) return
setLoading(true)
try {
const res = await api.huntKeywords({ keywords: aiNames, tlds })
const available = res.items.filter(i => i.status === 'available')
setResults(available.map(i => ({ domain: i.domain, available: true })))
showToast(`${available.length} available!`, 'success')
} catch (e) {
showToast('Check failed', 'error')
} finally {
setLoading(false)
}
}, [aiNames, tlds, showToast])
}, [concept, hasAI, tlds, showToast])
// Actions
const copy = (domain: string) => {
@ -139,6 +102,11 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type
setTimeout(() => setCopied(null), 1500)
}
const copyAll = () => {
navigator.clipboard.writeText(results.map(r => r.domain).join('\n'))
showToast(`Copied ${results.length} domains`, 'success')
}
const track = async (domain: string) => {
if (tracking) return
setTracking(domain)
@ -152,260 +120,158 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type
}
}
const copyAll = () => {
if (results.length === 0) return
navigator.clipboard.writeText(results.map(r => r.domain).join('\n'))
showToast(`Copied ${results.length} domains`, 'success')
}
const isGenerating = loading || aiLoading
return (
<div className="space-y-4">
<div className="space-y-6">
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* HEADER + MODE TOGGLE */}
{/* HEADER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h2 className="text-lg font-bold text-white flex items-center gap-2">
<Wand2 className="w-5 h-5 text-purple-400" />
Brandable Forge
</h2>
<p className="text-xs text-white/40 font-mono mt-0.5">
Generate unique, memorable domain names
</p>
</div>
<div className="flex">
<button
onClick={() => { setMode('pattern'); setResults([]); setAiNames([]) }}
className={clsx(
"px-4 py-2 text-xs font-bold uppercase tracking-wider border-y border-l transition-all",
mode === 'pattern'
? "bg-accent/10 border-accent text-accent"
: "bg-white/5 border-white/10 text-white/50 hover:text-white"
)}
>
Patterns
</button>
<button
onClick={() => { if (hasAI) { setMode('ai'); setResults([]); setAiNames([]) }}}
disabled={!hasAI}
className={clsx(
"px-4 py-2 text-xs font-bold uppercase tracking-wider border transition-all flex items-center gap-1.5",
!hasAI && "opacity-50",
mode === 'ai'
? "bg-purple-500/10 border-purple-500 text-purple-400"
: "bg-white/5 border-white/10 text-white/50 hover:text-white"
)}
>
<Brain className="w-3.5 h-3.5" />
AI
{!hasAI && <Lock className="w-3 h-3" />}
</button>
</div>
<div className="pb-4 border-b border-white/10">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Wand2 className="w-5 h-5 text-purple-400" />
Brandable Forge
</h2>
<p className="text-sm text-white/40 mt-1">
Generate unique, memorable domain names
</p>
</div>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* PATTERN MODE */}
{/* GENERATOR */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{mode === 'pattern' && (
<div className="border border-white/10 bg-white/[0.02] p-4 space-y-4">
{/* Pattern Selection */}
<div>
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Choose Pattern</p>
<div className="grid grid-cols-3 gap-2">
{PATTERNS.map(p => (
<button
key={p.key}
onClick={() => setPattern(p.key)}
className={clsx(
"p-3 border text-left transition-all",
pattern === p.key
? "border-accent bg-accent/10"
: "border-white/10 hover:border-white/20"
)}
>
<p className={clsx(
"text-sm font-bold font-mono",
pattern === p.key ? "text-accent" : "text-white/70"
)}>
{p.label}
</p>
<p className="text-[10px] text-white/30 mt-0.5">{p.desc}</p>
<div className="flex gap-1 mt-2">
{p.examples.slice(0, 2).map(ex => (
<span key={ex} className="text-[9px] font-mono text-white/20 bg-white/5 px-1.5 py-0.5">
{ex}
</span>
))}
</div>
</button>
))}
</div>
<div className="bg-white/[0.02] border border-white/10 p-4 space-y-5">
{/* Row 1: Pattern Selection */}
<div>
<p className="text-xs text-white/40 font-mono uppercase tracking-wider mb-2">Pattern</p>
<div className="flex gap-2">
{PATTERNS.map(p => (
<button
key={p.key}
onClick={() => setPattern(p.key)}
className={clsx(
"flex-1 py-3 px-4 border text-center transition-all",
pattern === p.key
? "border-accent bg-accent/10"
: "border-white/10 hover:border-white/20"
)}
>
<p className={clsx(
"text-sm font-bold font-mono",
pattern === p.key ? "text-accent" : "text-white/60"
)}>
{p.label}
</p>
<p className="text-[10px] text-white/30 mt-0.5">{p.example}</p>
</button>
))}
</div>
{/* TLD Selection */}
<div>
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Select TLDs</p>
<div className="flex flex-wrap gap-1.5">
{TLDS.map(tld => (
<button
key={tld}
onClick={() => setTlds(prev =>
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
)}
className={clsx(
"px-3 py-1.5 text-xs font-mono border transition-all",
tlds.includes(tld)
? "border-accent bg-accent/20 text-accent"
: "border-white/10 text-white/40 hover:text-white"
)}
>
.{tld}
</button>
))}
</div>
</div>
{/* Generate Button */}
<button
onClick={generatePattern}
disabled={loading || tlds.length === 0}
className={clsx(
"w-full py-3 text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all",
loading || tlds.length === 0
? "bg-white/10 text-white/30"
: "bg-accent text-black hover:bg-white"
)}
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
Generate Brandables
</button>
</div>
)}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* AI MODE */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{mode === 'ai' && hasAI && (
<div className="border border-purple-500/20 bg-purple-500/5 p-4 space-y-4">
{/* Concept Input */}
<div>
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Describe Your Brand</p>
<div className="flex gap-2">
<input
value={concept}
onChange={(e) => setConcept(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && generateFromConcept()}
placeholder="e.g., AI startup for legal docs..."
className="flex-1 px-3 py-2.5 bg-white/5 border border-white/10 text-sm text-white font-mono placeholder:text-white/25 outline-none focus:border-purple-500/50"
/>
{/* Row 2: TLDs */}
<div>
<p className="text-xs text-white/40 font-mono uppercase tracking-wider mb-2">TLDs</p>
<div className="flex flex-wrap gap-1.5">
{TLDS.map(tld => (
<button
key={tld}
onClick={() => setTlds(prev =>
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
)}
className={clsx(
"px-3 py-1.5 text-xs font-mono border transition-all",
tlds.includes(tld)
? "border-accent bg-accent/10 text-accent"
: "border-white/10 text-white/30 hover:text-white"
)}
>
.{tld}
</button>
))}
</div>
</div>
{/* Row 3: Generate Button */}
<button
onClick={generatePattern}
disabled={isGenerating || tlds.length === 0}
className={clsx(
"w-full py-3.5 text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all",
isGenerating || tlds.length === 0
? "bg-white/10 text-white/30"
: "bg-accent text-black hover:bg-white"
)}
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
Generate {pattern.toUpperCase()} Brandables
</button>
{/* Divider */}
<div className="relative py-2">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/10" />
</div>
<div className="relative flex justify-center">
<span className="px-3 bg-[#0a0a0a] text-[10px] text-white/30 font-mono uppercase">or use AI</span>
</div>
</div>
{/* Row 4: AI Concept */}
<div>
<div className="flex items-center gap-2 mb-2">
<p className="text-xs text-white/40 font-mono uppercase tracking-wider">AI Concept Generator</p>
{!hasAI && (
<span className="flex items-center gap-1 text-[10px] text-white/20">
<Lock className="w-3 h-3" />
Trader+
</span>
)}
</div>
<div className="flex gap-2">
<input
value={concept}
onChange={(e) => setConcept(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && hasAI && generateFromConcept()}
disabled={!hasAI}
placeholder={hasAI ? "Describe your brand concept..." : "Upgrade to Trader to unlock AI"}
className={clsx(
"flex-1 px-3 py-2.5 border text-sm font-mono outline-none transition-all",
hasAI
? "bg-purple-500/5 border-purple-500/20 text-white placeholder:text-white/25 focus:border-purple-500/50"
: "bg-white/[0.02] border-white/10 text-white/30 placeholder:text-white/20"
)}
/>
{hasAI ? (
<button
onClick={generateFromConcept}
disabled={!concept.trim() || aiLoading}
className={clsx(
"px-4 text-xs font-bold uppercase flex items-center gap-1.5 transition-all shrink-0",
"px-4 text-sm font-bold uppercase flex items-center gap-2 transition-all shrink-0",
!concept.trim() || aiLoading
? "bg-white/10 text-white/30"
: "bg-purple-500 text-white hover:bg-purple-400"
)}
>
{aiLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Zap className="w-3.5 h-3.5" />}
{aiLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
<span className="hidden sm:inline">Generate</span>
</button>
</div>
</div>
{/* OR Divider */}
<div className="flex items-center gap-3">
<div className="flex-1 h-px bg-white/10" />
<span className="text-[10px] text-white/30 font-mono">OR</span>
<div className="flex-1 h-px bg-white/10" />
</div>
{/* Similar Brand Input */}
<div>
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Find Names Like...</p>
<div className="flex gap-2">
<input
value={similarBrand}
onChange={(e) => setSimilarBrand(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && generateFromBrand()}
placeholder="e.g., Stripe, Notion, Figma..."
className="flex-1 px-3 py-2.5 bg-white/5 border border-white/10 text-sm text-white font-mono placeholder:text-white/25 outline-none focus:border-purple-500/50"
/>
<button
onClick={generateFromBrand}
disabled={!similarBrand.trim() || aiLoading}
className={clsx(
"px-4 text-xs font-bold uppercase flex items-center gap-1.5 transition-all shrink-0",
!similarBrand.trim() || aiLoading
? "bg-white/10 text-white/30"
: "bg-purple-500 text-white hover:bg-purple-400"
)}
) : (
<Link
href="/pricing"
className="px-4 flex items-center gap-2 bg-white/5 border border-white/10 text-white/40 text-xs font-bold uppercase hover:text-white hover:border-white/20 transition-all"
>
{aiLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Star className="w-3.5 h-3.5" />}
</button>
</div>
Upgrade
</Link>
)}
</div>
{/* AI Names */}
{aiNames.length > 0 && (
<div className="pt-3 border-t border-white/10">
<div className="flex items-center justify-between mb-2">
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider">
AI Suggestions ({aiNames.length})
</p>
<button
onClick={checkAiNames}
disabled={loading || tlds.length === 0}
className="text-[10px] font-mono text-purple-400 hover:text-purple-300 flex items-center gap-1"
>
<Zap className="w-3 h-3" />
Check Availability
</button>
</div>
<div className="flex flex-wrap gap-1.5 mb-3">
{TLDS.map(tld => (
<button
key={tld}
onClick={() => setTlds(prev =>
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
)}
className={clsx(
"px-2 py-1 text-[10px] font-mono border transition-all",
tlds.includes(tld)
? "border-purple-500/40 text-purple-400"
: "border-white/10 text-white/30"
)}
>
.{tld}
</button>
))}
</div>
<div className="flex flex-wrap gap-1.5">
{aiNames.map(name => (
<span key={name} className="px-3 py-1.5 text-xs font-mono text-purple-300 bg-purple-500/10 border border-purple-500/20">
{name}
</span>
))}
</div>
</div>
{hasAI && (
<p className="text-[10px] text-white/20 mt-1.5">
Examples: "AI startup for legal documents", "crypto wallet for teens", "fitness app"
</p>
)}
</div>
)}
{/* Upgrade CTA for Scout */}
{mode === 'ai' && !hasAI && (
<div className="border border-white/10 bg-white/[0.02] p-6 text-center">
<Lock className="w-8 h-8 text-white/20 mx-auto mb-2" />
<p className="text-sm text-white/60 mb-3">AI features require Trader or Tycoon</p>
<Link
href="/pricing"
className="inline-flex items-center gap-2 px-5 py-2.5 bg-accent text-black text-xs font-bold uppercase"
>
<Sparkles className="w-3.5 h-3.5" />
Upgrade
</Link>
</div>
)}
</div>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* RESULTS */}
@ -413,73 +279,62 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type
{results.length > 0 && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs text-white/50 font-mono">
{results.length} available domains
<p className="text-sm text-white/50">
<span className="text-accent font-bold">{results.length}</span> available domains
</p>
<div className="flex gap-2">
<div className="flex gap-3">
<button
onClick={copyAll}
className="text-[10px] font-mono text-accent hover:text-white flex items-center gap-1"
className="text-xs font-mono text-white/40 hover:text-accent flex items-center gap-1"
>
<Copy className="w-3 h-3" />
Copy All
Copy all
</button>
<button
onClick={() => mode === 'pattern' ? generatePattern() : checkAiNames()}
disabled={loading}
className="text-[10px] font-mono text-white/40 hover:text-white flex items-center gap-1"
onClick={generatePattern}
disabled={isGenerating}
className="text-xs font-mono text-white/40 hover:text-white flex items-center gap-1"
>
<RefreshCw className={clsx("w-3 h-3", loading && "animate-spin")} />
<RefreshCw className={clsx("w-3 h-3", isGenerating && "animate-spin")} />
Refresh
</button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{results.map((r, idx) => (
<div
key={r.domain}
className="flex items-center justify-between p-3 border border-accent/20 bg-accent/5 hover:bg-accent/10 transition-all"
className="flex items-center justify-between p-3 bg-accent/5 border border-accent/20 hover:bg-accent/10 transition-all group"
>
<div className="flex items-center gap-2.5 min-w-0">
<span className="w-6 h-6 bg-accent/20 text-accent text-[10px] font-bold font-mono flex items-center justify-center shrink-0">
{String(idx + 1).padStart(2, '0')}
<span className="w-5 h-5 bg-accent/20 text-accent text-[9px] font-bold font-mono flex items-center justify-center shrink-0">
{idx + 1}
</span>
<button
onClick={() => openAnalyze(r.domain)}
className="text-sm font-mono font-medium text-white truncate hover:text-accent"
className="text-sm font-mono font-medium text-white truncate group-hover:text-accent"
>
{r.domain}
</button>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={() => copy(r.domain)}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
>
{copied === r.domain ? <Check className="w-3.5 h-3.5 text-accent" /> : <Copy className="w-3.5 h-3.5" />}
<div className="flex items-center gap-0.5 shrink-0 opacity-60 group-hover:opacity-100">
<button onClick={() => copy(r.domain)} className="w-7 h-7 flex items-center justify-center text-white/50 hover:text-white">
{copied === r.domain ? <Check className="w-3 h-3 text-accent" /> : <Copy className="w-3 h-3" />}
</button>
<button
onClick={() => track(r.domain)}
disabled={tracking === r.domain}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
>
{tracking === r.domain ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
<button onClick={() => track(r.domain)} disabled={tracking === r.domain} className="w-7 h-7 flex items-center justify-center text-white/50 hover:text-white">
{tracking === r.domain ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />}
</button>
<button
onClick={() => openAnalyze(r.domain)}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-accent"
>
<Shield className="w-3.5 h-3.5" />
<button onClick={() => openAnalyze(r.domain)} className="w-7 h-7 flex items-center justify-center text-white/50 hover:text-accent">
<Shield className="w-3 h-3" />
</button>
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${r.domain}`}
target="_blank"
rel="noopener noreferrer"
className="h-8 px-2.5 bg-accent text-black text-[10px] font-bold flex items-center gap-1"
className="h-7 px-2 bg-accent text-black text-[9px] font-bold flex items-center"
>
<ShoppingCart className="w-3 h-3" />
<span className="hidden sm:inline">Buy</span>
Buy
</a>
</div>
</div>
@ -489,14 +344,20 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type
)}
{/* Empty State */}
{results.length === 0 && !loading && !aiLoading && (
<div className="text-center py-10 border border-dashed border-white/10">
<Wand2 className="w-8 h-8 text-white/20 mx-auto mb-2" />
<p className="text-sm text-white/40">
{mode === 'pattern'
? 'Select a pattern and click Generate'
: 'Describe your concept or enter a brand name'}
</p>
{results.length === 0 && !isGenerating && (
<div className="text-center py-12 border border-dashed border-white/10">
<Wand2 className="w-10 h-10 text-white/10 mx-auto mb-3" />
<p className="text-white/40">Choose a pattern and generate</p>
<p className="text-sm text-white/20 mt-1">or describe your concept for AI suggestions</p>
</div>
)}
{/* Loading State */}
{isGenerating && results.length === 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{[...Array(12)].map((_, i) => (
<div key={i} className="h-12 bg-white/5 animate-pulse" />
))}
</div>
)}
</div>

View File

@ -342,7 +342,7 @@ export function SearchTab({ showToast }: SearchTabProps) {
) : (
<div className={clsx(
"border-2 overflow-hidden bg-[#020202]",
searchResult.is_available ? "border-accent/40" : "border-white/[0.08]"
searchResult.is_available ? "border-accent/40" : "border-rose-500/30"
)}>
{/* Result Row */}
<div className="p-4">
@ -351,29 +351,32 @@ export function SearchTab({ showToast }: SearchTabProps) {
{/* Status Icon */}
<div className={clsx(
"w-12 h-12 flex items-center justify-center border shrink-0",
searchResult.is_available ? "bg-accent/10 border-accent/30" : "bg-white/[0.02] border-white/[0.08]"
searchResult.is_available ? "bg-accent/10 border-accent/30" : "bg-rose-500/10 border-rose-500/30"
)}>
{searchResult.is_available ? (
<CheckCircle2 className="w-6 h-6 text-accent" />
) : (
<XCircle className="w-6 h-6 text-white/30" />
<XCircle className="w-6 h-6 text-rose-500" />
)}
</div>
{/* Domain Info */}
<div className="min-w-0 flex-1">
<div className="text-lg font-bold text-white font-mono truncate">{searchResult.domain}</div>
<div className={clsx(
"text-lg font-bold font-mono truncate",
searchResult.is_available ? "text-white" : "text-rose-400"
)}>{searchResult.domain}</div>
<div className="flex items-center gap-3 text-[10px] font-mono text-white/40 mt-1">
<span className={clsx(
"px-2 py-0.5 uppercase font-bold",
searchResult.is_available ? "bg-accent/20 text-accent" : "bg-white/10 text-white/50"
searchResult.is_available ? "bg-accent/20 text-accent" : "bg-rose-500/20 text-rose-400"
)}>
{searchResult.is_available ? 'Available' : 'Taken'}
</span>
{searchResult.registrar && (
<>
<span className="text-white/10">|</span>
<span className="flex items-center gap-1">
<span className="flex items-center gap-1" title="Current registrar holding this domain">
<Building className="w-3 h-3" />
{searchResult.registrar}
</span>
@ -382,7 +385,7 @@ export function SearchTab({ showToast }: SearchTabProps) {
{searchResult.expiration_date && (
<>
<span className="text-white/10">|</span>
<span className="flex items-center gap-1">
<span className="flex items-center gap-1" title="Domain expiration date - monitor for potential drop">
<Calendar className="w-3 h-3" />
Expires {new Date(searchResult.expiration_date).toLocaleDateString()}
</span>
@ -397,7 +400,7 @@ export function SearchTab({ showToast }: SearchTabProps) {
<button
onClick={() => openAnalyze(searchResult.domain)}
className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/30 hover:text-accent hover:border-accent/20 hover:bg-accent/10 transition-colors"
title="Deep Analysis"
title="Deep Analysis - View SEO metrics, backlinks, and valuation"
>
<Shield className="w-4 h-4" />
</button>
@ -409,9 +412,9 @@ export function SearchTab({ showToast }: SearchTabProps) {
"w-9 h-9 flex items-center justify-center border transition-colors",
searchResult.is_available
? "border-white/10 text-white/30 hover:text-white hover:bg-white/5"
: "border-accent/30 text-accent hover:bg-accent/10"
: "border-rose-500/30 text-rose-400 hover:bg-rose-500/10"
)}
title={searchResult.is_available ? "Track" : "Monitor for drops"}
title={searchResult.is_available ? "Add to watchlist to track this domain" : "Monitor this domain and get notified when it drops"}
>
{addingToWatchlist ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
</button>
@ -422,20 +425,21 @@ export function SearchTab({ showToast }: SearchTabProps) {
target="_blank"
rel="noopener noreferrer"
className="h-9 px-4 bg-accent text-black text-xs font-bold uppercase tracking-wider flex items-center gap-2 hover:bg-white transition-colors"
title="Register this domain now via Namecheap"
>
Register
<ArrowRight className="w-3.5 h-3.5" />
</a>
) : (
<a
href={`https://www.expireddomains.net/domain-name-search/?q=${searchResult.domain.split('.')[0]}`}
target="_blank"
rel="noopener noreferrer"
className="h-9 px-4 bg-white/10 text-white text-xs font-bold uppercase tracking-wider flex items-center gap-2 hover:bg-white/20 transition-colors"
<button
onClick={handleAddToWatchlist}
disabled={addingToWatchlist}
className="h-9 px-4 bg-rose-500/20 text-rose-400 text-xs font-bold uppercase tracking-wider flex items-center gap-2 hover:bg-rose-500/30 border border-rose-500/30 transition-colors"
title="Add to watchlist and get notified when this domain becomes available"
>
Find Similar
<ExternalLink className="w-3.5 h-3.5" />
</a>
{addingToWatchlist ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
Monitor
</button>
)}
</div>
</div>
@ -467,13 +471,19 @@ export function SearchTab({ showToast }: SearchTabProps) {
{tldResults.map((result) => (
<div
key={result.tld}
title={result.loading
? `Checking ${result.domain}...`
: result.is_available
? `${result.domain} is available! Click to see details.`
: `${result.domain} is already registered`
}
className={clsx(
"p-3 border transition-all",
result.loading
? "border-white/[0.08] bg-white/[0.02]"
: result.is_available
? "border-accent/40 bg-accent/[0.05] hover:bg-accent/10 cursor-pointer"
: "border-white/[0.06] bg-white/[0.01]"
: "border-rose-500/20 bg-rose-500/[0.02]"
)}
onClick={() => {
if (result.is_available && !result.loading) {
@ -493,19 +503,19 @@ export function SearchTab({ showToast }: SearchTabProps) {
<div className="flex items-center justify-between mb-1">
<span className={clsx(
"text-xs font-mono font-bold",
result.is_available ? "text-accent" : "text-white/30"
result.is_available ? "text-accent" : "text-rose-400/60"
)}>
.{result.tld}
</span>
{result.is_available ? (
<CheckCircle2 className="w-3.5 h-3.5 text-accent" />
) : (
<XCircle className="w-3.5 h-3.5 text-white/20" />
<XCircle className="w-3.5 h-3.5 text-rose-400/40" />
)}
</div>
<div className={clsx(
"text-[9px] font-mono uppercase",
result.is_available ? "text-accent/60" : "text-white/20"
result.is_available ? "text-accent/60" : "text-rose-400/40"
)}>
{result.is_available ? 'Available' : 'Taken'}
</div>

View File

@ -14,9 +14,9 @@ import {
Flame,
Sparkles,
Lock,
ChevronRight,
Globe,
X,
ChevronDown,
} from 'lucide-react'
import Link from 'next/link'
import { api } from '@/lib/api'
@ -54,10 +54,14 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
const [trends, setTrends] = useState<Array<{ title: string; approx_traffic?: string | null }>>([])
const [selected, setSelected] = useState<string | null>(null)
const [tlds, setTlds] = useState(['com', 'io', 'ai'])
// Keywords to check (original + AI expanded)
const [keywords, setKeywords] = useState<string[]>([])
const [aiLoading, setAiLoading] = useState(false)
// Results
const [results, setResults] = useState<Array<{ domain: string; available: boolean }>>([])
const [checking, setChecking] = useState(false)
const [aiKeywords, setAiKeywords] = useState<string[]>([])
const [aiLoading, setAiLoading] = useState(false)
const [copied, setCopied] = useState<string | null>(null)
const [tracking, setTracking] = useState<string | null>(null)
@ -78,35 +82,62 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
loadTrends()
}, [loadTrends])
// Check availability
const checkAvailability = useCallback(async (keyword: string) => {
if (!keyword || tlds.length === 0) return
// When a trend is selected, set the base keyword and auto-expand with AI if available
const selectTrend = useCallback(async (trend: string) => {
const baseKeyword = trend.toLowerCase().replace(/\s+/g, '')
setSelected(trend)
setKeywords([baseKeyword])
setResults([])
// Auto-expand with AI if available
if (hasAI) {
setAiLoading(true)
try {
const res = await api.expandTrendKeywords(trend, geo)
if (res.keywords?.length) {
// Combine base + AI keywords, remove duplicates
const all = [baseKeyword, ...res.keywords.filter(k => k !== baseKeyword)]
setKeywords(all.slice(0, 8)) // Max 8 keywords
}
} catch (e) {
// Silent fail for AI
} finally {
setAiLoading(false)
}
}
}, [geo, hasAI])
// Check availability for all keywords
const checkAvailability = useCallback(async () => {
if (keywords.length === 0 || tlds.length === 0) return
setChecking(true)
setResults([])
try {
const kw = keyword.toLowerCase().replace(/\s+/g, '')
const res = await api.huntKeywords({ keywords: [kw], tlds })
const res = await api.huntKeywords({ keywords, tlds })
setResults(res.items.map(i => ({ domain: i.domain, available: i.status === 'available' })))
const avail = res.items.filter(i => i.status === 'available').length
showToast(`Found ${avail} available domains!`, 'success')
} catch (e) {
showToast('Check failed', 'error')
} finally {
setChecking(false)
}
}, [tlds, showToast])
}, [keywords, tlds, showToast])
// AI Expand
const expandWithAI = useCallback(async () => {
if (!selected || !hasAI) return
setAiLoading(true)
try {
const res = await api.expandTrendKeywords(selected, geo)
setAiKeywords(res.keywords || [])
} catch (e) {
showToast('AI expansion failed', 'error')
} finally {
setAiLoading(false)
// Remove a keyword
const removeKeyword = (kw: string) => {
setKeywords(prev => prev.filter(k => k !== kw))
}
// Add custom keyword
const [customKw, setCustomKw] = useState('')
const addKeyword = () => {
const kw = customKw.trim().toLowerCase().replace(/\s+/g, '')
if (kw && !keywords.includes(kw)) {
setKeywords(prev => [...prev, kw])
setCustomKw('')
}
}, [selected, geo, hasAI, showToast])
}
// Actions
const copy = (domain: string) => {
@ -129,28 +160,29 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
}
const currentGeo = GEOS.find(g => g.code === geo)
const availableCount = results.filter(r => r.available).length
const availableResults = results.filter(r => r.available)
const takenResults = results.filter(r => !r.available)
return (
<div className="space-y-4">
<div className="space-y-6">
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* HEADER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-4 border-b border-white/10">
<div>
<h2 className="text-lg font-bold text-white flex items-center gap-2">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Flame className="w-5 h-5 text-orange-400" />
Trending Now
Trend Surfer
</h2>
<p className="text-xs text-white/40 font-mono mt-0.5">
Real-time Google Trends Domain opportunities
<p className="text-sm text-white/40 mt-1">
Find domains for trending topics {currentGeo?.flag} {currentGeo?.name}
</p>
</div>
<div className="flex items-center gap-2">
<select
value={geo}
onChange={(e) => { setGeo(e.target.value); setSelected(null); setResults([]); setAiKeywords([]) }}
className="h-10 px-3 bg-white/5 border border-white/10 text-sm font-mono text-white outline-none"
onChange={(e) => { setGeo(e.target.value); setSelected(null); setKeywords([]); setResults([]) }}
className="h-9 px-3 bg-white/5 border border-white/10 text-sm text-white outline-none"
>
{GEOS.map(g => (
<option key={g.code} value={g.code}>{g.flag} {g.name}</option>
@ -159,7 +191,7 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
<button
onClick={loadTrends}
disabled={loading}
className="h-10 w-10 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5"
className="h-9 w-9 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5"
>
<RefreshCw className={clsx("w-4 h-4", loading && "animate-spin")} />
</button>
@ -169,72 +201,122 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* TRENDS GRID */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{loading ? (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
{[...Array(8)].map((_, i) => (
<div key={i} className="h-16 bg-white/5 animate-pulse" />
))}
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
{trends.slice(0, 16).map((t, idx) => {
const isSelected = selected === t.title
const isHot = idx < 3
return (
<button
key={t.title}
onClick={() => {
setSelected(isSelected ? null : t.title)
setResults([])
setAiKeywords([])
}}
className={clsx(
"relative p-3 text-left border transition-all",
isSelected
? "border-accent bg-accent/10"
: "border-white/10 bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.04]"
)}
>
{isHot && (
<span className="absolute top-2 right-2 text-[10px]">🔥</span>
)}
<p className={clsx(
"text-sm font-medium truncate",
isSelected ? "text-accent" : "text-white/80"
)}>
{t.title}
</p>
{t.approx_traffic && (
<p className="text-[10px] text-white/30 font-mono mt-1">{t.approx_traffic}</p>
)}
</button>
)
})}
</div>
)}
<div>
<p className="text-xs text-white/30 font-mono uppercase tracking-wider mb-3">
Select a trending topic
</p>
{loading ? (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{[...Array(8)].map((_, i) => (
<div key={i} className="h-14 bg-white/5 animate-pulse" />
))}
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{trends.slice(0, 12).map((t, idx) => {
const isSelected = selected === t.title
return (
<button
key={t.title}
onClick={() => selectTrend(t.title)}
className={clsx(
"relative p-3 text-left border transition-all",
isSelected
? "border-accent bg-accent/10"
: "border-white/10 bg-white/[0.02] hover:border-white/20"
)}
>
{idx < 3 && <span className="absolute top-1.5 right-1.5 text-[10px]">🔥</span>}
<p className={clsx(
"text-sm font-medium truncate pr-4",
isSelected ? "text-accent" : "text-white/80"
)}>
{t.title}
</p>
</button>
)
})}
</div>
)}
</div>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* SELECTED TREND PANEL */}
{/* KEYWORD BUILDER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{selected && (
<div className="border border-accent/30 bg-accent/5 p-4 space-y-4">
{/* Header */}
<div className="flex items-start justify-between gap-3">
<div className="bg-white/[0.02] border border-white/10 p-4 space-y-4">
{/* Selected Trend Header */}
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-accent font-mono uppercase tracking-wider">Selected Trend</p>
<h3 className="text-xl font-bold text-white mt-1">{selected}</h3>
<p className="text-xs text-white/40 font-mono uppercase tracking-wider">Building domains for</p>
<h3 className="text-lg font-bold text-white">{selected}</h3>
</div>
<button
onClick={() => { setSelected(null); setResults([]); setAiKeywords([]) }}
className="p-2 text-white/40 hover:text-white"
onClick={() => { setSelected(null); setKeywords([]); setResults([]) }}
className="p-2 text-white/30 hover:text-white"
>
<X className="w-4 h-4" />
</button>
</div>
{/* TLD Selection */}
{/* Keywords */}
<div>
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Select TLDs</p>
<div className="flex items-center gap-2 mb-2">
<p className="text-xs text-white/40 font-mono uppercase tracking-wider">Keywords to check</p>
{aiLoading && (
<span className="flex items-center gap-1 text-[10px] text-purple-400">
<Loader2 className="w-3 h-3 animate-spin" />
AI expanding...
</span>
)}
{hasAI && keywords.length > 1 && !aiLoading && (
<span className="flex items-center gap-1 text-[10px] text-purple-400">
<Sparkles className="w-3 h-3" />
AI expanded
</span>
)}
{!hasAI && (
<Link href="/pricing" className="flex items-center gap-1 text-[10px] text-white/30 hover:text-accent">
<Lock className="w-3 h-3" />
Upgrade for AI
</Link>
)}
</div>
<div className="flex flex-wrap gap-1.5 mb-3">
{keywords.map((kw, idx) => (
<span
key={kw}
className={clsx(
"inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-mono border",
idx === 0
? "bg-accent/10 border-accent/30 text-accent"
: "bg-purple-500/10 border-purple-500/20 text-purple-400"
)}
>
{kw}
{keywords.length > 1 && (
<button onClick={() => removeKeyword(kw)} className="hover:text-white">
<X className="w-3 h-3" />
</button>
)}
</span>
))}
{/* Add custom keyword */}
<div className="flex">
<input
value={customKw}
onChange={(e) => setCustomKw(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addKeyword()}
placeholder="+ add"
className="w-20 px-2 py-1 bg-transparent border border-white/10 text-xs font-mono text-white placeholder:text-white/20 outline-none focus:border-white/30"
/>
</div>
</div>
</div>
{/* TLDs */}
<div>
<p className="text-xs text-white/40 font-mono uppercase tracking-wider mb-2">TLDs</p>
<div className="flex flex-wrap gap-1.5">
{TLDS.map(tld => (
<button
@ -243,10 +325,10 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
)}
className={clsx(
"px-3 py-1.5 text-xs font-mono border transition-all",
"px-2.5 py-1 text-xs font-mono border transition-all",
tlds.includes(tld)
? "border-accent bg-accent/20 text-accent"
: "border-white/10 text-white/40 hover:text-white"
? "border-accent bg-accent/10 text-accent"
: "border-white/10 text-white/30 hover:text-white"
)}
>
.{tld}
@ -257,134 +339,96 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
{/* Check Button */}
<button
onClick={() => checkAvailability(selected)}
disabled={checking || tlds.length === 0}
onClick={checkAvailability}
disabled={checking || tlds.length === 0 || keywords.length === 0}
className={clsx(
"w-full py-3 text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all",
checking || tlds.length === 0
checking || tlds.length === 0 || keywords.length === 0
? "bg-white/10 text-white/30"
: "bg-accent text-black hover:bg-white"
)}
>
{checking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
Check {selected.toLowerCase().replace(/\s+/g, '')} Availability
Check {keywords.length} × {tlds.length} = {keywords.length * tlds.length} Domains
</button>
</div>
)}
{/* AI Expansion */}
<div className="pt-3 border-t border-white/10">
<div className="flex items-center justify-between mb-2">
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider flex items-center gap-1.5">
<Sparkles className="w-3 h-3 text-purple-400" />
AI Related Keywords
{!hasAI && <Lock className="w-3 h-3" />}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* RESULTS */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{results.length > 0 && (
<div className="space-y-3">
{/* Available */}
{availableResults.length > 0 && (
<div>
<p className="text-xs text-accent font-mono uppercase tracking-wider mb-2 flex items-center gap-2">
<span className="w-2 h-2 bg-accent rounded-full" />
{availableResults.length} Available
</p>
{hasAI ? (
<button
onClick={expandWithAI}
disabled={aiLoading}
className="text-[10px] font-mono text-purple-400 hover:text-purple-300 flex items-center gap-1"
>
{aiLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <ChevronRight className="w-3 h-3" />}
Expand
</button>
) : (
<Link href="/pricing" className="text-[10px] font-mono text-accent">
Upgrade
</Link>
)}
</div>
{aiKeywords.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{aiKeywords.map(kw => (
<button
key={kw}
onClick={() => checkAvailability(kw)}
className="px-2.5 py-1 text-[11px] font-mono text-purple-400 bg-purple-500/10 border border-purple-500/20 hover:bg-purple-500/20"
>
{kw}
</button>
))}
</div>
)}
</div>
{/* Results */}
{results.length > 0 && (
<div className="pt-3 border-t border-white/10">
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">
{availableCount} of {results.length} available
</p>
<div className="space-y-1.5">
{results.map(r => (
<div
key={r.domain}
className={clsx(
"flex items-center justify-between p-2.5 border transition-all",
r.available
? "border-accent/30 bg-accent/5"
: "border-white/5 bg-white/[0.02]"
)}
>
<div className="flex items-center gap-2 min-w-0">
<span className={clsx(
"w-2 h-2 rounded-full shrink-0",
r.available ? "bg-accent" : "bg-white/20"
)} />
<button
onClick={() => openAnalyze(r.domain)}
className={clsx(
"text-sm font-mono truncate",
r.available ? "text-white hover:text-accent" : "text-white/40"
)}
>
{r.domain}
</button>
</div>
<div className="space-y-1">
{availableResults.map(r => (
<div key={r.domain} className="flex items-center justify-between p-2.5 bg-accent/5 border border-accent/20">
<button
onClick={() => openAnalyze(r.domain)}
className="text-sm font-mono text-white hover:text-accent truncate"
>
{r.domain}
</button>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={() => copy(r.domain)}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
>
<button onClick={() => copy(r.domain)} className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white">
{copied === r.domain ? <Check className="w-3.5 h-3.5 text-accent" /> : <Copy className="w-3.5 h-3.5" />}
</button>
<button
onClick={() => track(r.domain)}
disabled={tracking === r.domain}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
>
<button onClick={() => track(r.domain)} disabled={tracking === r.domain} className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white">
{tracking === r.domain ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
</button>
<button
onClick={() => openAnalyze(r.domain)}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-accent"
>
<button onClick={() => openAnalyze(r.domain)} className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-accent">
<Shield className="w-3.5 h-3.5" />
</button>
{r.available && (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${r.domain}`}
target="_blank"
rel="noopener noreferrer"
className="h-8 px-2.5 bg-accent text-black text-[10px] font-bold flex items-center gap-1"
>
<ShoppingCart className="w-3 h-3" />
<span className="hidden sm:inline">Buy</span>
</a>
)}
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${r.domain}`}
target="_blank"
rel="noopener noreferrer"
className="h-8 px-3 bg-accent text-black text-[10px] font-bold flex items-center gap-1"
>
<ShoppingCart className="w-3 h-3" />
Buy
</a>
</div>
</div>
))}
</div>
</div>
)}
{/* Taken (collapsed) */}
{takenResults.length > 0 && (
<details className="group">
<summary className="text-xs text-white/30 font-mono uppercase tracking-wider cursor-pointer flex items-center gap-2 py-2">
<ChevronDown className="w-3 h-3 group-open:rotate-180 transition-transform" />
{takenResults.length} Taken
</summary>
<div className="space-y-1 mt-2">
{takenResults.map(r => (
<div key={r.domain} className="flex items-center justify-between p-2 bg-white/[0.02] border border-white/5">
<span className="text-sm font-mono text-white/30 truncate">{r.domain}</span>
<button onClick={() => openAnalyze(r.domain)} className="text-white/20 hover:text-white">
<Shield className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
</details>
)}
</div>
)}
{/* Empty State */}
{!selected && !loading && trends.length > 0 && (
<div className="text-center py-8 border border-dashed border-white/10">
<Globe className="w-8 h-8 text-white/20 mx-auto mb-2" />
<p className="text-sm text-white/40">Select a trend to find available domains</p>
<div className="text-center py-12 border border-dashed border-white/10">
<Globe className="w-10 h-10 text-white/10 mx-auto mb-3" />
<p className="text-white/40">Select a trending topic above</p>
<p className="text-sm text-white/20 mt-1">We'll find available domains for you</p>
</div>
)}
</div>