diff --git a/frontend/src/components/hunt/BrandableForgeTab.tsx b/frontend/src/components/hunt/BrandableForgeTab.tsx index 5745054..ffdb338 100644 --- a/frontend/src/components/hunt/BrandableForgeTab.tsx +++ b/frontend/src/components/hunt/BrandableForgeTab.tsx @@ -3,23 +3,19 @@ import { useCallback, useState } from 'react' import clsx from 'clsx' import { - ExternalLink, Loader2, Shield, - Sparkles, Eye, Wand2, - Settings, Zap, Copy, Check, ShoppingCart, - Star, - Lightbulb, - RefreshCw, + Sparkles, Lock, + Star, Brain, - MessageSquare, + RefreshCw, } from 'lucide-react' import Link from 'next/link' import { api } from '@/lib/api' @@ -31,627 +27,460 @@ import { useStore } from '@/lib/store' // ============================================================================ const PATTERNS = [ - { - key: 'cvcvc', - label: 'CVCVC', - desc: 'Classic 5-letter brandables', - examples: ['Zalor', 'Mivex', 'Ronix'], - color: 'accent' - }, - { - key: 'cvccv', - label: 'CVCCV', - desc: 'Punchy 5-letter names', - examples: ['Bento', 'Salvo', 'Vento'], - color: 'blue' - }, - { - key: 'human', - label: 'Human', - desc: 'AI agent ready names', - examples: ['Siri', 'Alexa', 'Levi'], - color: 'purple' - }, + { key: 'cvcvc', label: 'CVCVC', examples: ['Zalor', 'Mivex', 'Ronix'], desc: 'Classic 5-letter' }, + { key: 'cvccv', label: 'CVCCV', examples: ['Bento', 'Salvo'], desc: 'Punchy sound' }, + { key: 'human', label: 'Human', examples: ['Siri', 'Alexa', 'Levi'], desc: 'AI agent names' }, ] -const TLDS = [ - { tld: 'com', premium: true, label: '.com' }, - { tld: 'io', premium: true, label: '.io' }, - { tld: 'ai', premium: true, label: '.ai' }, - { tld: 'co', premium: false, label: '.co' }, - { tld: 'net', premium: false, label: '.net' }, - { tld: 'org', premium: false, label: '.org' }, - { tld: 'app', premium: false, label: '.app' }, - { tld: 'dev', premium: false, label: '.dev' }, -] +const TLDS = ['com', 'io', 'ai', 'co', 'net', 'app'] // ============================================================================ // COMPONENT // ============================================================================ -export function BrandableForgeTab({ showToast }: { showToast: (message: string, type?: any) => void }) { +export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type?: any) => void }) { const openAnalyze = useAnalyzePanelStore((s) => s.open) const addDomain = useStore((s) => s.addDomain) const subscription = useStore((s) => s.subscription) const tier = (subscription?.tier || '').toLowerCase() const hasAI = tier === 'trader' || tier === 'tycoon' - // Mode: 'pattern' (classic) or 'ai' (concept-based) + // Mode const [mode, setMode] = useState<'pattern' | 'ai'>('pattern') - // Config State + // Pattern Mode const [pattern, setPattern] = useState('cvcvc') - const [selectedTlds, setSelectedTlds] = useState(['com', 'io']) - const [limit, setLimit] = useState(30) - const [showConfig, setShowConfig] = useState(false) + const [tlds, setTlds] = useState(['com', 'io']) - // AI State + // AI Mode const [concept, setConcept] = useState('') - const [conceptFocused, setConceptFocused] = useState(false) const [similarBrand, setSimilarBrand] = useState('') - const [similarFocused, setSimilarFocused] = useState(false) const [aiNames, setAiNames] = useState([]) - const [aiLoading, setAiLoading] = useState(false) - // Results State + // Shared + const [results, setResults] = useState>([]) const [loading, setLoading] = useState(false) - const [items, setItems] = useState>([]) - const [error, setError] = useState(null) - const [tracking, setTracking] = useState(null) + const [aiLoading, setAiLoading] = useState(false) const [copied, setCopied] = useState(null) + const [tracking, setTracking] = useState(null) - const toggleTld = useCallback((tld: string) => { - setSelectedTlds((prev) => - prev.includes(tld) ? prev.filter((t) => t !== tld) : [...prev, tld] - ) - }, []) - - const copyDomain = useCallback((domain: string) => { - navigator.clipboard.writeText(domain) - setCopied(domain) - setTimeout(() => setCopied(null), 1500) - }, []) - - const copyAll = useCallback(() => { - if (items.length === 0) return - navigator.clipboard.writeText(items.map(i => i.domain).join('\n')) - showToast(`Copied ${items.length} domains to clipboard`, 'success') - }, [items, showToast]) - - const run = useCallback(async () => { - if (selectedTlds.length === 0) { + // Generate Pattern-based + const generatePattern = useCallback(async () => { + if (tlds.length === 0) { showToast('Select at least one TLD', 'error') return } setLoading(true) - setError(null) - setItems([]) + setResults([]) try { - const res = await api.huntBrandables({ pattern, tlds: selectedTlds, limit, max_checks: 400 }) - setItems(res.items.map((i) => ({ domain: i.domain, status: i.status }))) - if (res.items.length === 0) { - showToast('No available domains found. Try different settings.', 'info') - } else { - showToast(`Found ${res.items.length} available brandable domains!`, 'success') - } + const res = await api.huntBrandables({ pattern, tlds, limit: 30, max_checks: 400 }) + setResults(res.items.map(i => ({ domain: i.domain, available: true }))) + showToast(`Found ${res.items.length} brandable domains!`, 'success') } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - setError(msg) - showToast(msg, 'error') - setItems([]) + showToast('Generation failed', 'error') } finally { setLoading(false) } - }, [pattern, selectedTlds, limit, showToast]) + }, [pattern, tlds, showToast]) - const track = useCallback( - async (domain: string) => { - if (tracking) return - setTracking(domain) - try { - await addDomain(domain) - showToast(`Added to watchlist: ${domain}`, 'success') - } catch (e) { - showToast(e instanceof Error ? e.message : 'Failed to track domain', 'error') - } finally { - setTracking(null) - } - }, - [addDomain, showToast, tracking] - ) - - const currentPattern = PATTERNS.find(p => p.key === pattern) - - // AI Generation + // Generate AI-based const generateFromConcept = useCallback(async () => { - if (!concept.trim() || !hasAI || aiLoading) return + if (!concept.trim() || !hasAI) return setAiLoading(true) setAiNames([]) + setResults([]) 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') - } + showToast(`AI generated ${res.names?.length || 0} names!`, 'success') } catch (e) { - showToast(e instanceof Error ? e.message : 'AI generation failed', 'error') + showToast('AI generation failed', 'error') } finally { setAiLoading(false) } - }, [concept, hasAI, aiLoading, showToast]) + }, [concept, hasAI, showToast]) const generateFromBrand = useCallback(async () => { - if (!similarBrand.trim() || !hasAI || aiLoading) return + if (!similarBrand.trim() || !hasAI) return setAiLoading(true) setAiNames([]) + setResults([]) 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') - } + showToast(`Found ${res.names?.length || 0} similar names!`, 'success') } catch (e) { - showToast(e instanceof Error ? e.message : 'AI generation failed', 'error') + showToast('AI generation failed', 'error') } finally { setAiLoading(false) } - }, [similarBrand, hasAI, aiLoading, showToast]) + }, [similarBrand, hasAI, showToast]) - // Check AI-generated names for availability + // Check AI names availability const checkAiNames = useCallback(async () => { - if (aiNames.length === 0 || selectedTlds.length === 0) return + if (aiNames.length === 0 || tlds.length === 0) return setLoading(true) - setItems([]) try { - const res = await api.huntKeywords({ keywords: aiNames, tlds: selectedTlds }) + const res = await api.huntKeywords({ keywords: aiNames, tlds }) 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') + setResults(available.map(i => ({ domain: i.domain, available: true }))) + showToast(`${available.length} available!`, 'success') } catch (e) { - showToast(e instanceof Error ? e.message : 'Availability check failed', 'error') + showToast('Check failed', 'error') } finally { setLoading(false) } - }, [aiNames, selectedTlds, showToast]) + }, [aiNames, tlds, showToast]) + + // Actions + const copy = (domain: string) => { + navigator.clipboard.writeText(domain) + setCopied(domain) + setTimeout(() => setCopied(null), 1500) + } + + const track = async (domain: string) => { + if (tracking) return + setTracking(domain) + try { + await addDomain(domain) + showToast(`Added: ${domain}`, 'success') + } catch (e) { + showToast('Failed', 'error') + } finally { + setTracking(null) + } + } + + const copyAll = () => { + if (results.length === 0) return + navigator.clipboard.writeText(results.map(r => r.domain).join('\n')) + showToast(`Copied ${results.length} domains`, 'success') + } return ( -
+
{/* ═══════════════════════════════════════════════════════════════════════ */} - {/* MAIN GENERATOR CARD */} + {/* HEADER + MODE TOGGLE */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
- {/* Header */} -
-
-
-
- -
-
-

Brandable Forge

-

- {mode === 'pattern' ? 'Pattern-based name generator' : 'AI-powered concept generator'} -

-
-
-
- - -
-
+
+
+

+ + Brandable Forge +

+

+ Generate unique, memorable domain names +

- - {/* Mode Toggle */} -
-
- - - {!hasAI && ( - - Upgrade for AI - +
+
+ > + Patterns + +
+
- {/* AI Concept Mode */} - {mode === 'ai' && hasAI && ( -
- {/* Concept Input */} -
- -
- - setConcept(e.target.value)} - onFocus={() => setConceptFocused(true)} - onBlur={() => setConceptFocused(false)} - onKeyDown={(e) => e.key === 'Enter' && generateFromConcept()} - placeholder="e.g., AI startup for legal documents, crypto wallet for teens..." - className="flex-1 bg-transparent py-3 pr-3 text-sm text-white placeholder:text-white/25 outline-none font-mono" - /> - -
-
- - {/* Similar Brand Input */} -
- -
- - setSimilarBrand(e.target.value)} - onFocus={() => setSimilarFocused(true)} - onBlur={() => setSimilarFocused(false)} - onKeyDown={(e) => e.key === 'Enter' && generateFromBrand()} - placeholder="e.g., Stripe, Notion, Figma..." - className="flex-1 bg-transparent py-3 pr-3 text-sm text-white placeholder:text-white/25 outline-none font-mono" - /> - -
-
- - {/* AI Generated Names */} - {aiNames.length > 0 && ( -
-
- - AI Suggestions ({aiNames.length}) - - -
-
- {aiNames.map((name) => ( - - {name} - - ))} -
-
- )} -
- )} - - {/* Pattern Selection */} - {mode === 'pattern' && ( -
-
- - Choose Pattern -
-
- {PATTERNS.map((p) => { - const isActive = pattern === p.key - const colorClass = p.color === 'accent' ? 'accent' : p.color === 'blue' ? 'blue-400' : 'purple-400' - return ( + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* PATTERN MODE */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {mode === 'pattern' && ( +
+ {/* Pattern Selection */} +
+

Choose Pattern

+
+ {PATTERNS.map(p => ( - ) - })} -
-
- )} - - {/* TLD Selection */} -
-
-
- Select TLDs - ({selectedTlds.length} selected) + ))}
-
-
- {TLDS.map((t) => ( + + {/* TLD Selection */} +
+

Select TLDs

+
+ {TLDS.map(tld => ( + + ))} +
+
+ + {/* Generate Button */} + +
+ )} + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* AI MODE */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {mode === 'ai' && hasAI && ( +
+ {/* Concept Input */} +
+

Describe Your Brand

+
+ setConcept(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && generateFromConcept()} + placeholder="e.g., AI startup for legal docs..." + className="flex-1 px-3 py-2.5 bg-white/5 border border-white/10 text-sm text-white font-mono placeholder:text-white/25 outline-none focus:border-purple-500/50" + /> - ))} -
-
- - {/* Advanced Config */} - {showConfig && ( -
-
-
- -
- setLimit(Number(e.target.value))} - min={10} - max={100} - step={10} - className="w-32 accent-accent" - /> - {limit} -
-
-
-

We'll check up to 400 random combinations and return the first {limit} verified available domains.

-
- )} - {/* Stats Bar */} -
- - {items.length > 0 ? ( - - - {items.length} brandable domains ready - - ) : ( - 'Configure settings and click Generate' - )} - - {items.length > 0 && ( - + {/* OR Divider */} +
+
+ OR +
+
+ + {/* Similar Brand Input */} +
+

Find Names Like...

+
+ setSimilarBrand(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && generateFromBrand()} + placeholder="e.g., Stripe, Notion, Figma..." + className="flex-1 px-3 py-2.5 bg-white/5 border border-white/10 text-sm text-white font-mono placeholder:text-white/25 outline-none focus:border-purple-500/50" + /> + +
+
+ + {/* AI Names */} + {aiNames.length > 0 && ( +
+
+

+ AI Suggestions ({aiNames.length}) +

+ +
+
+ {TLDS.map(tld => ( + + ))} +
+
+ {aiNames.map(name => ( + + {name} + + ))} +
+
)}
-
+ )} - {/* Error Message */} - {error && ( -
-
- -
-
-

{error}

- -
+ {/* Upgrade CTA for Scout */} + {mode === 'ai' && !hasAI && ( +
+ +

AI features require Trader or Tycoon

+ + + Upgrade +
)} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* RESULTS */} {/* ═══════════════════════════════════════════════════════════════════════ */} - {items.length > 0 && ( -
-
- - Generated Domains - - -
- -
- {items.map((i, idx) => ( -
0 && ( +
+
+

+ {results.length} available domains +

+
+ -
- -
- - ✓ AVAIL - - - - - - - - - - - Buy - -
+ + Copy All + + +
+
+ +
+ {results.map((r, idx) => ( +
+
+ + {String(idx + 1).padStart(2, '0')} + + +
+
+ + + + + + Buy +
))} @@ -660,39 +489,14 @@ export function BrandableForgeTab({ showToast }: { showToast: (message: string, )} {/* Empty State */} - {items.length === 0 && !loading && ( -
-
- -
-

Ready to forge

-

- Select a pattern and TLDs, then click "Generate" to discover available brandable domain names + {results.length === 0 && !loading && !aiLoading && ( +

+ +

+ {mode === 'pattern' + ? 'Select a pattern and click Generate' + : 'Describe your concept or enter a brand name'}

-
- Verified available - - DNS checked -
-
- )} - - {/* Loading State */} - {loading && items.length === 0 && ( -
- {[...Array(6)].map((_, i) => ( -
-
-
-
-
-
-
-
-
-
-
- ))}
)}
diff --git a/frontend/src/components/hunt/SearchTab.tsx b/frontend/src/components/hunt/SearchTab.tsx index a0384b9..f7f7d3e 100644 --- a/frontend/src/components/hunt/SearchTab.tsx +++ b/frontend/src/components/hunt/SearchTab.tsx @@ -47,7 +47,8 @@ interface TldCheckResult { } // Popular TLDs to check when user enters only a name without extension -const POPULAR_TLDS = ['com', 'net', 'org', 'io', 'ch', 'de', 'app', 'dev', 'co', 'ai'] +// Ordered by popularity/importance - most common first for faster perceived loading +const POPULAR_TLDS = ['com', 'ch', 'io', 'net', 'org', 'de', 'ai', 'co', 'app', 'dev'] // Known valid TLDs (subset for quick validation) const KNOWN_TLDS = new Set([ @@ -131,7 +132,7 @@ export function SearchTab({ showToast }: SearchTabProps) { } }, []) - // Check multiple TLDs for a name + // Check multiple TLDs for a name - with progressive loading and quick mode const checkMultipleTlds = useCallback(async (name: string) => { // Initialize results with loading state const initialResults: TldCheckResult[] = POPULAR_TLDS.map(tld => ({ @@ -142,31 +143,36 @@ export function SearchTab({ showToast }: SearchTabProps) { })) setTldResults(initialResults) - // Check each TLD in parallel - const results = await Promise.all( - POPULAR_TLDS.map(async (tld): Promise => { - const domain = `${name}.${tld}` - try { - const result = await api.checkDomain(domain) - return { + // Check each TLD in parallel with progressive updates (using quick=true for speed) + POPULAR_TLDS.forEach(async (tld, index) => { + const domain = `${name}.${tld}` + try { + // Use quick=true for DNS-only check (much faster!) + const result = await api.checkDomain(domain, true) + setTldResults(prev => { + const updated = [...prev] + updated[index] = { tld, domain, is_available: result.is_available, loading: false, } - } catch { - return { + return updated + }) + } catch { + setTldResults(prev => { + const updated = [...prev] + updated[index] = { tld, domain, is_available: null, loading: false, error: 'Check failed', } - } - }) - ) - - setTldResults(results) + return updated + }) + } + }) }, []) // Search Handler @@ -303,10 +309,10 @@ export function SearchTab({ showToast }: SearchTabProps) { {/* Stats Bar */}
- Enter a domain to check availability via RDAP/WHOIS + Enter a name (checks 10 TLDs) or full domain (e.g. example.com) - Instant check + RDAP/WHOIS
diff --git a/frontend/src/components/hunt/TrendSurferTab.tsx b/frontend/src/components/hunt/TrendSurferTab.tsx index 44737cc..4409000 100644 --- a/frontend/src/components/hunt/TrendSurferTab.tsx +++ b/frontend/src/components/hunt/TrendSurferTab.tsx @@ -1,493 +1,252 @@ 'use client' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import clsx from 'clsx' import { - ExternalLink, Loader2, - Search, Shield, - Sparkles, Eye, - TrendingUp, RefreshCw, - Globe, Zap, - X, Check, Copy, ShoppingCart, Flame, - ArrowRight, - AlertCircle, - Wand2, + Sparkles, Lock, + ChevronRight, + Globe, + X, } from 'lucide-react' +import Link from 'next/link' import { api } from '@/lib/api' import { useAnalyzePanelStore } from '@/lib/analyze-store' import { useStore } from '@/lib/store' -import Link from 'next/link' // ============================================================================ -// TYPES & CONSTANTS +// CONSTANTS // ============================================================================ -const GEO_OPTIONS = [ - { value: 'US', label: 'United States', flag: '🇺🇸' }, - { value: 'CH', label: 'Switzerland', flag: '🇨🇭' }, - { value: 'DE', label: 'Germany', flag: '🇩🇪' }, - { value: 'GB', label: 'United Kingdom', flag: '🇬🇧' }, - { value: 'FR', label: 'France', flag: '🇫🇷' }, - { value: 'CA', label: 'Canada', flag: '🇨🇦' }, - { value: 'AU', label: 'Australia', flag: '🇦🇺' }, +const GEOS = [ + { code: 'US', flag: '🇺🇸', name: 'USA' }, + { code: 'DE', flag: '🇩🇪', name: 'Germany' }, + { code: 'GB', flag: '🇬🇧', name: 'UK' }, + { code: 'CH', flag: '🇨🇭', name: 'Switzerland' }, + { code: 'FR', flag: '🇫🇷', name: 'France' }, ] -const POPULAR_TLDS = ['com', 'io', 'ai', 'co', 'net', 'org', 'app', 'dev'] - -// ============================================================================ -// HELPERS -// ============================================================================ - -function normalizeKeyword(s: string) { - return s.trim().replace(/\s+/g, ' ') -} +const TLDS = ['com', 'io', 'ai', 'co', 'net', 'app'] // ============================================================================ // COMPONENT // ============================================================================ -export function TrendSurferTab({ showToast }: { showToast: (message: string, type?: any) => void }) { +export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?: any) => void }) { const openAnalyze = useAnalyzePanelStore((s) => s.open) const addDomain = useStore((s) => s.addDomain) const subscription = useStore((s) => s.subscription) const tier = (subscription?.tier || '').toLowerCase() const hasAI = tier === 'trader' || tier === 'tycoon' - - // Trends State - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [geo, setGeo] = useState('US') - const [trends, setTrends] = useState>([]) - const [selected, setSelected] = useState('') - const [refreshing, setRefreshing] = useState(false) - // AI Expansion State + // State + const [geo, setGeo] = useState('US') + const [loading, setLoading] = useState(true) + const [trends, setTrends] = useState>([]) + const [selected, setSelected] = useState(null) + const [tlds, setTlds] = useState(['com', 'io', 'ai']) + const [results, setResults] = useState>([]) + const [checking, setChecking] = useState(false) const [aiKeywords, setAiKeywords] = useState([]) const [aiLoading, setAiLoading] = useState(false) - const [aiAnalysis, setAiAnalysis] = useState('') - - // Keyword Check State - const [keywordInput, setKeywordInput] = useState('') - const [keywordFocused, setKeywordFocused] = useState(false) - const [selectedTlds, setSelectedTlds] = useState(['com', 'io', 'ai']) - const [availability, setAvailability] = useState>([]) - const [checking, setChecking] = useState(false) - - // Typo Check State - const [brand, setBrand] = useState('') - const [brandFocused, setBrandFocused] = useState(false) - const [typos, setTypos] = useState>([]) - const [typoLoading, setTypoLoading] = useState(false) - - // Tracking & Copy State - const [tracking, setTracking] = useState(null) const [copied, setCopied] = useState(null) + const [tracking, setTracking] = useState(null) - // AI Keyword Expansion - const expandWithAI = useCallback(async () => { - if (!selected || !hasAI || aiLoading) return - setAiLoading(true) - setAiKeywords([]) - setAiAnalysis('') - try { - const [expandRes, analyzeRes] = await Promise.all([ - api.expandTrendKeywords(selected, geo), - api.analyzeTrend(selected, geo), - ]) - setAiKeywords(expandRes.keywords || []) - setAiAnalysis(analyzeRes.analysis || '') - if (expandRes.keywords?.length) { - showToast(`AI found ${expandRes.keywords.length} related keywords!`, 'success') - } - } catch (e) { - showToast(e instanceof Error ? e.message : 'AI expansion failed', 'error') - } finally { - setAiLoading(false) - } - }, [selected, geo, hasAI, aiLoading, showToast]) - - const copyDomain = useCallback((domain: string) => { - navigator.clipboard.writeText(domain) - setCopied(domain) - setTimeout(() => setCopied(null), 1500) - }, []) - - const track = useCallback( - async (domain: string) => { - if (tracking) return - setTracking(domain) - try { - await addDomain(domain) - showToast(`Added to watchlist: ${domain}`, 'success') - } catch (e) { - showToast(e instanceof Error ? e.message : 'Failed to track domain', 'error') - } finally { - setTracking(null) - } - }, - [addDomain, showToast, tracking] - ) - - const loadTrends = useCallback(async (isRefresh = false) => { - if (isRefresh) setRefreshing(true) - setError(null) + // Load trends + const loadTrends = useCallback(async () => { + setLoading(true) 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([]) + showToast('Failed to load trends', 'error') } finally { - if (isRefresh) setRefreshing(false) + setLoading(false) } - }, [geo, selected]) + }, [geo, showToast]) useEffect(() => { - let cancelled = false - const run = async () => { - setLoading(true) - try { - await loadTrends() - } catch (e) { - if (!cancelled) setError(e instanceof Error ? e.message : String(e)) - } finally { - if (!cancelled) setLoading(false) - } - } - run() - return () => { cancelled = true } + loadTrends() }, [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 - } + // 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: selectedTlds }) - setAvailability(res.items.map((r) => ({ domain: r.domain, status: r.status, is_available: r.is_available }))) + const res = await api.huntKeywords({ keywords: [kw], tlds }) + setResults(res.items.map(i => ({ domain: i.domain, available: i.status === 'available' }))) } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to check availability' - showToast(msg, 'error') - setAvailability([]) + showToast('Check failed', 'error') } finally { setChecking(false) } - }, [keyword, selectedTlds, showToast]) + }, [tlds, showToast]) - const runTypos = useCallback(async () => { - const b = brand.trim() - if (!b) return - setTypoLoading(true) + // AI Expand + const expandWithAI = useCallback(async () => { + if (!selected || !hasAI) return + setAiLoading(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') - } + const res = await api.expandTrendKeywords(selected, geo) + setAiKeywords(res.keywords || []) } catch (e) { - const msg = e instanceof Error ? e.message : 'Failed to run typo check' - showToast(msg, 'error') - setTypos([]) + showToast('AI expansion failed', 'error') } finally { - setTypoLoading(false) + setAiLoading(false) } - }, [brand, showToast]) + }, [selected, geo, hasAI, showToast]) - const availableCount = useMemo(() => availability.filter(a => a.status === 'available').length, [availability]) - const currentGeo = GEO_OPTIONS.find(g => g.value === geo) - - if (loading) { - return ( -
- {/* Skeleton Loader */} -
-
-
-
-
-
- {[...Array(8)].map((_, i) => ( -
- ))} -
-
-
- ) + // Actions + const copy = (domain: string) => { + navigator.clipboard.writeText(domain) + setCopied(domain) + setTimeout(() => setCopied(null), 1500) } + const track = async (domain: string) => { + if (tracking) return + setTracking(domain) + try { + await addDomain(domain) + showToast(`Added: ${domain}`, 'success') + } catch (e) { + showToast('Failed to track', 'error') + } finally { + setTracking(null) + } + } + + const currentGeo = GEOS.find(g => g.code === geo) + const availableCount = results.filter(r => r.available).length + return ( -
+
{/* ═══════════════════════════════════════════════════════════════════════ */} - {/* TRENDING TOPICS */} + {/* HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-
-
-
- -
-
-

Trending Now

-

- Real-time Google Trends • {currentGeo?.flag} {currentGeo?.label} -

-
-
-
- - -
-
+
+
+

+ + Trending Now +

+

+ Real-time Google Trends → Domain opportunities +

+
+
+ +
- - {error ? ( -
- -

{error}

- -
- ) : ( -
-
- {trends.slice(0, 16).map((t, idx) => { - const active = selected === t.title - const isHot = idx < 3 - return ( - - ) - })} -
- {trends.length === 0 && ( -
- No trends available for this region -
- )} -
- )} - - {/* AI Expansion Section */} - {selected && ( -
-
-
- - AI Keyword Expansion - {!hasAI && ( - - TRADER+ - - )} -
- {hasAI ? ( - - ) : ( - - - Upgrade - - )} -
- - {/* AI Analysis */} - {aiAnalysis && ( -
-

{aiAnalysis}

-
- )} - - {/* AI Keywords */} - {aiKeywords.length > 0 && ( -
- {aiKeywords.map((kw) => ( - - ))} -
- )} - - {!aiKeywords.length && !aiLoading && hasAI && ( -

- Click "Expand with AI" to find related keywords for "{selected}" -

- )} -
- )}
{/* ═══════════════════════════════════════════════════════════════════════ */} - {/* DOMAIN AVAILABILITY CHECKER */} + {/* TRENDS GRID */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-
-
- -
-
-

Check Availability

-

- {keyword ? `Find ${keyword.toLowerCase().replace(/\s+/g, '')} across multiple TLDs` : 'Select a trend or enter a keyword'} -

-
-
+ {loading ? ( +
+ {[...Array(8)].map((_, i) => ( +
+ ))}
- -
- {/* Keyword Input */} -
-
-
- - 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) && ( - + ) : ( +
+ {trends.slice(0, 16).map((t, idx) => { + const isSelected = selected === t.title + const isHot = idx < 3 + return ( +
+ > + {isHot && ( + 🔥 + )} +

+ {t.title} +

+ {t.approx_traffic && ( +

{t.approx_traffic}

+ )} + + ) + })} +
+ )} + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* SELECTED TREND PANEL */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {selected && ( +
+ {/* Header */} +
+
+

Selected Trend

+

{selected}

{/* TLD Selection */}
-
- Select TLDs - ({selectedTlds.length} selected) -
+

Select TLDs

- {POPULAR_TLDS.map(tld => ( + {TLDS.map(tld => (
- {/* Results */} - {availability.length > 0 && ( -
-
- - Results • {availableCount} available - -
-
- {availability.map((a) => { - const isAvailable = a.status === 'available' - return ( -
-
-
- -
- -
- - {isAvailable ? '✓ AVAIL' : 'TAKEN'} - - - - - - - - - {isAvailable && ( - - - Buy - - )} -
-
- ) - })} -
-
- )} + {/* Check Button */} + - {/* Empty State */} - {availability.length === 0 && keyword && !checking && ( -
- -

Ready to check

-

- Click "Check" to find available domains for {keyword.toLowerCase().replace(/\s+/g, '')} + {/* AI Expansion */} +

+
+

+ + AI Related Keywords + {!hasAI && }

-
- )} -
-
- - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* TYPO FINDER */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-
-
- -
-
-

Typo Finder

-

- Find available misspellings of popular brands -

-
-
-
- -
-
-
-
- - 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 && ( - - )} -
-
- -
- - {/* Typo Results */} - {typos.length > 0 && ( -
- {typos.map((t) => ( -
- -
- - - - - -
-
- ))} + {aiLoading ? : } + Expand + + ) : ( + + Upgrade + + )}
- )} + {aiKeywords.length > 0 && ( +
+ {aiKeywords.map(kw => ( + + ))} +
+ )} +
- {/* Empty State */} - {typos.length === 0 && !typoLoading && ( -
-

- Enter a brand name to discover available typo domains + {/* Results */} + {results.length > 0 && ( +

+

+ {availableCount} of {results.length} available

+
+ {results.map(r => ( +
+
+ + +
+
+ + + + {r.available && ( + + + Buy + + )} +
+
+ ))} +
)}
-
+ )} + + {/* Empty State */} + {!selected && !loading && trends.length > 0 && ( +
+ +

Select a trend to find available domains

+
+ )}
) }