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 { useCallback, useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
ExternalLink,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
Shield,
|
Shield,
|
||||||
Sparkles,
|
|
||||||
Eye,
|
Eye,
|
||||||
Wand2,
|
Wand2,
|
||||||
Settings,
|
|
||||||
Zap,
|
Zap,
|
||||||
Copy,
|
Copy,
|
||||||
Check,
|
Check,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Star,
|
Sparkles,
|
||||||
Lightbulb,
|
|
||||||
RefreshCw,
|
|
||||||
Lock,
|
Lock,
|
||||||
|
Star,
|
||||||
Brain,
|
Brain,
|
||||||
MessageSquare,
|
RefreshCw,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
@ -31,383 +27,362 @@ import { useStore } from '@/lib/store'
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const PATTERNS = [
|
const PATTERNS = [
|
||||||
{
|
{ key: 'cvcvc', label: 'CVCVC', examples: ['Zalor', 'Mivex', 'Ronix'], desc: 'Classic 5-letter' },
|
||||||
key: 'cvcvc',
|
{ key: 'cvccv', label: 'CVCCV', examples: ['Bento', 'Salvo'], desc: 'Punchy sound' },
|
||||||
label: 'CVCVC',
|
{ key: 'human', label: 'Human', examples: ['Siri', 'Alexa', 'Levi'], desc: 'AI agent names' },
|
||||||
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'
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const TLDS = [
|
const TLDS = ['com', 'io', 'ai', 'co', 'net', 'app']
|
||||||
{ 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' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// COMPONENT
|
// 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 openAnalyze = useAnalyzePanelStore((s) => s.open)
|
||||||
const addDomain = useStore((s) => s.addDomain)
|
const addDomain = useStore((s) => s.addDomain)
|
||||||
const subscription = useStore((s) => s.subscription)
|
const subscription = useStore((s) => s.subscription)
|
||||||
const tier = (subscription?.tier || '').toLowerCase()
|
const tier = (subscription?.tier || '').toLowerCase()
|
||||||
const hasAI = tier === 'trader' || tier === 'tycoon'
|
const hasAI = tier === 'trader' || tier === 'tycoon'
|
||||||
|
|
||||||
// Mode: 'pattern' (classic) or 'ai' (concept-based)
|
// Mode
|
||||||
const [mode, setMode] = useState<'pattern' | 'ai'>('pattern')
|
const [mode, setMode] = useState<'pattern' | 'ai'>('pattern')
|
||||||
|
|
||||||
// Config State
|
// Pattern Mode
|
||||||
const [pattern, setPattern] = useState('cvcvc')
|
const [pattern, setPattern] = useState('cvcvc')
|
||||||
const [selectedTlds, setSelectedTlds] = useState<string[]>(['com', 'io'])
|
const [tlds, setTlds] = useState(['com', 'io'])
|
||||||
const [limit, setLimit] = useState(30)
|
|
||||||
const [showConfig, setShowConfig] = useState(false)
|
|
||||||
|
|
||||||
// AI State
|
// AI Mode
|
||||||
const [concept, setConcept] = useState('')
|
const [concept, setConcept] = useState('')
|
||||||
const [conceptFocused, setConceptFocused] = useState(false)
|
|
||||||
const [similarBrand, setSimilarBrand] = useState('')
|
const [similarBrand, setSimilarBrand] = useState('')
|
||||||
const [similarFocused, setSimilarFocused] = useState(false)
|
|
||||||
const [aiNames, setAiNames] = useState<string[]>([])
|
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 [loading, setLoading] = useState(false)
|
||||||
const [items, setItems] = useState<Array<{ domain: string; status: string }>>([])
|
const [aiLoading, setAiLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [tracking, setTracking] = useState<string | null>(null)
|
|
||||||
const [copied, setCopied] = useState<string | null>(null)
|
const [copied, setCopied] = useState<string | null>(null)
|
||||||
|
const [tracking, setTracking] = useState<string | null>(null)
|
||||||
|
|
||||||
const toggleTld = useCallback((tld: string) => {
|
// Generate Pattern-based
|
||||||
setSelectedTlds((prev) =>
|
const generatePattern = useCallback(async () => {
|
||||||
prev.includes(tld) ? prev.filter((t) => t !== tld) : [...prev, tld]
|
if (tlds.length === 0) {
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
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) {
|
|
||||||
showToast('Select at least one TLD', 'error')
|
showToast('Select at least one TLD', 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setResults([])
|
||||||
setItems([])
|
|
||||||
try {
|
try {
|
||||||
const res = await api.huntBrandables({ pattern, tlds: selectedTlds, limit, max_checks: 400 })
|
const res = await api.huntBrandables({ pattern, tlds, limit: 30, max_checks: 400 })
|
||||||
setItems(res.items.map((i) => ({ domain: i.domain, status: i.status })))
|
setResults(res.items.map(i => ({ domain: i.domain, available: true })))
|
||||||
if (res.items.length === 0) {
|
showToast(`Found ${res.items.length} brandable domains!`, 'success')
|
||||||
showToast('No available domains found. Try different settings.', 'info')
|
|
||||||
} else {
|
|
||||||
showToast(`Found ${res.items.length} available brandable domains!`, 'success')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
showToast('Generation failed', 'error')
|
||||||
setError(msg)
|
|
||||||
showToast(msg, 'error')
|
|
||||||
setItems([])
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [pattern, selectedTlds, limit, showToast])
|
}, [pattern, tlds, showToast])
|
||||||
|
|
||||||
const track = useCallback(
|
// Generate AI-based
|
||||||
async (domain: string) => {
|
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
|
if (tracking) return
|
||||||
setTracking(domain)
|
setTracking(domain)
|
||||||
try {
|
try {
|
||||||
await addDomain(domain)
|
await addDomain(domain)
|
||||||
showToast(`Added to watchlist: ${domain}`, 'success')
|
showToast(`Added: ${domain}`, 'success')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e instanceof Error ? e.message : 'Failed to track domain', 'error')
|
showToast('Failed', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
setTracking(null)
|
setTracking(null)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
[addDomain, showToast, tracking]
|
|
||||||
)
|
|
||||||
|
|
||||||
const currentPattern = PATTERNS.find(p => p.key === pattern)
|
const copyAll = () => {
|
||||||
|
if (results.length === 0) return
|
||||||
// AI Generation
|
navigator.clipboard.writeText(results.map(r => r.domain).join('\n'))
|
||||||
const generateFromConcept = useCallback(async () => {
|
showToast(`Copied ${results.length} domains`, 'success')
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
} 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 (
|
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]">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
{/* 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>
|
<div>
|
||||||
<h3 className="text-base font-bold text-white">Brandable Forge</h3>
|
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
||||||
<p className="text-[11px] font-mono text-white/40">
|
<Wand2 className="w-5 h-5 text-purple-400" />
|
||||||
{mode === 'pattern' ? 'Pattern-based name generator' : 'AI-powered concept generator'}
|
Brandable Forge
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-white/40 font-mono mt-0.5">
|
||||||
|
Generate unique, memorable domain names
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowConfig(!showConfig)}
|
onClick={() => { setMode('pattern'); setResults([]); setAiNames([]) }}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-9 h-9 flex items-center justify-center border transition-all",
|
"px-4 py-2 text-xs font-bold uppercase tracking-wider border-y border-l 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",
|
|
||||||
mode === 'pattern'
|
mode === 'pattern'
|
||||||
? "border-accent bg-accent/10 text-accent"
|
? "bg-accent/10 border-accent text-accent"
|
||||||
: "border-white/10 text-white/40 hover:text-white hover:border-white/20"
|
: "bg-white/5 border-white/10 text-white/50 hover:text-white"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Lightbulb className="w-3.5 h-3.5" />
|
|
||||||
Patterns
|
Patterns
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => hasAI && setMode('ai')}
|
onClick={() => { if (hasAI) { setMode('ai'); setResults([]); setAiNames([]) }}}
|
||||||
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"
|
|
||||||
)}
|
|
||||||
disabled={!hasAI}
|
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" />
|
<Brain className="w-3.5 h-3.5" />
|
||||||
AI Concept
|
AI
|
||||||
{!hasAI && <Lock className="w-3 h-3 ml-1" />}
|
{!hasAI && <Lock className="w-3 h-3" />}
|
||||||
</button>
|
</button>
|
||||||
{!hasAI && (
|
|
||||||
<Link href="/pricing" className="ml-auto text-[10px] font-mono text-accent hover:underline">
|
|
||||||
Upgrade for AI
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</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 && (
|
{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 */}
|
{/* Concept Input */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] font-mono text-white/40 mb-2 uppercase tracking-wider">
|
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Describe Your Brand</p>
|
||||||
Describe your brand concept
|
<div className="flex gap-2">
|
||||||
</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")} />
|
|
||||||
<input
|
<input
|
||||||
value={concept}
|
value={concept}
|
||||||
onChange={(e) => setConcept(e.target.value)}
|
onChange={(e) => setConcept(e.target.value)}
|
||||||
onFocus={() => setConceptFocused(true)}
|
|
||||||
onBlur={() => setConceptFocused(false)}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && generateFromConcept()}
|
onKeyDown={(e) => e.key === 'Enter' && generateFromConcept()}
|
||||||
placeholder="e.g., AI startup for legal documents, crypto wallet for teens..."
|
placeholder="e.g., AI startup for legal docs..."
|
||||||
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
|
<button
|
||||||
onClick={generateFromConcept}
|
onClick={generateFromConcept}
|
||||||
disabled={!concept.trim() || aiLoading}
|
disabled={!concept.trim() || aiLoading}
|
||||||
className={clsx(
|
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
|
!concept.trim() || aiLoading
|
||||||
? "bg-white/5 text-white/20"
|
? "bg-white/10 text-white/30"
|
||||||
: "bg-purple-500 text-white hover:bg-purple-400"
|
: "bg-purple-500 text-white hover:bg-purple-400"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{aiLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <Sparkles className="w-3 h-3" />}
|
{aiLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Zap className="w-3.5 h-3.5" />}
|
||||||
Generate
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Similar Brand Input */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[10px] font-mono text-white/40 mb-2 uppercase tracking-wider">
|
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Find Names Like...</p>
|
||||||
Or find names similar to a brand
|
<div className="flex gap-2">
|
||||||
</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")} />
|
|
||||||
<input
|
<input
|
||||||
value={similarBrand}
|
value={similarBrand}
|
||||||
onChange={(e) => setSimilarBrand(e.target.value)}
|
onChange={(e) => setSimilarBrand(e.target.value)}
|
||||||
onFocus={() => setSimilarFocused(true)}
|
|
||||||
onBlur={() => setSimilarFocused(false)}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && generateFromBrand()}
|
onKeyDown={(e) => e.key === 'Enter' && generateFromBrand()}
|
||||||
placeholder="e.g., Stripe, Notion, Figma..."
|
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
|
<button
|
||||||
onClick={generateFromBrand}
|
onClick={generateFromBrand}
|
||||||
disabled={!similarBrand.trim() || aiLoading}
|
disabled={!similarBrand.trim() || aiLoading}
|
||||||
className={clsx(
|
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
|
!similarBrand.trim() || aiLoading
|
||||||
? "bg-white/5 text-white/20"
|
? "bg-white/10 text-white/30"
|
||||||
: "bg-purple-500 text-white hover:bg-purple-400"
|
: "bg-purple-500 text-white hover:bg-purple-400"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{aiLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <Zap className="w-3 h-3" />}
|
{aiLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Star className="w-3.5 h-3.5" />}
|
||||||
Find Similar
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AI Generated Names */}
|
{/* AI Names */}
|
||||||
{aiNames.length > 0 && (
|
{aiNames.length > 0 && (
|
||||||
<div>
|
<div className="pt-3 border-t border-white/10">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<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})
|
AI Suggestions ({aiNames.length})
|
||||||
</span>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={checkAiNames}
|
onClick={checkAiNames}
|
||||||
disabled={loading || selectedTlds.length === 0}
|
disabled={loading || tlds.length === 0}
|
||||||
className="flex items-center gap-1.5 text-[10px] font-mono text-purple-400 hover:text-purple-300 transition-colors"
|
className="text-[10px] font-mono text-purple-400 hover:text-purple-300 flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<Zap className="w-3 h-3" />
|
<Zap className="w-3 h-3" />
|
||||||
Check Availability
|
Check Availability
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||||
{aiNames.map((name) => (
|
{TLDS.map(tld => (
|
||||||
<span
|
<button
|
||||||
key={name}
|
key={tld}
|
||||||
className="px-3 py-1.5 text-[11px] font-mono text-purple-400 bg-purple-500/10 border border-purple-500/20"
|
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}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -417,282 +392,111 @@ export function BrandableForgeTab({ showToast }: { showToast: (message: string,
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pattern Selection */}
|
{/* Upgrade CTA for Scout */}
|
||||||
{mode === 'pattern' && (
|
{mode === 'ai' && !hasAI && (
|
||||||
<div className="p-4 border-b border-white/[0.08]">
|
<div className="border border-white/10 bg-white/[0.02] p-6 text-center">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<Lock className="w-8 h-8 text-white/20 mx-auto mb-2" />
|
||||||
<Lightbulb className="w-3.5 h-3.5 text-white/30" />
|
<p className="text-sm text-white/60 mb-3">AI features require Trader or Tycoon</p>
|
||||||
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">Choose Pattern</span>
|
<Link
|
||||||
</div>
|
href="/pricing"
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
className="inline-flex items-center gap-2 px-5 py-2.5 bg-accent text-black text-xs font-bold uppercase"
|
||||||
{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]"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<Sparkles className="w-3.5 h-3.5" />
|
||||||
<span className={clsx(
|
Upgrade
|
||||||
"text-sm font-bold font-mono",
|
</Link>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
{/* RESULTS */}
|
{/* RESULTS */}
|
||||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
{items.length > 0 && (
|
{results.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between px-1">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[10px] font-mono text-white/40 uppercase tracking-wider">
|
<p className="text-xs text-white/50 font-mono">
|
||||||
Generated Domains
|
{results.length} available domains
|
||||||
</span>
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
<button
|
<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}
|
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")} />
|
<RefreshCw className={clsx("w-3 h-3", loading && "animate-spin")} />
|
||||||
Regenerate
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
{items.map((i, idx) => (
|
{results.map((r, idx) => (
|
||||||
<div
|
<div
|
||||||
key={i.domain}
|
key={r.domain}
|
||||||
className={clsx(
|
className="flex items-center justify-between p-3 border border-accent/20 bg-accent/5 hover:bg-accent/10 transition-all"
|
||||||
"group p-3 border bg-[#020202] hover:bg-accent/[0.03] transition-all",
|
|
||||||
"border-white/[0.06] hover:border-accent/20"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center gap-2.5 min-w-0">
|
||||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
<span className="w-6 h-6 bg-accent/20 text-accent text-[10px] font-bold font-mono flex items-center justify-center shrink-0">
|
||||||
<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">
|
|
||||||
{String(idx + 1).padStart(2, '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>
|
</span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => copyDomain(i.domain)}
|
onClick={() => openAnalyze(r.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"
|
className="text-sm font-mono font-medium text-white truncate hover:text-accent"
|
||||||
title="Copy"
|
|
||||||
>
|
>
|
||||||
{copied === i.domain ? <Check className="w-3.5 h-3.5 text-accent" /> : <Copy className="w-3.5 h-3.5" />}
|
{r.domain}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => track(i.domain)}
|
onClick={() => copy(r.domain)}
|
||||||
disabled={tracking === i.domain}
|
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => openAnalyze(i.domain)}
|
onClick={() => track(r.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"
|
disabled={tracking === r.domain}
|
||||||
title="Analyze"
|
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" />
|
<Shield className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a
|
<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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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" />
|
<ShoppingCart className="w-3 h-3" />
|
||||||
<span className="hidden sm:inline">Buy</span>
|
<span className="hidden sm:inline">Buy</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty State */}
|
{/* Empty State */}
|
||||||
{items.length === 0 && !loading && (
|
{results.length === 0 && !loading && !aiLoading && (
|
||||||
<div className="text-center py-16 border border-dashed border-white/[0.08] bg-white/[0.01]">
|
<div className="text-center py-10 border border-dashed border-white/10">
|
||||||
<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-white/20 mx-auto mb-2" />
|
||||||
<Wand2 className="w-8 h-8 text-accent/40" />
|
<p className="text-sm text-white/40">
|
||||||
</div>
|
{mode === 'pattern'
|
||||||
<h3 className="text-white/60 text-sm font-medium mb-1">Ready to forge</h3>
|
? 'Select a pattern and click Generate'
|
||||||
<p className="text-white/30 text-xs font-mono max-w-xs mx-auto">
|
: 'Describe your concept or enter a brand name'}
|
||||||
Select a pattern and TLDs, then click "Generate" to discover available brandable domain names
|
|
||||||
</p>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -47,7 +47,8 @@ interface TldCheckResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Popular TLDs to check when user enters only a name without extension
|
// 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)
|
// Known valid TLDs (subset for quick validation)
|
||||||
const KNOWN_TLDS = new Set([
|
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) => {
|
const checkMultipleTlds = useCallback(async (name: string) => {
|
||||||
// Initialize results with loading state
|
// Initialize results with loading state
|
||||||
const initialResults: TldCheckResult[] = POPULAR_TLDS.map(tld => ({
|
const initialResults: TldCheckResult[] = POPULAR_TLDS.map(tld => ({
|
||||||
@ -142,31 +143,36 @@ export function SearchTab({ showToast }: SearchTabProps) {
|
|||||||
}))
|
}))
|
||||||
setTldResults(initialResults)
|
setTldResults(initialResults)
|
||||||
|
|
||||||
// Check each TLD in parallel
|
// Check each TLD in parallel with progressive updates (using quick=true for speed)
|
||||||
const results = await Promise.all(
|
POPULAR_TLDS.forEach(async (tld, index) => {
|
||||||
POPULAR_TLDS.map(async (tld): Promise<TldCheckResult> => {
|
|
||||||
const domain = `${name}.${tld}`
|
const domain = `${name}.${tld}`
|
||||||
try {
|
try {
|
||||||
const result = await api.checkDomain(domain)
|
// Use quick=true for DNS-only check (much faster!)
|
||||||
return {
|
const result = await api.checkDomain(domain, true)
|
||||||
|
setTldResults(prev => {
|
||||||
|
const updated = [...prev]
|
||||||
|
updated[index] = {
|
||||||
tld,
|
tld,
|
||||||
domain,
|
domain,
|
||||||
is_available: result.is_available,
|
is_available: result.is_available,
|
||||||
loading: false,
|
loading: false,
|
||||||
}
|
}
|
||||||
|
return updated
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
setTldResults(prev => {
|
||||||
|
const updated = [...prev]
|
||||||
|
updated[index] = {
|
||||||
tld,
|
tld,
|
||||||
domain,
|
domain,
|
||||||
is_available: null,
|
is_available: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: 'Check failed',
|
error: 'Check failed',
|
||||||
}
|
}
|
||||||
|
return updated
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
|
||||||
setTldResults(results)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Search Handler
|
// Search Handler
|
||||||
@ -303,10 +309,10 @@ export function SearchTab({ showToast }: SearchTabProps) {
|
|||||||
|
|
||||||
{/* Stats Bar */}
|
{/* Stats Bar */}
|
||||||
<div className="flex items-center justify-between text-[10px] font-mono text-white/40">
|
<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">
|
<span className="flex items-center gap-1">
|
||||||
<Sparkles className="w-3 h-3" />
|
<Sparkles className="w-3 h-3" />
|
||||||
Instant check
|
RDAP/WHOIS
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,493 +1,252 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
ExternalLink,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
Search,
|
|
||||||
Shield,
|
Shield,
|
||||||
Sparkles,
|
|
||||||
Eye,
|
Eye,
|
||||||
TrendingUp,
|
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Globe,
|
|
||||||
Zap,
|
Zap,
|
||||||
X,
|
|
||||||
Check,
|
Check,
|
||||||
Copy,
|
Copy,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Flame,
|
Flame,
|
||||||
ArrowRight,
|
Sparkles,
|
||||||
AlertCircle,
|
|
||||||
Wand2,
|
|
||||||
Lock,
|
Lock,
|
||||||
|
ChevronRight,
|
||||||
|
Globe,
|
||||||
|
X,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { useAnalyzePanelStore } from '@/lib/analyze-store'
|
import { useAnalyzePanelStore } from '@/lib/analyze-store'
|
||||||
import { useStore } from '@/lib/store'
|
import { useStore } from '@/lib/store'
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TYPES & CONSTANTS
|
// CONSTANTS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const GEO_OPTIONS = [
|
const GEOS = [
|
||||||
{ value: 'US', label: 'United States', flag: '🇺🇸' },
|
{ code: 'US', flag: '🇺🇸', name: 'USA' },
|
||||||
{ value: 'CH', label: 'Switzerland', flag: '🇨🇭' },
|
{ code: 'DE', flag: '🇩🇪', name: 'Germany' },
|
||||||
{ value: 'DE', label: 'Germany', flag: '🇩🇪' },
|
{ code: 'GB', flag: '🇬🇧', name: 'UK' },
|
||||||
{ value: 'GB', label: 'United Kingdom', flag: '🇬🇧' },
|
{ code: 'CH', flag: '🇨🇭', name: 'Switzerland' },
|
||||||
{ value: 'FR', label: 'France', flag: '🇫🇷' },
|
{ code: 'FR', flag: '🇫🇷', name: 'France' },
|
||||||
{ value: 'CA', label: 'Canada', flag: '🇨🇦' },
|
|
||||||
{ value: 'AU', label: 'Australia', flag: '🇦🇺' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const POPULAR_TLDS = ['com', 'io', 'ai', 'co', 'net', 'org', 'app', 'dev']
|
const TLDS = ['com', 'io', 'ai', 'co', 'net', 'app']
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function normalizeKeyword(s: string) {
|
|
||||||
return s.trim().replace(/\s+/g, ' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// COMPONENT
|
// 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 openAnalyze = useAnalyzePanelStore((s) => s.open)
|
||||||
const addDomain = useStore((s) => s.addDomain)
|
const addDomain = useStore((s) => s.addDomain)
|
||||||
const subscription = useStore((s) => s.subscription)
|
const subscription = useStore((s) => s.subscription)
|
||||||
const tier = (subscription?.tier || '').toLowerCase()
|
const tier = (subscription?.tier || '').toLowerCase()
|
||||||
const hasAI = tier === 'trader' || tier === 'tycoon'
|
const hasAI = tier === 'trader' || tier === 'tycoon'
|
||||||
|
|
||||||
// Trends State
|
// State
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [geo, setGeo] = useState('US')
|
const [geo, setGeo] = useState('US')
|
||||||
const [trends, setTrends] = useState<Array<{ title: string; approx_traffic?: string | null; link?: string | null }>>([])
|
const [loading, setLoading] = useState(true)
|
||||||
const [selected, setSelected] = useState<string>('')
|
const [trends, setTrends] = useState<Array<{ title: string; approx_traffic?: string | null }>>([])
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [selected, setSelected] = useState<string | null>(null)
|
||||||
|
const [tlds, setTlds] = useState(['com', 'io', 'ai'])
|
||||||
// AI Expansion State
|
const [results, setResults] = useState<Array<{ domain: string; available: boolean }>>([])
|
||||||
|
const [checking, setChecking] = useState(false)
|
||||||
const [aiKeywords, setAiKeywords] = useState<string[]>([])
|
const [aiKeywords, setAiKeywords] = useState<string[]>([])
|
||||||
const [aiLoading, setAiLoading] = useState(false)
|
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 [copied, setCopied] = useState<string | null>(null)
|
||||||
|
const [tracking, setTracking] = useState<string | null>(null)
|
||||||
|
|
||||||
// AI Keyword Expansion
|
// Load trends
|
||||||
const expandWithAI = useCallback(async () => {
|
const loadTrends = useCallback(async () => {
|
||||||
if (!selected || !hasAI || aiLoading) return
|
setLoading(true)
|
||||||
setAiLoading(true)
|
|
||||||
setAiKeywords([])
|
|
||||||
setAiAnalysis('')
|
|
||||||
try {
|
try {
|
||||||
const [expandRes, analyzeRes] = await Promise.all([
|
const res = await api.getHuntTrends(geo)
|
||||||
api.expandTrendKeywords(selected, geo),
|
setTrends(res.items || [])
|
||||||
api.analyzeTrend(selected, geo),
|
|
||||||
])
|
|
||||||
setAiKeywords(expandRes.keywords || [])
|
|
||||||
setAiAnalysis(analyzeRes.analysis || '')
|
|
||||||
if (expandRes.keywords?.length) {
|
|
||||||
showToast(`AI found ${expandRes.keywords.length} related keywords!`, 'success')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
setAiLoading(false)
|
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)
|
navigator.clipboard.writeText(domain)
|
||||||
setCopied(domain)
|
setCopied(domain)
|
||||||
setTimeout(() => setCopied(null), 1500)
|
setTimeout(() => setCopied(null), 1500)
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
const track = useCallback(
|
const track = async (domain: string) => {
|
||||||
async (domain: string) => {
|
|
||||||
if (tracking) return
|
if (tracking) return
|
||||||
setTracking(domain)
|
setTracking(domain)
|
||||||
try {
|
try {
|
||||||
await addDomain(domain)
|
await addDomain(domain)
|
||||||
showToast(`Added to watchlist: ${domain}`, 'success')
|
showToast(`Added: ${domain}`, 'success')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e instanceof Error ? e.message : 'Failed to track domain', 'error')
|
showToast('Failed to track', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
setTracking(null)
|
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(() => {
|
const currentGeo = GEOS.find(g => g.code === geo)
|
||||||
let cancelled = false
|
const availableCount = results.filter(r => r.available).length
|
||||||
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 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 (
|
return (
|
||||||
<div className="space-y-4">
|
<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="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
<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>
|
<div>
|
||||||
<h3 className="text-base font-bold text-white">Trending Now</h3>
|
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
||||||
<p className="text-[11px] font-mono text-white/40">
|
<Flame className="w-5 h-5 text-orange-400" />
|
||||||
Real-time Google Trends • {currentGeo?.flag} {currentGeo?.label}
|
Trending Now
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-white/40 font-mono mt-0.5">
|
||||||
|
Real-time Google Trends → Domain opportunities
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
value={geo}
|
value={geo}
|
||||||
onChange={(e) => { setGeo(e.target.value); setSelected(''); setAvailability([]) }}
|
onChange={(e) => { setGeo(e.target.value); setSelected(null); setResults([]); setAiKeywords([]) }}
|
||||||
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"
|
className="h-10 px-3 bg-white/5 border border-white/10 text-sm font-mono text-white outline-none"
|
||||||
>
|
>
|
||||||
{GEO_OPTIONS.map(g => (
|
{GEOS.map(g => (
|
||||||
<option key={g.value} value={g.value}>{g.flag} {g.label}</option>
|
<option key={g.code} value={g.code}>{g.flag} {g.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={() => loadTrends(true)}
|
onClick={loadTrends}
|
||||||
disabled={refreshing}
|
disabled={loading}
|
||||||
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{error ? (
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
<div className="p-4 flex items-center gap-3 bg-rose-500/5 border-b border-rose-500/20">
|
{/* TRENDS GRID */}
|
||||||
<AlertCircle className="w-4 h-4 text-rose-400 shrink-0" />
|
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||||
<p className="text-xs font-mono text-rose-400">{error}</p>
|
{loading ? (
|
||||||
<button
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||||
onClick={() => loadTrends(true)}
|
{[...Array(8)].map((_, i) => (
|
||||||
className="ml-auto text-[10px] font-mono text-rose-400 underline hover:no-underline"
|
<div key={i} className="h-16 bg-white/5 animate-pulse" />
|
||||||
>
|
))}
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{trends.slice(0, 16).map((t, idx) => {
|
{trends.slice(0, 16).map((t, idx) => {
|
||||||
const active = selected === t.title
|
const isSelected = selected === t.title
|
||||||
const isHot = idx < 3
|
const isHot = idx < 3
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={t.title}
|
key={t.title}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelected(t.title)
|
setSelected(isSelected ? null : t.title)
|
||||||
setKeywordInput('')
|
setResults([])
|
||||||
setAvailability([])
|
setAiKeywords([])
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'group relative px-4 py-2.5 border text-left transition-all',
|
"relative p-3 text-left border transition-all",
|
||||||
active
|
isSelected
|
||||||
? 'border-accent bg-accent/10'
|
? "border-accent bg-accent/10"
|
||||||
: 'border-white/[0.08] hover:border-white/20 bg-white/[0.02] hover:bg-white/[0.04]'
|
: "border-white/10 bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.04]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isHot && (
|
{isHot && (
|
||||||
<span className="text-[9px] font-bold text-orange-400 bg-orange-400/10 px-1 py-0.5">
|
<span className="absolute top-2 right-2 text-[10px]">🔥</span>
|
||||||
🔥
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
<span className={clsx(
|
<p className={clsx(
|
||||||
"text-xs font-medium truncate max-w-[140px]",
|
"text-sm font-medium truncate",
|
||||||
active ? "text-accent" : "text-white/70 group-hover:text-white"
|
isSelected ? "text-accent" : "text-white/80"
|
||||||
)}>
|
)}>
|
||||||
{t.title}
|
{t.title}
|
||||||
</span>
|
</p>
|
||||||
</div>
|
|
||||||
{t.approx_traffic && (
|
{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>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</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 && (
|
{selected && (
|
||||||
<div className="px-4 pb-4 border-t border-white/[0.08] pt-4">
|
<div className="border border-accent/30 bg-accent/5 p-4 space-y-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<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>
|
<div>
|
||||||
<h3 className="text-base font-bold text-white">Check Availability</h3>
|
<p className="text-xs text-accent font-mono uppercase tracking-wider">Selected Trend</p>
|
||||||
<p className="text-[11px] font-mono text-white/40">
|
<h3 className="text-xl font-bold text-white mt-1">{selected}</h3>
|
||||||
{keyword ? `Find ${keyword.toLowerCase().replace(/\s+/g, '')} across multiple TLDs` : 'Select a trend or enter a keyword'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={() => { setKeywordInput(''); setSelected(''); setAvailability([]) }}
|
onClick={() => { setSelected(null); setResults([]); setAiKeywords([]) }}
|
||||||
className="p-3 text-white/30 hover:text-white transition-colors"
|
className="p-2 text-white/40 hover:text-white"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* TLD Selection */}
|
{/* TLD Selection */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Select TLDs</p>
|
||||||
<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>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{POPULAR_TLDS.map(tld => (
|
{TLDS.map(tld => (
|
||||||
<button
|
<button
|
||||||
key={tld}
|
key={tld}
|
||||||
onClick={() => toggleTld(tld)}
|
onClick={() => setTlds(prev =>
|
||||||
|
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
|
||||||
|
)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-3 py-1.5 text-[11px] font-mono uppercase border transition-all",
|
"px-3 py-1.5 text-xs font-mono border transition-all",
|
||||||
selectedTlds.includes(tld)
|
tlds.includes(tld)
|
||||||
? "border-accent bg-accent/10 text-accent"
|
? "border-accent bg-accent/20 text-accent"
|
||||||
: "border-white/[0.08] text-white/40 hover:text-white/60 hover:border-white/20"
|
: "border-white/10 text-white/40 hover:text-white"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
.{tld}
|
.{tld}
|
||||||
@ -496,222 +255,138 @@ export function TrendSurferTab({ showToast }: { showToast: (message: string, typ
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Results */}
|
{/* Check Button */}
|
||||||
{availability.length > 0 && (
|
<button
|
||||||
<div className="space-y-2">
|
onClick={() => checkAvailability(selected)}
|
||||||
<div className="flex items-center justify-between">
|
disabled={checking || tlds.length === 0}
|
||||||
<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}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-3 flex items-center justify-between gap-3 border transition-all",
|
"w-full py-3 text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all",
|
||||||
isAvailable
|
checking || tlds.length === 0
|
||||||
? "bg-accent/[0.03] border-accent/20 hover:bg-accent/[0.06]"
|
? "bg-white/10 text-white/30"
|
||||||
: "bg-white/[0.02] border-white/[0.06] hover:bg-white/[0.04]"
|
: "bg-accent text-black hover:bg-white"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
{checking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
|
||||||
<div className={clsx(
|
Check {selected.toLowerCase().replace(/\s+/g, '')} Availability
|
||||||
"w-2.5 h-2.5 rounded-full shrink-0",
|
</button>
|
||||||
isAvailable ? "bg-accent" : "bg-white/20"
|
|
||||||
|
{/* 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
|
<button
|
||||||
onClick={() => openAnalyze(a.domain)}
|
onClick={() => openAnalyze(r.domain)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"text-sm font-mono truncate text-left transition-colors",
|
"text-sm font-mono truncate",
|
||||||
isAvailable ? "text-white hover:text-accent" : "text-white/50"
|
r.available ? "text-white hover:text-accent" : "text-white/40"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{a.domain}
|
{r.domain}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<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>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => copyDomain(a.domain)}
|
onClick={() => copy(r.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"
|
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
|
||||||
title="Copy"
|
|
||||||
>
|
>
|
||||||
{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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => track(a.domain)}
|
onClick={() => track(r.domain)}
|
||||||
disabled={tracking === a.domain}
|
disabled={tracking === r.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"
|
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
|
||||||
title="Add to Watchlist"
|
|
||||||
>
|
>
|
||||||
{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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => openAnalyze(a.domain)}
|
onClick={() => openAnalyze(r.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"
|
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-accent"
|
||||||
title="Analyze"
|
|
||||||
>
|
>
|
||||||
<Shield className="w-3.5 h-3.5" />
|
<Shield className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
{r.available && (
|
||||||
{isAvailable && (
|
|
||||||
<a
|
<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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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" />
|
<ShoppingCart className="w-3 h-3" />
|
||||||
Buy
|
<span className="hidden sm:inline">Buy</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty State */}
|
{/* Empty State */}
|
||||||
{typos.length === 0 && !typoLoading && (
|
{!selected && !loading && trends.length > 0 && (
|
||||||
<div className="text-center py-8 border border-dashed border-white/[0.08] bg-white/[0.01]">
|
<div className="text-center py-8 border border-dashed border-white/10">
|
||||||
<p className="text-white/30 text-xs font-mono">
|
<Globe className="w-8 h-8 text-white/20 mx-auto mb-2" />
|
||||||
Enter a brand name to discover available typo domains
|
<p className="text-sm text-white/40">Select a trend to find available domains</p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user