Trends & Forge tabs: complete redesign - cleaner UI, mobile-first, progressive disclosure
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 15:56:37 +01:00
parent e135c3258b
commit 0618d8517d
3 changed files with 671 additions and 1186 deletions

View File

@ -3,23 +3,19 @@
import { useCallback, useState } from 'react'
import clsx from 'clsx'
import {
ExternalLink,
Loader2,
Shield,
Sparkles,
Eye,
Wand2,
Settings,
Zap,
Copy,
Check,
ShoppingCart,
Star,
Lightbulb,
RefreshCw,
Sparkles,
Lock,
Star,
Brain,
MessageSquare,
RefreshCw,
} from 'lucide-react'
import Link from 'next/link'
import { api } from '@/lib/api'
@ -31,383 +27,362 @@ import { useStore } from '@/lib/store'
// ============================================================================
const PATTERNS = [
{
key: 'cvcvc',
label: 'CVCVC',
desc: 'Classic 5-letter brandables',
examples: ['Zalor', 'Mivex', 'Ronix'],
color: 'accent'
},
{
key: 'cvccv',
label: 'CVCCV',
desc: 'Punchy 5-letter names',
examples: ['Bento', 'Salvo', 'Vento'],
color: 'blue'
},
{
key: 'human',
label: 'Human',
desc: 'AI agent ready names',
examples: ['Siri', 'Alexa', 'Levi'],
color: 'purple'
},
{ 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' },
]
const TLDS = [
{ tld: 'com', premium: true, label: '.com' },
{ tld: 'io', premium: true, label: '.io' },
{ tld: 'ai', premium: true, label: '.ai' },
{ tld: 'co', premium: false, label: '.co' },
{ tld: 'net', premium: false, label: '.net' },
{ tld: 'org', premium: false, label: '.org' },
{ tld: 'app', premium: false, label: '.app' },
{ tld: 'dev', premium: false, label: '.dev' },
]
const TLDS = ['com', 'io', 'ai', 'co', 'net', 'app']
// ============================================================================
// COMPONENT
// ============================================================================
export function BrandableForgeTab({ showToast }: { showToast: (message: string, type?: any) => void }) {
export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type?: any) => void }) {
const openAnalyze = useAnalyzePanelStore((s) => s.open)
const addDomain = useStore((s) => s.addDomain)
const subscription = useStore((s) => s.subscription)
const tier = (subscription?.tier || '').toLowerCase()
const hasAI = tier === 'trader' || tier === 'tycoon'
// Mode: 'pattern' (classic) or 'ai' (concept-based)
// Mode
const [mode, setMode] = useState<'pattern' | 'ai'>('pattern')
// Config State
// Pattern Mode
const [pattern, setPattern] = useState('cvcvc')
const [selectedTlds, setSelectedTlds] = useState<string[]>(['com', 'io'])
const [limit, setLimit] = useState(30)
const [showConfig, setShowConfig] = useState(false)
const [tlds, setTlds] = useState(['com', 'io'])
// AI State
// AI Mode
const [concept, setConcept] = useState('')
const [conceptFocused, setConceptFocused] = useState(false)
const [similarBrand, setSimilarBrand] = useState('')
const [similarFocused, setSimilarFocused] = useState(false)
const [aiNames, setAiNames] = useState<string[]>([])
const [aiLoading, setAiLoading] = useState(false)
// Results State
// Shared
const [results, setResults] = useState<Array<{ domain: string; available: boolean }>>([])
const [loading, setLoading] = useState(false)
const [items, setItems] = useState<Array<{ domain: string; status: string }>>([])
const [error, setError] = useState<string | null>(null)
const [tracking, setTracking] = useState<string | null>(null)
const [aiLoading, setAiLoading] = useState(false)
const [copied, setCopied] = useState<string | null>(null)
const [tracking, setTracking] = useState<string | null>(null)
const toggleTld = useCallback((tld: string) => {
setSelectedTlds((prev) =>
prev.includes(tld) ? prev.filter((t) => t !== tld) : [...prev, tld]
)
}, [])
const copyDomain = useCallback((domain: string) => {
navigator.clipboard.writeText(domain)
setCopied(domain)
setTimeout(() => setCopied(null), 1500)
}, [])
const copyAll = useCallback(() => {
if (items.length === 0) return
navigator.clipboard.writeText(items.map(i => i.domain).join('\n'))
showToast(`Copied ${items.length} domains to clipboard`, 'success')
}, [items, showToast])
const run = useCallback(async () => {
if (selectedTlds.length === 0) {
// Generate Pattern-based
const generatePattern = useCallback(async () => {
if (tlds.length === 0) {
showToast('Select at least one TLD', 'error')
return
}
setLoading(true)
setError(null)
setItems([])
setResults([])
try {
const res = await api.huntBrandables({ pattern, tlds: selectedTlds, limit, max_checks: 400 })
setItems(res.items.map((i) => ({ domain: i.domain, status: i.status })))
if (res.items.length === 0) {
showToast('No available domains found. Try different settings.', 'info')
} else {
showToast(`Found ${res.items.length} available brandable domains!`, 'success')
}
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')
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
setError(msg)
showToast(msg, 'error')
setItems([])
showToast('Generation failed', 'error')
} finally {
setLoading(false)
}
}, [pattern, selectedTlds, limit, showToast])
}, [pattern, tlds, showToast])
const track = useCallback(
async (domain: string) => {
// Generate AI-based
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')
} 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])
// Actions
const copy = (domain: string) => {
navigator.clipboard.writeText(domain)
setCopied(domain)
setTimeout(() => setCopied(null), 1500)
}
const track = async (domain: string) => {
if (tracking) return
setTracking(domain)
try {
await addDomain(domain)
showToast(`Added to watchlist: ${domain}`, 'success')
showToast(`Added: ${domain}`, 'success')
} catch (e) {
showToast(e instanceof Error ? e.message : 'Failed to track domain', 'error')
showToast('Failed', 'error')
} finally {
setTracking(null)
}
},
[addDomain, showToast, tracking]
)
}
const currentPattern = PATTERNS.find(p => p.key === pattern)
// AI Generation
const generateFromConcept = useCallback(async () => {
if (!concept.trim() || !hasAI || aiLoading) return
setAiLoading(true)
setAiNames([])
try {
const res = await api.generateBrandableNames(concept.trim(), undefined, 15)
setAiNames(res.names || [])
if (res.names?.length) {
showToast(`AI generated ${res.names.length} names!`, 'success')
} else {
showToast('No names generated. Try a different concept.', 'info')
const copyAll = () => {
if (results.length === 0) return
navigator.clipboard.writeText(results.map(r => r.domain).join('\n'))
showToast(`Copied ${results.length} domains`, 'success')
}
} catch (e) {
showToast(e instanceof Error ? e.message : 'AI generation failed', 'error')
} finally {
setAiLoading(false)
}
}, [concept, hasAI, aiLoading, showToast])
const generateFromBrand = useCallback(async () => {
if (!similarBrand.trim() || !hasAI || aiLoading) return
setAiLoading(true)
setAiNames([])
try {
const res = await api.generateSimilarNames(similarBrand.trim(), 12)
setAiNames(res.names || [])
if (res.names?.length) {
showToast(`AI found ${res.names.length} similar names!`, 'success')
}
} catch (e) {
showToast(e instanceof Error ? e.message : 'AI generation failed', 'error')
} finally {
setAiLoading(false)
}
}, [similarBrand, hasAI, aiLoading, showToast])
// Check AI-generated names for availability
const checkAiNames = useCallback(async () => {
if (aiNames.length === 0 || selectedTlds.length === 0) return
setLoading(true)
setItems([])
try {
const res = await api.huntKeywords({ keywords: aiNames, tlds: selectedTlds })
const available = res.items.filter(i => i.status === 'available')
setItems(available.map(i => ({ domain: i.domain, status: i.status })))
showToast(`Found ${available.length} available domains!`, 'success')
} catch (e) {
showToast(e instanceof Error ? e.message : 'Availability check failed', 'error')
} finally {
setLoading(false)
}
}, [aiNames, selectedTlds, showToast])
return (
<div className="space-y-6">
<div className="space-y-4">
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* MAIN GENERATOR CARD */}
{/* HEADER + MODE TOGGLE */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="border border-white/[0.08] bg-[#020202]">
{/* Header */}
<div className="px-4 py-4 border-b border-white/[0.08]">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-accent/20 to-purple-500/10 border border-accent/30 flex items-center justify-center">
<Wand2 className="w-5 h-5 text-accent" />
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h3 className="text-base font-bold text-white">Brandable Forge</h3>
<p className="text-[11px] font-mono text-white/40">
{mode === 'pattern' ? 'Pattern-based name generator' : 'AI-powered concept generator'}
<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>
<div className="flex items-center gap-2">
<div className="flex">
<button
onClick={() => setShowConfig(!showConfig)}
onClick={() => { setMode('pattern'); setResults([]); setAiNames([]) }}
className={clsx(
"w-9 h-9 flex items-center justify-center border transition-all",
showConfig
? "border-accent/30 bg-accent/10 text-accent"
: "border-white/10 text-white/40 hover:text-white hover:bg-white/5"
)}
title="Settings"
>
<Settings className="w-4 h-4" />
</button>
<button
onClick={run}
disabled={loading || selectedTlds.length === 0}
className={clsx(
"h-9 px-5 text-sm font-bold uppercase tracking-wider transition-all flex items-center gap-2",
loading || selectedTlds.length === 0
? "bg-white/5 text-white/20 cursor-not-allowed"
: "bg-accent text-black hover:bg-white"
)}
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Generating...
</>
) : (
<>
<Sparkles className="w-4 h-4" />
Generate
</>
)}
</button>
</div>
</div>
</div>
{/* Mode Toggle */}
<div className="px-4 py-3 border-b border-white/[0.08] bg-white/[0.01]">
<div className="flex items-center gap-2">
<button
onClick={() => setMode('pattern')}
className={clsx(
"flex items-center gap-2 px-4 py-2 text-xs font-bold uppercase tracking-wider border transition-all",
"px-4 py-2 text-xs font-bold uppercase tracking-wider border-y border-l transition-all",
mode === 'pattern'
? "border-accent bg-accent/10 text-accent"
: "border-white/10 text-white/40 hover:text-white hover:border-white/20"
? "bg-accent/10 border-accent text-accent"
: "bg-white/5 border-white/10 text-white/50 hover:text-white"
)}
>
<Lightbulb className="w-3.5 h-3.5" />
Patterns
</button>
<button
onClick={() => hasAI && setMode('ai')}
className={clsx(
"flex items-center gap-2 px-4 py-2 text-xs font-bold uppercase tracking-wider border transition-all",
!hasAI && "opacity-50 cursor-not-allowed",
mode === 'ai'
? "border-purple-500 bg-purple-500/10 text-purple-400"
: "border-white/10 text-white/40 hover:text-white hover:border-white/20"
)}
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 Concept
{!hasAI && <Lock className="w-3 h-3 ml-1" />}
AI
{!hasAI && <Lock className="w-3 h-3" />}
</button>
{!hasAI && (
<Link href="/pricing" className="ml-auto text-[10px] font-mono text-accent hover:underline">
Upgrade for AI
</Link>
)}
</div>
</div>
{/* AI Concept Mode */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* PATTERN MODE */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{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>
{/* 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="p-4 border-b border-white/[0.08] space-y-4">
<div className="border border-purple-500/20 bg-purple-500/5 p-4 space-y-4">
{/* Concept Input */}
<div>
<label className="block text-[10px] font-mono text-white/40 mb-2 uppercase tracking-wider">
Describe your brand concept
</label>
<div className={clsx(
"flex border-2 transition-all",
conceptFocused ? "border-purple-400/50 bg-purple-400/[0.03]" : "border-white/[0.08] bg-white/[0.02]"
)}>
<MessageSquare className={clsx("w-4 h-4 m-3.5 transition-colors", conceptFocused ? "text-purple-400" : "text-white/30")} />
<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)}
onFocus={() => setConceptFocused(true)}
onBlur={() => setConceptFocused(false)}
onKeyDown={(e) => e.key === 'Enter' && generateFromConcept()}
placeholder="e.g., AI startup for legal documents, crypto wallet for teens..."
className="flex-1 bg-transparent py-3 pr-3 text-sm text-white placeholder:text-white/25 outline-none font-mono"
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"
/>
<button
onClick={generateFromConcept}
disabled={!concept.trim() || aiLoading}
className={clsx(
"px-4 m-1.5 text-[10px] font-bold uppercase flex items-center gap-1.5 transition-all",
"px-4 text-xs font-bold uppercase flex items-center gap-1.5 transition-all shrink-0",
!concept.trim() || aiLoading
? "bg-white/5 text-white/20"
? "bg-white/10 text-white/30"
: "bg-purple-500 text-white hover:bg-purple-400"
)}
>
{aiLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <Sparkles className="w-3 h-3" />}
Generate
{aiLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Zap className="w-3.5 h-3.5" />}
</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>
<label className="block text-[10px] font-mono text-white/40 mb-2 uppercase tracking-wider">
Or find names similar to a brand
</label>
<div className={clsx(
"flex border-2 transition-all",
similarFocused ? "border-purple-400/50 bg-purple-400/[0.03]" : "border-white/[0.08] bg-white/[0.02]"
)}>
<Star className={clsx("w-4 h-4 m-3.5 transition-colors", similarFocused ? "text-purple-400" : "text-white/30")} />
<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)}
onFocus={() => setSimilarFocused(true)}
onBlur={() => setSimilarFocused(false)}
onKeyDown={(e) => e.key === 'Enter' && generateFromBrand()}
placeholder="e.g., Stripe, Notion, Figma..."
className="flex-1 bg-transparent py-3 pr-3 text-sm text-white placeholder:text-white/25 outline-none font-mono"
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 m-1.5 text-[10px] font-bold uppercase flex items-center gap-1.5 transition-all",
"px-4 text-xs font-bold uppercase flex items-center gap-1.5 transition-all shrink-0",
!similarBrand.trim() || aiLoading
? "bg-white/5 text-white/20"
? "bg-white/10 text-white/30"
: "bg-purple-500 text-white hover:bg-purple-400"
)}
>
{aiLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <Zap className="w-3 h-3" />}
Find Similar
{aiLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Star className="w-3.5 h-3.5" />}
</button>
</div>
</div>
{/* AI Generated Names */}
{/* AI Names */}
{aiNames.length > 0 && (
<div>
<div className="pt-3 border-t border-white/10">
<div className="flex items-center justify-between mb-2">
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider">
AI Suggestions ({aiNames.length})
</span>
</p>
<button
onClick={checkAiNames}
disabled={loading || selectedTlds.length === 0}
className="flex items-center gap-1.5 text-[10px] font-mono text-purple-400 hover:text-purple-300 transition-colors"
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">
{aiNames.map((name) => (
<span
key={name}
className="px-3 py-1.5 text-[11px] font-mono text-purple-400 bg-purple-500/10 border border-purple-500/20"
<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>
))}
@ -417,282 +392,111 @@ export function BrandableForgeTab({ showToast }: { showToast: (message: string,
</div>
)}
{/* Pattern Selection */}
{mode === 'pattern' && (
<div className="p-4 border-b border-white/[0.08]">
<div className="flex items-center gap-2 mb-3">
<Lightbulb className="w-3.5 h-3.5 text-white/30" />
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">Choose Pattern</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{PATTERNS.map((p) => {
const isActive = pattern === p.key
const colorClass = p.color === 'accent' ? 'accent' : p.color === 'blue' ? 'blue-400' : 'purple-400'
return (
<button
key={p.key}
onClick={() => setPattern(p.key)}
className={clsx(
"p-4 border text-left transition-all group",
isActive
? `border-${colorClass}/40 bg-${colorClass}/10`
: "border-white/[0.08] hover:border-white/20 bg-white/[0.02] hover:bg-white/[0.04]"
)}
{/* 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"
>
<div className="flex items-center justify-between mb-2">
<span className={clsx(
"text-sm font-bold font-mono",
isActive ? `text-${colorClass}` : "text-white/70 group-hover:text-white"
)}>
{p.label}
</span>
{isActive && (
<div className={`w-2 h-2 rounded-full bg-${colorClass}`} />
)}
</div>
<p className="text-[11px] text-white/40 mb-2">{p.desc}</p>
<div className="flex items-center gap-1.5">
{p.examples.map((ex, i) => (
<span
key={ex}
className={clsx(
"text-[10px] font-mono px-1.5 py-0.5 border",
isActive
? "text-white/60 border-white/20 bg-white/5"
: "text-white/30 border-white/10"
)}
>
{ex}
</span>
))}
</div>
</button>
)
})}
</div>
</div>
)}
{/* TLD Selection */}
<div className="p-4 border-b border-white/[0.08]">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">Select TLDs</span>
<span className="text-[10px] font-mono text-white/20">({selectedTlds.length} selected)</span>
</div>
<button
onClick={() => setSelectedTlds(selectedTlds.length === TLDS.length ? ['com'] : TLDS.map(t => t.tld))}
className="text-[10px] font-mono text-accent hover:text-white transition-colors"
>
{selectedTlds.length === TLDS.length ? 'Select .com only' : 'Select all'}
</button>
</div>
<div className="flex flex-wrap gap-2">
{TLDS.map((t) => (
<button
key={t.tld}
onClick={() => toggleTld(t.tld)}
className={clsx(
"px-3 py-2 text-[11px] font-mono uppercase border transition-all flex items-center gap-1.5",
selectedTlds.includes(t.tld)
? "border-accent bg-accent/10 text-accent"
: "border-white/[0.08] text-white/40 hover:text-white/60 hover:border-white/20"
)}
>
{t.premium && <Star className="w-3 h-3" />}
{t.label}
</button>
))}
</div>
</div>
{/* Advanced Config */}
{showConfig && (
<div className="p-4 border-b border-white/[0.08] bg-white/[0.01] animate-in fade-in slide-in-from-top-2 duration-200">
<div className="flex items-center gap-6">
<div>
<label className="block text-[10px] font-mono text-white/40 mb-1.5 uppercase tracking-wider">Results Count</label>
<div className="flex items-center gap-2">
<input
type="range"
value={limit}
onChange={(e) => setLimit(Number(e.target.value))}
min={10}
max={100}
step={10}
className="w-32 accent-accent"
/>
<span className="text-sm font-mono text-white w-8">{limit}</span>
</div>
</div>
<div className="flex-1 text-[10px] font-mono text-white/30 border-l border-white/10 pl-6">
<p>We'll check up to 400 random combinations and return the first {limit} verified available domains.</p>
</div>
</div>
</div>
)}
{/* Stats Bar */}
<div className="px-4 py-3 flex items-center justify-between bg-white/[0.01]">
<span className="text-[11px] font-mono text-white/40">
{items.length > 0 ? (
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent animate-pulse" />
{items.length} brandable domains ready
</span>
) : (
'Configure settings and click Generate'
)}
</span>
{items.length > 0 && (
<button
onClick={copyAll}
className="flex items-center gap-1.5 text-[10px] font-mono text-accent hover:text-white transition-colors"
>
<Copy className="w-3 h-3" />
Copy All
</button>
)}
</div>
</div>
{/* Error Message */}
{error && (
<div className="p-4 border border-rose-500/20 bg-rose-500/5 flex items-center gap-3">
<div className="w-8 h-8 bg-rose-500/10 border border-rose-500/20 flex items-center justify-center shrink-0">
<Zap className="w-4 h-4 text-rose-400" />
</div>
<div>
<p className="text-xs font-mono text-rose-400">{error}</p>
<button onClick={run} className="text-[10px] font-mono text-rose-400/60 hover:text-rose-400 mt-1">
Try again →
</button>
</div>
<Sparkles className="w-3.5 h-3.5" />
Upgrade
</Link>
</div>
)}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* RESULTS */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{items.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between px-1">
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">
Generated Domains
</span>
{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>
<div className="flex gap-2">
<button
onClick={run}
onClick={copyAll}
className="text-[10px] font-mono text-accent hover:text-white flex items-center gap-1"
>
<Copy className="w-3 h-3" />
Copy All
</button>
<button
onClick={() => mode === 'pattern' ? generatePattern() : checkAiNames()}
disabled={loading}
className="flex items-center gap-1.5 text-[10px] font-mono text-white/40 hover:text-accent transition-colors"
className="text-[10px] font-mono text-white/40 hover:text-white flex items-center gap-1"
>
<RefreshCw className={clsx("w-3 h-3", loading && "animate-spin")} />
Regenerate
Refresh
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
{items.map((i, idx) => (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{results.map((r, idx) => (
<div
key={i.domain}
className={clsx(
"group p-3 border bg-[#020202] hover:bg-accent/[0.03] transition-all",
"border-white/[0.06] hover:border-accent/20"
)}
key={r.domain}
className="flex items-center justify-between p-3 border border-accent/20 bg-accent/5 hover:bg-accent/10 transition-all"
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="w-8 h-8 bg-accent/10 border border-accent/20 flex items-center justify-center shrink-0 text-[10px] font-mono text-accent font-bold">
<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')}
</div>
<button
onClick={() => openAnalyze(i.domain)}
className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left"
>
{i.domain}
</button>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<span className="hidden sm:inline-flex text-[9px] font-mono font-bold text-accent bg-accent/10 px-2 py-1 border border-accent/20">
AVAIL
</span>
<button
onClick={() => copyDomain(i.domain)}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/30 hover:text-white hover:bg-white/5 transition-all"
title="Copy"
onClick={() => openAnalyze(r.domain)}
className="text-sm font-mono font-medium text-white truncate hover:text-accent"
>
{copied === i.domain ? <Check className="w-3.5 h-3.5 text-accent" /> : <Copy className="w-3.5 h-3.5" />}
{r.domain}
</button>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={() => track(i.domain)}
disabled={tracking === i.domain}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/30 hover:text-white hover:bg-white/5 transition-all"
title="Add to Watchlist"
onClick={() => copy(r.domain)}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
>
{tracking === i.domain ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
{copied === r.domain ? <Check className="w-3.5 h-3.5 text-accent" /> : <Copy className="w-3.5 h-3.5" />}
</button>
<button
onClick={() => openAnalyze(i.domain)}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/30 hover:text-accent hover:border-accent/20 hover:bg-accent/10 transition-all"
title="Analyze"
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"
>
<Shield className="w-3.5 h-3.5" />
</button>
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${encodeURIComponent(i.domain)}`}
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 uppercase flex items-center gap-1.5 hover:bg-white transition-colors"
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>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Empty State */}
{items.length === 0 && !loading && (
<div className="text-center py-16 border border-dashed border-white/[0.08] bg-white/[0.01]">
<div className="w-16 h-16 mx-auto mb-4 bg-accent/5 border border-accent/20 flex items-center justify-center">
<Wand2 className="w-8 h-8 text-accent/40" />
</div>
<h3 className="text-white/60 text-sm font-medium mb-1">Ready to forge</h3>
<p className="text-white/30 text-xs font-mono max-w-xs mx-auto">
Select a pattern and TLDs, then click "Generate" to discover available brandable domain names
{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>
<div className="mt-6 flex items-center justify-center gap-3 text-[10px] font-mono text-white/20">
<span className="flex items-center gap-1"><Zap className="w-3 h-3" /> Verified available</span>
<span></span>
<span className="flex items-center gap-1"><Shield className="w-3 h-3" /> DNS checked</span>
</div>
</div>
)}
{/* Loading State */}
{loading && items.length === 0 && (
<div className="space-y-2">
{[...Array(6)].map((_, i) => (
<div key={i} className="p-3 border border-white/[0.06] bg-[#020202] animate-pulse">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-white/10 rounded" />
<div className="h-4 w-32 bg-white/10 rounded" />
<div className="ml-auto flex gap-2">
<div className="w-8 h-8 bg-white/5 rounded" />
<div className="w-8 h-8 bg-white/5 rounded" />
<div className="w-16 h-8 bg-white/5 rounded" />
</div>
</div>
</div>
))}
</div>
)}
</div>

View File

@ -47,7 +47,8 @@ interface TldCheckResult {
}
// Popular TLDs to check when user enters only a name without extension
const POPULAR_TLDS = ['com', 'net', 'org', 'io', 'ch', 'de', 'app', 'dev', 'co', 'ai']
// Ordered by popularity/importance - most common first for faster perceived loading
const POPULAR_TLDS = ['com', 'ch', 'io', 'net', 'org', 'de', 'ai', 'co', 'app', 'dev']
// Known valid TLDs (subset for quick validation)
const KNOWN_TLDS = new Set([
@ -131,7 +132,7 @@ export function SearchTab({ showToast }: SearchTabProps) {
}
}, [])
// Check multiple TLDs for a name
// Check multiple TLDs for a name - with progressive loading and quick mode
const checkMultipleTlds = useCallback(async (name: string) => {
// Initialize results with loading state
const initialResults: TldCheckResult[] = POPULAR_TLDS.map(tld => ({
@ -142,31 +143,36 @@ export function SearchTab({ showToast }: SearchTabProps) {
}))
setTldResults(initialResults)
// Check each TLD in parallel
const results = await Promise.all(
POPULAR_TLDS.map(async (tld): Promise<TldCheckResult> => {
// Check each TLD in parallel with progressive updates (using quick=true for speed)
POPULAR_TLDS.forEach(async (tld, index) => {
const domain = `${name}.${tld}`
try {
const result = await api.checkDomain(domain)
return {
// Use quick=true for DNS-only check (much faster!)
const result = await api.checkDomain(domain, true)
setTldResults(prev => {
const updated = [...prev]
updated[index] = {
tld,
domain,
is_available: result.is_available,
loading: false,
}
return updated
})
} catch {
return {
setTldResults(prev => {
const updated = [...prev]
updated[index] = {
tld,
domain,
is_available: null,
loading: false,
error: 'Check failed',
}
return updated
})
}
})
)
setTldResults(results)
}, [])
// Search Handler
@ -303,10 +309,10 @@ export function SearchTab({ showToast }: SearchTabProps) {
{/* Stats Bar */}
<div className="flex items-center justify-between text-[10px] font-mono text-white/40">
<span>Enter a domain to check availability via RDAP/WHOIS</span>
<span>Enter a name (checks 10 TLDs) or full domain (e.g. example.com)</span>
<span className="flex items-center gap-1">
<Sparkles className="w-3 h-3" />
Instant check
RDAP/WHOIS
</span>
</div>

View File

@ -1,493 +1,252 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import clsx from 'clsx'
import {
ExternalLink,
Loader2,
Search,
Shield,
Sparkles,
Eye,
TrendingUp,
RefreshCw,
Globe,
Zap,
X,
Check,
Copy,
ShoppingCart,
Flame,
ArrowRight,
AlertCircle,
Wand2,
Sparkles,
Lock,
ChevronRight,
Globe,
X,
} from 'lucide-react'
import Link from 'next/link'
import { api } from '@/lib/api'
import { useAnalyzePanelStore } from '@/lib/analyze-store'
import { useStore } from '@/lib/store'
import Link from 'next/link'
// ============================================================================
// TYPES & CONSTANTS
// CONSTANTS
// ============================================================================
const GEO_OPTIONS = [
{ value: 'US', label: 'United States', flag: '🇺🇸' },
{ value: 'CH', label: 'Switzerland', flag: '🇨🇭' },
{ value: 'DE', label: 'Germany', flag: '🇩🇪' },
{ value: 'GB', label: 'United Kingdom', flag: '🇬🇧' },
{ value: 'FR', label: 'France', flag: '🇫🇷' },
{ value: 'CA', label: 'Canada', flag: '🇨🇦' },
{ value: 'AU', label: 'Australia', flag: '🇦🇺' },
const GEOS = [
{ code: 'US', flag: '🇺🇸', name: 'USA' },
{ code: 'DE', flag: '🇩🇪', name: 'Germany' },
{ code: 'GB', flag: '🇬🇧', name: 'UK' },
{ code: 'CH', flag: '🇨🇭', name: 'Switzerland' },
{ code: 'FR', flag: '🇫🇷', name: 'France' },
]
const POPULAR_TLDS = ['com', 'io', 'ai', 'co', 'net', 'org', 'app', 'dev']
// ============================================================================
// HELPERS
// ============================================================================
function normalizeKeyword(s: string) {
return s.trim().replace(/\s+/g, ' ')
}
const TLDS = ['com', 'io', 'ai', 'co', 'net', 'app']
// ============================================================================
// COMPONENT
// ============================================================================
export function TrendSurferTab({ showToast }: { showToast: (message: string, type?: any) => void }) {
export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?: any) => void }) {
const openAnalyze = useAnalyzePanelStore((s) => s.open)
const addDomain = useStore((s) => s.addDomain)
const subscription = useStore((s) => s.subscription)
const tier = (subscription?.tier || '').toLowerCase()
const hasAI = tier === 'trader' || tier === 'tycoon'
// Trends State
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// State
const [geo, setGeo] = useState('US')
const [trends, setTrends] = useState<Array<{ title: string; approx_traffic?: string | null; link?: string | null }>>([])
const [selected, setSelected] = useState<string>('')
const [refreshing, setRefreshing] = useState(false)
// AI Expansion State
const [loading, setLoading] = useState(true)
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'])
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 [aiAnalysis, setAiAnalysis] = useState<string>('')
// Keyword Check State
const [keywordInput, setKeywordInput] = useState('')
const [keywordFocused, setKeywordFocused] = useState(false)
const [selectedTlds, setSelectedTlds] = useState<string[]>(['com', 'io', 'ai'])
const [availability, setAvailability] = useState<Array<{ domain: string; status: string; is_available: boolean | null }>>([])
const [checking, setChecking] = useState(false)
// Typo Check State
const [brand, setBrand] = useState('')
const [brandFocused, setBrandFocused] = useState(false)
const [typos, setTypos] = useState<Array<{ domain: string; status: string }>>([])
const [typoLoading, setTypoLoading] = useState(false)
// Tracking & Copy State
const [tracking, setTracking] = useState<string | null>(null)
const [copied, setCopied] = useState<string | null>(null)
const [tracking, setTracking] = useState<string | null>(null)
// AI Keyword Expansion
const expandWithAI = useCallback(async () => {
if (!selected || !hasAI || aiLoading) return
setAiLoading(true)
setAiKeywords([])
setAiAnalysis('')
// Load trends
const loadTrends = useCallback(async () => {
setLoading(true)
try {
const [expandRes, analyzeRes] = await Promise.all([
api.expandTrendKeywords(selected, geo),
api.analyzeTrend(selected, geo),
])
setAiKeywords(expandRes.keywords || [])
setAiAnalysis(analyzeRes.analysis || '')
if (expandRes.keywords?.length) {
showToast(`AI found ${expandRes.keywords.length} related keywords!`, 'success')
}
const res = await api.getHuntTrends(geo)
setTrends(res.items || [])
} catch (e) {
showToast(e instanceof Error ? e.message : 'AI expansion failed', 'error')
showToast('Failed to load trends', 'error')
} finally {
setLoading(false)
}
}, [geo, showToast])
useEffect(() => {
loadTrends()
}, [loadTrends])
// Check availability
const checkAvailability = useCallback(async (keyword: string) => {
if (!keyword || tlds.length === 0) return
setChecking(true)
setResults([])
try {
const kw = keyword.toLowerCase().replace(/\s+/g, '')
const res = await api.huntKeywords({ keywords: [kw], tlds })
setResults(res.items.map(i => ({ domain: i.domain, available: i.status === 'available' })))
} catch (e) {
showToast('Check failed', 'error')
} finally {
setChecking(false)
}
}, [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)
}
}, [selected, geo, hasAI, aiLoading, showToast])
}, [selected, geo, hasAI, showToast])
const copyDomain = useCallback((domain: string) => {
// Actions
const copy = (domain: string) => {
navigator.clipboard.writeText(domain)
setCopied(domain)
setTimeout(() => setCopied(null), 1500)
}, [])
}
const track = useCallback(
async (domain: string) => {
const track = async (domain: string) => {
if (tracking) return
setTracking(domain)
try {
await addDomain(domain)
showToast(`Added to watchlist: ${domain}`, 'success')
showToast(`Added: ${domain}`, 'success')
} catch (e) {
showToast(e instanceof Error ? e.message : 'Failed to track domain', 'error')
showToast('Failed to track', 'error')
} finally {
setTracking(null)
}
},
[addDomain, showToast, tracking]
)
const loadTrends = useCallback(async (isRefresh = false) => {
if (isRefresh) setRefreshing(true)
setError(null)
try {
const res = await api.getHuntTrends(geo)
setTrends(res.items || [])
if (!selected && res.items?.[0]?.title) setSelected(res.items[0].title)
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
setError(msg)
setTrends([])
} finally {
if (isRefresh) setRefreshing(false)
}
}, [geo, selected])
useEffect(() => {
let cancelled = false
const run = async () => {
setLoading(true)
try {
await loadTrends()
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : String(e))
} finally {
if (!cancelled) setLoading(false)
}
}
run()
return () => { cancelled = true }
}, [loadTrends])
const currentGeo = GEOS.find(g => g.code === geo)
const availableCount = results.filter(r => r.available).length
const keyword = useMemo(() => normalizeKeyword(keywordInput || selected || ''), [keywordInput, selected])
const toggleTld = useCallback((tld: string) => {
setSelectedTlds(prev =>
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
)
}, [])
const runCheck = useCallback(async () => {
if (!keyword) return
if (selectedTlds.length === 0) {
showToast('Select at least one TLD', 'error')
return
}
setChecking(true)
try {
const kw = keyword.toLowerCase().replace(/\s+/g, '')
const res = await api.huntKeywords({ keywords: [kw], tlds: selectedTlds })
setAvailability(res.items.map((r) => ({ domain: r.domain, status: r.status, is_available: r.is_available })))
} catch (e) {
const msg = e instanceof Error ? e.message : 'Failed to check availability'
showToast(msg, 'error')
setAvailability([])
} finally {
setChecking(false)
}
}, [keyword, selectedTlds, showToast])
const runTypos = useCallback(async () => {
const b = brand.trim()
if (!b) return
setTypoLoading(true)
try {
const res = await api.huntTypos({ brand: b, tlds: ['com'], limit: 50 })
setTypos(res.items.map((i) => ({ domain: i.domain, status: i.status })))
if (res.items.length === 0) {
showToast('No available typo domains found', 'info')
}
} catch (e) {
const msg = e instanceof Error ? e.message : 'Failed to run typo check'
showToast(msg, 'error')
setTypos([])
} finally {
setTypoLoading(false)
}
}, [brand, showToast])
const availableCount = useMemo(() => availability.filter(a => a.status === 'available').length, [availability])
const currentGeo = GEO_OPTIONS.find(g => g.value === geo)
if (loading) {
return (
<div className="space-y-4">
{/* Skeleton Loader */}
<div className="border border-white/[0.08] bg-[#020202] animate-pulse">
<div className="px-4 py-4 border-b border-white/[0.08]">
<div className="h-5 w-48 bg-white/10 rounded mb-2" />
<div className="h-3 w-32 bg-white/5 rounded" />
</div>
<div className="p-4 flex flex-wrap gap-2">
{[...Array(8)].map((_, i) => (
<div key={i} className="h-10 w-24 bg-white/5 rounded" />
))}
</div>
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* TRENDING TOPICS */}
{/* HEADER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="border border-white/[0.08] bg-[#020202]">
<div className="px-4 py-4 border-b border-white/[0.08]">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-accent/20 to-accent/5 border border-accent/30 flex items-center justify-center">
<Flame className="w-5 h-5 text-accent" />
</div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h3 className="text-base font-bold text-white">Trending Now</h3>
<p className="text-[11px] font-mono text-white/40">
Real-time Google Trends {currentGeo?.flag} {currentGeo?.label}
<h2 className="text-lg font-bold text-white flex items-center gap-2">
<Flame className="w-5 h-5 text-orange-400" />
Trending Now
</h2>
<p className="text-xs text-white/40 font-mono mt-0.5">
Real-time Google Trends Domain opportunities
</p>
</div>
</div>
<div className="flex items-center gap-2">
<select
value={geo}
onChange={(e) => { setGeo(e.target.value); setSelected(''); setAvailability([]) }}
className="bg-white/[0.03] border border-white/10 px-3 py-2 text-xs font-mono text-white/70 outline-none focus:border-accent/40 cursor-pointer hover:bg-white/[0.05] transition-colors"
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"
>
{GEO_OPTIONS.map(g => (
<option key={g.value} value={g.value}>{g.flag} {g.label}</option>
{GEOS.map(g => (
<option key={g.code} value={g.code}>{g.flag} {g.name}</option>
))}
</select>
<button
onClick={() => loadTrends(true)}
disabled={refreshing}
className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/40 hover:text-white hover:bg-white/5 transition-all"
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"
>
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
<RefreshCw className={clsx("w-4 h-4", loading && "animate-spin")} />
</button>
</div>
</div>
</div>
{error ? (
<div className="p-4 flex items-center gap-3 bg-rose-500/5 border-b border-rose-500/20">
<AlertCircle className="w-4 h-4 text-rose-400 shrink-0" />
<p className="text-xs font-mono text-rose-400">{error}</p>
<button
onClick={() => loadTrends(true)}
className="ml-auto text-[10px] font-mono text-rose-400 underline hover:no-underline"
>
Retry
</button>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* 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="p-4">
<div className="flex flex-wrap gap-2">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
{trends.slice(0, 16).map((t, idx) => {
const active = selected === t.title
const isSelected = selected === t.title
const isHot = idx < 3
return (
<button
key={t.title}
onClick={() => {
setSelected(t.title)
setKeywordInput('')
setAvailability([])
setSelected(isSelected ? null : t.title)
setResults([])
setAiKeywords([])
}}
className={clsx(
'group relative px-4 py-2.5 border text-left transition-all',
active
? 'border-accent bg-accent/10'
: 'border-white/[0.08] hover:border-white/20 bg-white/[0.02] hover:bg-white/[0.04]'
"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]"
)}
>
<div className="flex items-center gap-2">
{isHot && (
<span className="text-[9px] font-bold text-orange-400 bg-orange-400/10 px-1 py-0.5">
🔥
</span>
<span className="absolute top-2 right-2 text-[10px]">🔥</span>
)}
<span className={clsx(
"text-xs font-medium truncate max-w-[140px]",
active ? "text-accent" : "text-white/70 group-hover:text-white"
<p className={clsx(
"text-sm font-medium truncate",
isSelected ? "text-accent" : "text-white/80"
)}>
{t.title}
</span>
</div>
</p>
{t.approx_traffic && (
<div className="text-[9px] text-white/30 mt-0.5 font-mono">{t.approx_traffic}</div>
<p className="text-[10px] text-white/30 font-mono mt-1">{t.approx_traffic}</p>
)}
</button>
)
})}
</div>
{trends.length === 0 && (
<div className="text-center py-6 text-white/30 text-xs font-mono">
No trends available for this region
</div>
)}
</div>
)}
{/* AI Expansion Section */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* SELECTED TREND PANEL */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{selected && (
<div className="px-4 pb-4 border-t border-white/[0.08] pt-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Wand2 className="w-4 h-4 text-purple-400" />
<span className="text-xs font-mono text-white/60">AI Keyword Expansion</span>
{!hasAI && (
<span className="text-[9px] font-mono text-white/30 bg-white/5 px-1.5 py-0.5 border border-white/10">
TRADER+
</span>
)}
</div>
{hasAI ? (
<button
onClick={expandWithAI}
disabled={aiLoading}
className={clsx(
"flex items-center gap-1.5 px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider transition-all",
aiLoading
? "bg-purple-500/20 text-purple-300 cursor-wait"
: "bg-purple-500/20 text-purple-400 hover:bg-purple-500/30 border border-purple-500/30"
)}
>
{aiLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <Sparkles className="w-3 h-3" />}
{aiLoading ? 'Expanding...' : 'Expand with AI'}
</button>
) : (
<Link
href="/pricing"
className="flex items-center gap-1.5 px-3 py-1.5 text-[10px] font-mono text-white/40 border border-white/10 hover:border-white/20 hover:text-white/60 transition-all"
>
<Lock className="w-3 h-3" />
Upgrade
</Link>
)}
</div>
{/* AI Analysis */}
{aiAnalysis && (
<div className="p-3 bg-purple-500/5 border border-purple-500/20 mb-3">
<p className="text-xs text-white/70 leading-relaxed">{aiAnalysis}</p>
</div>
)}
{/* AI Keywords */}
{aiKeywords.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{aiKeywords.map((kw) => (
<button
key={kw}
onClick={() => {
setKeywordInput(kw)
setAvailability([])
}}
className="px-3 py-1.5 text-[11px] font-mono text-purple-400 bg-purple-500/10 border border-purple-500/20 hover:bg-purple-500/20 transition-all"
>
{kw}
</button>
))}
</div>
)}
{!aiKeywords.length && !aiLoading && hasAI && (
<p className="text-[10px] font-mono text-white/30 text-center py-2">
Click "Expand with AI" to find related keywords for "{selected}"
</p>
)}
</div>
)}
</div>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* DOMAIN AVAILABILITY CHECKER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="border border-white/[0.08] bg-[#020202]">
<div className="px-4 py-4 border-b border-white/[0.08]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-white/[0.03] border border-white/[0.08] flex items-center justify-center">
<Globe className="w-5 h-5 text-white/50" />
</div>
<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>
<h3 className="text-base font-bold text-white">Check Availability</h3>
<p className="text-[11px] font-mono text-white/40">
{keyword ? `Find ${keyword.toLowerCase().replace(/\s+/g, '')} across multiple TLDs` : 'Select a trend or enter a keyword'}
</p>
<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>
</div>
</div>
</div>
<div className="p-4 space-y-4">
{/* Keyword Input */}
<div className="flex gap-2">
<div className={clsx(
"flex-1 relative border-2 transition-all",
keywordFocused ? "border-accent bg-accent/[0.03]" : "border-white/[0.08] bg-white/[0.02]"
)}>
<div className="flex items-center">
<Search className={clsx("w-4 h-4 ml-4 transition-colors", keywordFocused ? "text-accent" : "text-white/30")} />
<input
value={keywordInput || selected}
onChange={(e) => setKeywordInput(e.target.value)}
onFocus={() => setKeywordFocused(true)}
onBlur={() => setKeywordFocused(false)}
onKeyDown={(e) => e.key === 'Enter' && runCheck()}
placeholder="Enter keyword or select trend above..."
className="flex-1 bg-transparent px-3 py-3.5 text-sm text-white placeholder:text-white/25 outline-none font-mono"
/>
{(keywordInput || selected) && (
<button
onClick={() => { setKeywordInput(''); setSelected(''); setAvailability([]) }}
className="p-3 text-white/30 hover:text-white transition-colors"
onClick={() => { setSelected(null); setResults([]); setAiKeywords([]) }}
className="p-2 text-white/40 hover:text-white"
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
<button
onClick={runCheck}
disabled={!keyword || checking}
className={clsx(
"px-6 py-3 text-sm font-bold uppercase tracking-wider transition-all flex items-center gap-2",
!keyword || checking
? "bg-white/5 text-white/20 cursor-not-allowed"
: "bg-accent text-black hover:bg-white"
)}
>
{checking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
Check
</button>
</div>
{/* TLD Selection */}
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">Select TLDs</span>
<span className="text-[10px] font-mono text-white/20">({selectedTlds.length} selected)</span>
</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">
{POPULAR_TLDS.map(tld => (
{TLDS.map(tld => (
<button
key={tld}
onClick={() => toggleTld(tld)}
onClick={() => setTlds(prev =>
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
)}
className={clsx(
"px-3 py-1.5 text-[11px] font-mono uppercase border transition-all",
selectedTlds.includes(tld)
? "border-accent bg-accent/10 text-accent"
: "border-white/[0.08] text-white/40 hover:text-white/60 hover:border-white/20"
"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}
@ -496,222 +255,138 @@ export function TrendSurferTab({ showToast }: { showToast: (message: string, typ
</div>
</div>
{/* Results */}
{availability.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">
Results {availableCount} available
</span>
</div>
<div className="space-y-1">
{availability.map((a) => {
const isAvailable = a.status === 'available'
return (
<div
key={a.domain}
{/* Check Button */}
<button
onClick={() => checkAvailability(selected)}
disabled={checking || tlds.length === 0}
className={clsx(
"p-3 flex items-center justify-between gap-3 border transition-all",
isAvailable
? "bg-accent/[0.03] border-accent/20 hover:bg-accent/[0.06]"
: "bg-white/[0.02] border-white/[0.06] hover:bg-white/[0.04]"
"w-full py-3 text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all",
checking || tlds.length === 0
? "bg-white/10 text-white/30"
: "bg-accent text-black hover:bg-white"
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className={clsx(
"w-2.5 h-2.5 rounded-full shrink-0",
isAvailable ? "bg-accent" : "bg-white/20"
{checking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
Check {selected.toLowerCase().replace(/\s+/g, '')} Availability
</button>
{/* 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" />}
</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(a.domain)}
onClick={() => openAnalyze(r.domain)}
className={clsx(
"text-sm font-mono truncate text-left transition-colors",
isAvailable ? "text-white hover:text-accent" : "text-white/50"
"text-sm font-mono truncate",
r.available ? "text-white hover:text-accent" : "text-white/40"
)}
>
{a.domain}
{r.domain}
</button>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className={clsx(
"text-[10px] font-mono font-bold px-2 py-1 border",
isAvailable
? "text-accent bg-accent/10 border-accent/30"
: "text-white/30 bg-white/5 border-white/10"
)}>
{isAvailable ? '✓ AVAIL' : 'TAKEN'}
</span>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={() => copyDomain(a.domain)}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/30 hover:text-white hover:bg-white/5 transition-all"
title="Copy"
onClick={() => copy(r.domain)}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
>
{copied === a.domain ? <Check className="w-3.5 h-3.5 text-accent" /> : <Copy className="w-3.5 h-3.5" />}
{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(a.domain)}
disabled={tracking === a.domain}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/30 hover:text-white hover:bg-white/5 transition-all"
title="Add to Watchlist"
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 === a.domain ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
{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(a.domain)}
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/30 hover:text-accent hover:border-accent/20 hover:bg-accent/10 transition-all"
title="Analyze"
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>
{isAvailable && (
{r.available && (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${a.domain}`}
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 uppercase flex items-center gap-1.5 hover:bg-white transition-colors"
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" />
Buy
<span className="hidden sm:inline">Buy</span>
</a>
)}
</div>
</div>
)
})}
</div>
</div>
)}
{/* Empty State */}
{availability.length === 0 && keyword && !checking && (
<div className="text-center py-10 border border-dashed border-white/[0.08] bg-white/[0.01]">
<Zap className="w-8 h-8 text-white/10 mx-auto mb-3" />
<p className="text-white/40 text-sm font-mono mb-1">Ready to check</p>
<p className="text-white/25 text-xs font-mono">
Click "Check" to find available domains for <span className="text-accent">{keyword.toLowerCase().replace(/\s+/g, '')}</span>
</p>
</div>
)}
</div>
</div>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* TYPO FINDER */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="border border-white/[0.08] bg-[#020202]">
<div className="px-4 py-4 border-b border-white/[0.08]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-500/10 border border-purple-500/20 flex items-center justify-center">
<Sparkles className="w-5 h-5 text-purple-400" />
</div>
<div>
<h3 className="text-base font-bold text-white">Typo Finder</h3>
<p className="text-[11px] font-mono text-white/40">
Find available misspellings of popular brands
</p>
</div>
</div>
</div>
<div className="p-4 space-y-4">
<div className="flex gap-2">
<div className={clsx(
"flex-1 relative border-2 transition-all",
brandFocused ? "border-purple-400/50 bg-purple-400/[0.03]" : "border-white/[0.08] bg-white/[0.02]"
)}>
<div className="flex items-center">
<Sparkles className={clsx("w-4 h-4 ml-4 transition-colors", brandFocused ? "text-purple-400" : "text-white/30")} />
<input
value={brand}
onChange={(e) => setBrand(e.target.value)}
onFocus={() => setBrandFocused(true)}
onBlur={() => setBrandFocused(false)}
onKeyDown={(e) => e.key === 'Enter' && runTypos()}
placeholder="Enter a brand name (e.g. Google, Amazon, Shopify)..."
className="flex-1 bg-transparent px-3 py-3.5 text-sm text-white placeholder:text-white/25 outline-none font-mono"
/>
{brand && (
<button onClick={() => { setBrand(''); setTypos([]) }} className="p-3 text-white/30 hover:text-white">
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
<button
onClick={runTypos}
disabled={!brand.trim() || typoLoading}
className={clsx(
"px-6 py-3 text-sm font-bold uppercase tracking-wider transition-all flex items-center gap-2",
!brand.trim() || typoLoading
? "bg-white/5 text-white/20 cursor-not-allowed"
: "bg-purple-500 text-white hover:bg-purple-400"
)}
>
{typoLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <ArrowRight className="w-4 h-4" />}
Find
</button>
</div>
{/* Typo Results */}
{typos.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{typos.map((t) => (
<div
key={t.domain}
className="group border border-white/[0.08] bg-white/[0.02] px-3 py-2.5 flex items-center justify-between hover:border-purple-400/30 hover:bg-purple-400/[0.03] transition-all"
>
<button
onClick={() => openAnalyze(t.domain)}
className="text-xs font-mono text-white/70 group-hover:text-purple-400 truncate text-left transition-colors"
>
{t.domain}
</button>
<div className="flex items-center gap-1.5 shrink-0 ml-2">
<button
onClick={() => copyDomain(t.domain)}
className="w-6 h-6 flex items-center justify-center text-white/30 hover:text-white transition-colors"
title="Copy"
>
{copied === t.domain ? <Check className="w-3 h-3 text-accent" /> : <Copy className="w-3 h-3" />}
</button>
<button
onClick={() => track(t.domain)}
disabled={tracking === t.domain}
className="w-6 h-6 flex items-center justify-center text-white/30 hover:text-white transition-colors"
title="Track"
>
{tracking === t.domain ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />}
</button>
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${t.domain}`}
target="_blank"
rel="noopener noreferrer"
className="w-6 h-6 flex items-center justify-center text-white/30 hover:text-accent transition-colors"
title="Buy"
>
<ExternalLink className="w-3 h-3" />
</a>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Empty State */}
{typos.length === 0 && !typoLoading && (
<div className="text-center py-8 border border-dashed border-white/[0.08] bg-white/[0.01]">
<p className="text-white/30 text-xs font-mono">
Enter a brand name to discover available typo domains
</p>
{!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>
)}
</div>
</div>
</div>
)
}