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
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:
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user