From 129716ad1d9f1e610320f894d7b882c8cd51605b Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Wed, 17 Dec 2025 16:05:40 +0100 Subject: [PATCH] Trends & Forge v2: clearer AI integration, unified layout, auto-expand keywords --- .../src/components/analyze/AnalyzePanel.tsx | 138 ++++- frontend/src/components/hunt/AuctionsTab.tsx | 10 +- .../src/components/hunt/BrandableForgeTab.tsx | 519 +++++++----------- frontend/src/components/hunt/SearchTab.tsx | 54 +- .../src/components/hunt/TrendSurferTab.tsx | 418 +++++++------- 5 files changed, 570 insertions(+), 569 deletions(-) diff --git a/frontend/src/components/analyze/AnalyzePanel.tsx b/frontend/src/components/analyze/AnalyzePanel.tsx index 9a2afb1..a6efd92 100644 --- a/frontend/src/components/analyze/AnalyzePanel.tsx +++ b/frontend/src/components/analyze/AnalyzePanel.tsx @@ -101,11 +101,50 @@ function getStatusStyle(status: string) { } } -const SECTION_CONFIG: Record = { - authority: { icon: Shield, color: 'blue', label: 'Authority' }, - market: { icon: TrendingUp, color: 'emerald', label: 'Market' }, - risk: { icon: AlertTriangle, color: 'amber', label: 'Risk' }, - value: { icon: DollarSign, color: 'violet', label: 'Value' }, +const SECTION_CONFIG: Record = { + authority: { + icon: Shield, + color: 'blue', + label: 'Authority', + description: 'Domain trust signals: age, backlinks, brand memorability' + }, + market: { + icon: TrendingUp, + color: 'emerald', + label: 'Market', + description: 'Search demand, competition, and commercial value indicators' + }, + risk: { + icon: AlertTriangle, + color: 'amber', + label: 'Risk', + description: 'Legal, reputation and technical risks to consider' + }, + value: { + icon: DollarSign, + color: 'violet', + label: 'Value', + description: 'Estimated worth and comparable sales data' + }, +} + +// Tooltips for each analysis item +const ITEM_TOOLTIPS: Record = { + availability: 'Is this domain currently available for registration?', + radio_test: 'Can this domain be easily spelled when heard? Good brandables score high.', + age: 'Older domains often have more trust with search engines.', + backlinks: 'Number of websites linking to this domain. More = higher authority.', + trust_flow: 'Quality score of the backlink profile (0-100). Higher is better.', + search_volume: 'Monthly Google searches for this keyword. Higher = more traffic potential.', + cpc: 'Cost-per-click for ads on this keyword. Higher CPC = more commercial intent.', + competition: 'How competitive the keyword is for SEO and ads.', + tld_matrix: 'Availability of this name across different domain extensions.', + blacklist: 'Is this domain flagged for spam, malware, or phishing?', + trademark: 'Does this domain potentially infringe on known trademarks?', + archive: 'Historical data from the Wayback Machine - what was hosted here before?', + valuation: 'Estimated market value based on comparable sales and metrics.', + tld_cheapest_register_usd: 'Lowest registration price available from major registrars.', + tld_cheapest_renew_usd: 'Annual renewal cost - factor this into your ROI calculations.', } async function copyToClipboard(text: string) { @@ -302,7 +341,10 @@ export function AnalyzePanel() {
{/* Pounce Score */} -
+
Pounce Score @@ -311,12 +353,21 @@ export function AnalyzePanel() { {pounceScore}
- {pounceScore >= 80 ? 'Excellent' : pounceScore >= 60 ? 'Good' : pounceScore >= 40 ? 'Fair' : 'Poor'} + {pounceScore >= 80 ? 'Excellent investment' : pounceScore >= 60 ? 'Good opportunity' : pounceScore >= 40 ? 'Proceed with caution' : 'High risk'}
{/* Recommendation Badge */} -
+
{recommendation.label}
@@ -384,6 +435,7 @@ export function AnalyzePanel() { - - - -
Actions
+
Actions
{filteredItems.map((item) => { diff --git a/frontend/src/components/hunt/BrandableForgeTab.tsx b/frontend/src/components/hunt/BrandableForgeTab.tsx index ffdb338..1a86a8c 100644 --- a/frontend/src/components/hunt/BrandableForgeTab.tsx +++ b/frontend/src/components/hunt/BrandableForgeTab.tsx @@ -13,9 +13,8 @@ import { ShoppingCart, Sparkles, Lock, - Star, - Brain, RefreshCw, + Plus, } from 'lucide-react' import Link from 'next/link' import { api } from '@/lib/api' @@ -27,9 +26,9 @@ import { useStore } from '@/lib/store' // ============================================================================ const PATTERNS = [ - { key: 'cvcvc', label: 'CVCVC', examples: ['Zalor', 'Mivex', 'Ronix'], desc: 'Classic 5-letter' }, - { key: 'cvccv', label: 'CVCCV', examples: ['Bento', 'Salvo'], desc: 'Punchy sound' }, - { key: 'human', label: 'Human', examples: ['Siri', 'Alexa', 'Levi'], desc: 'AI agent names' }, + { key: 'cvcvc', label: 'CVCVC', example: 'Zalor' }, + { key: 'cvccv', label: 'CVCCV', example: 'Bento' }, + { key: 'human', label: 'Human', example: 'Alexa' }, ] const TLDS = ['com', 'io', 'ai', 'co', 'net', 'app'] @@ -45,26 +44,19 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type const tier = (subscription?.tier || '').toLowerCase() const hasAI = tier === 'trader' || tier === 'tycoon' - // Mode - const [mode, setMode] = useState<'pattern' | 'ai'>('pattern') - - // Pattern Mode + // Config const [pattern, setPattern] = useState('cvcvc') const [tlds, setTlds] = useState(['com', 'io']) - - // AI Mode const [concept, setConcept] = useState('') - const [similarBrand, setSimilarBrand] = useState('') - const [aiNames, setAiNames] = useState([]) - // Shared - const [results, setResults] = useState>([]) + // State + const [results, setResults] = useState>([]) const [loading, setLoading] = useState(false) const [aiLoading, setAiLoading] = useState(false) const [copied, setCopied] = useState(null) const [tracking, setTracking] = useState(null) - // Generate Pattern-based + // Generate from pattern const generatePattern = useCallback(async () => { if (tlds.length === 0) { showToast('Select at least one TLD', 'error') @@ -73,9 +65,9 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type setLoading(true) setResults([]) try { - const res = await api.huntBrandables({ pattern, tlds, limit: 30, max_checks: 400 }) - setResults(res.items.map(i => ({ domain: i.domain, available: true }))) - showToast(`Found ${res.items.length} brandable domains!`, 'success') + const res = await api.huntBrandables({ pattern, tlds, limit: 24, max_checks: 300 }) + setResults(res.items.map(i => ({ domain: i.domain }))) + showToast(`Found ${res.items.length} domains!`, 'success') } catch (e) { showToast('Generation failed', 'error') } finally { @@ -83,54 +75,25 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type } }, [pattern, tlds, showToast]) - // Generate AI-based + // Generate from AI concept const generateFromConcept = useCallback(async () => { if (!concept.trim() || !hasAI) return setAiLoading(true) - setAiNames([]) - setResults([]) try { - const res = await api.generateBrandableNames(concept.trim(), undefined, 15) - setAiNames(res.names || []) - showToast(`AI generated ${res.names?.length || 0} names!`, 'success') + const res = await api.generateBrandableNames(concept.trim(), undefined, 12) + if (res.names?.length) { + // Check availability + const checkRes = await api.huntKeywords({ keywords: res.names, tlds }) + const available = checkRes.items.filter(i => i.status === 'available') + setResults(available.map(i => ({ domain: i.domain }))) + showToast(`Found ${available.length} available from ${res.names.length} AI names!`, 'success') + } } catch (e) { showToast('AI generation failed', 'error') } finally { setAiLoading(false) } - }, [concept, hasAI, showToast]) - - const generateFromBrand = useCallback(async () => { - if (!similarBrand.trim() || !hasAI) return - setAiLoading(true) - setAiNames([]) - setResults([]) - try { - const res = await api.generateSimilarNames(similarBrand.trim(), 12) - setAiNames(res.names || []) - showToast(`Found ${res.names?.length || 0} similar names!`, 'success') - } catch (e) { - showToast('AI generation failed', 'error') - } finally { - setAiLoading(false) - } - }, [similarBrand, hasAI, showToast]) - - // Check AI names availability - const checkAiNames = useCallback(async () => { - if (aiNames.length === 0 || tlds.length === 0) return - setLoading(true) - try { - const res = await api.huntKeywords({ keywords: aiNames, tlds }) - const available = res.items.filter(i => i.status === 'available') - setResults(available.map(i => ({ domain: i.domain, available: true }))) - showToast(`${available.length} available!`, 'success') - } catch (e) { - showToast('Check failed', 'error') - } finally { - setLoading(false) - } - }, [aiNames, tlds, showToast]) + }, [concept, hasAI, tlds, showToast]) // Actions const copy = (domain: string) => { @@ -139,6 +102,11 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type setTimeout(() => setCopied(null), 1500) } + const copyAll = () => { + navigator.clipboard.writeText(results.map(r => r.domain).join('\n')) + showToast(`Copied ${results.length} domains`, 'success') + } + const track = async (domain: string) => { if (tracking) return setTracking(domain) @@ -152,260 +120,158 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type } } - const copyAll = () => { - if (results.length === 0) return - navigator.clipboard.writeText(results.map(r => r.domain).join('\n')) - showToast(`Copied ${results.length} domains`, 'success') - } + const isGenerating = loading || aiLoading return ( -
+
{/* ═══════════════════════════════════════════════════════════════════════ */} - {/* HEADER + MODE TOGGLE */} + {/* HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-

- - Brandable Forge -

-

- Generate unique, memorable domain names -

-
-
- - -
+
+

+ + Brandable Forge +

+

+ Generate unique, memorable domain names +

{/* ═══════════════════════════════════════════════════════════════════════ */} - {/* PATTERN MODE */} + {/* GENERATOR */} {/* ═══════════════════════════════════════════════════════════════════════ */} - {mode === 'pattern' && ( -
- {/* Pattern Selection */} -
-

Choose Pattern

-
- {PATTERNS.map(p => ( - - ))} -
+
+ + {/* Row 1: Pattern Selection */} +
+

Pattern

+
+ {PATTERNS.map(p => ( + + ))}
- - {/* 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" - /> + {/* Row 2: TLDs */} +
+

TLDs

+
+ {TLDS.map(tld => ( + + ))} +
+
+ + {/* Row 3: Generate Button */} + + + {/* Divider */} +
+
+
+
+
+ or use AI +
+
+ + {/* Row 4: AI Concept */} +
+
+

AI Concept Generator

+ {!hasAI && ( + + + Trader+ + + )} +
+
+ setConcept(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && hasAI && generateFromConcept()} + disabled={!hasAI} + placeholder={hasAI ? "Describe your brand concept..." : "Upgrade to Trader to unlock AI"} + className={clsx( + "flex-1 px-3 py-2.5 border text-sm font-mono outline-none transition-all", + hasAI + ? "bg-purple-500/5 border-purple-500/20 text-white placeholder:text-white/25 focus:border-purple-500/50" + : "bg-white/[0.02] border-white/10 text-white/30 placeholder:text-white/20" + )} + /> + {hasAI ? ( -
-
- - {/* 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" - /> - -
+ Upgrade + + )}
- - {/* AI Names */} - {aiNames.length > 0 && ( -
-
-

- AI Suggestions ({aiNames.length}) -

- -
-
- {TLDS.map(tld => ( - - ))} -
-
- {aiNames.map(name => ( - - {name} - - ))} -
-
+ {hasAI && ( +

+ Examples: "AI startup for legal documents", "crypto wallet for teens", "fitness app" +

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

AI features require Trader or Tycoon

- - - Upgrade - -
- )} +
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* RESULTS */} @@ -413,73 +279,62 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type {results.length > 0 && (
-

- {results.length} available domains +

+ {results.length} available domains

-
+
-
+
{results.map((r, idx) => (
- - {String(idx + 1).padStart(2, '0')} + + {idx + 1}
-
- - - - - Buy + Buy
@@ -489,14 +344,20 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type )} {/* Empty State */} - {results.length === 0 && !loading && !aiLoading && ( -
- -

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

+ {results.length === 0 && !isGenerating && ( +
+ +

Choose a pattern and generate

+

or describe your concept for AI suggestions

+
+ )} + + {/* Loading State */} + {isGenerating && results.length === 0 && ( +
+ {[...Array(12)].map((_, i) => ( +
+ ))}
)}
diff --git a/frontend/src/components/hunt/SearchTab.tsx b/frontend/src/components/hunt/SearchTab.tsx index f7f7d3e..4e0d5d5 100644 --- a/frontend/src/components/hunt/SearchTab.tsx +++ b/frontend/src/components/hunt/SearchTab.tsx @@ -342,7 +342,7 @@ export function SearchTab({ showToast }: SearchTabProps) { ) : (
{/* Result Row */}
@@ -351,29 +351,32 @@ export function SearchTab({ showToast }: SearchTabProps) { {/* Status Icon */}
{searchResult.is_available ? ( ) : ( - + )}
{/* Domain Info */}
-
{searchResult.domain}
+
{searchResult.domain}
{searchResult.is_available ? 'Available' : 'Taken'} {searchResult.registrar && ( <> | - + {searchResult.registrar} @@ -382,7 +385,7 @@ export function SearchTab({ showToast }: SearchTabProps) { {searchResult.expiration_date && ( <> | - + Expires {new Date(searchResult.expiration_date).toLocaleDateString()} @@ -397,7 +400,7 @@ export function SearchTab({ showToast }: SearchTabProps) { @@ -409,9 +412,9 @@ export function SearchTab({ showToast }: SearchTabProps) { "w-9 h-9 flex items-center justify-center border transition-colors", searchResult.is_available ? "border-white/10 text-white/30 hover:text-white hover:bg-white/5" - : "border-accent/30 text-accent hover:bg-accent/10" + : "border-rose-500/30 text-rose-400 hover:bg-rose-500/10" )} - title={searchResult.is_available ? "Track" : "Monitor for drops"} + title={searchResult.is_available ? "Add to watchlist to track this domain" : "Monitor this domain and get notified when it drops"} > {addingToWatchlist ? : } @@ -422,20 +425,21 @@ export function SearchTab({ showToast }: SearchTabProps) { target="_blank" rel="noopener noreferrer" className="h-9 px-4 bg-accent text-black text-xs font-bold uppercase tracking-wider flex items-center gap-2 hover:bg-white transition-colors" + title="Register this domain now via Namecheap" > Register ) : ( - - Find Similar - - + {addingToWatchlist ? : } + Monitor + )}
@@ -467,13 +471,19 @@ export function SearchTab({ showToast }: SearchTabProps) { {tldResults.map((result) => (
{ if (result.is_available && !result.loading) { @@ -493,19 +503,19 @@ export function SearchTab({ showToast }: SearchTabProps) {
.{result.tld} {result.is_available ? ( ) : ( - + )}
{result.is_available ? 'Available' : 'Taken'}
diff --git a/frontend/src/components/hunt/TrendSurferTab.tsx b/frontend/src/components/hunt/TrendSurferTab.tsx index 4409000..0685739 100644 --- a/frontend/src/components/hunt/TrendSurferTab.tsx +++ b/frontend/src/components/hunt/TrendSurferTab.tsx @@ -14,9 +14,9 @@ import { Flame, Sparkles, Lock, - ChevronRight, Globe, X, + ChevronDown, } from 'lucide-react' import Link from 'next/link' import { api } from '@/lib/api' @@ -54,10 +54,14 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?: const [trends, setTrends] = useState>([]) const [selected, setSelected] = useState(null) const [tlds, setTlds] = useState(['com', 'io', 'ai']) + + // Keywords to check (original + AI expanded) + const [keywords, setKeywords] = useState([]) + const [aiLoading, setAiLoading] = useState(false) + + // Results const [results, setResults] = useState>([]) const [checking, setChecking] = useState(false) - const [aiKeywords, setAiKeywords] = useState([]) - const [aiLoading, setAiLoading] = useState(false) const [copied, setCopied] = useState(null) const [tracking, setTracking] = useState(null) @@ -78,35 +82,62 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?: loadTrends() }, [loadTrends]) - // Check availability - const checkAvailability = useCallback(async (keyword: string) => { - if (!keyword || tlds.length === 0) return + // When a trend is selected, set the base keyword and auto-expand with AI if available + const selectTrend = useCallback(async (trend: string) => { + const baseKeyword = trend.toLowerCase().replace(/\s+/g, '') + setSelected(trend) + setKeywords([baseKeyword]) + setResults([]) + + // Auto-expand with AI if available + if (hasAI) { + setAiLoading(true) + try { + const res = await api.expandTrendKeywords(trend, geo) + if (res.keywords?.length) { + // Combine base + AI keywords, remove duplicates + const all = [baseKeyword, ...res.keywords.filter(k => k !== baseKeyword)] + setKeywords(all.slice(0, 8)) // Max 8 keywords + } + } catch (e) { + // Silent fail for AI + } finally { + setAiLoading(false) + } + } + }, [geo, hasAI]) + + // Check availability for all keywords + const checkAvailability = useCallback(async () => { + if (keywords.length === 0 || tlds.length === 0) return setChecking(true) setResults([]) try { - const kw = keyword.toLowerCase().replace(/\s+/g, '') - const res = await api.huntKeywords({ keywords: [kw], tlds }) + const res = await api.huntKeywords({ keywords, tlds }) setResults(res.items.map(i => ({ domain: i.domain, available: i.status === 'available' }))) + const avail = res.items.filter(i => i.status === 'available').length + showToast(`Found ${avail} available domains!`, 'success') } catch (e) { showToast('Check failed', 'error') } finally { setChecking(false) } - }, [tlds, showToast]) + }, [keywords, tlds, showToast]) - // AI Expand - const expandWithAI = useCallback(async () => { - if (!selected || !hasAI) return - setAiLoading(true) - try { - const res = await api.expandTrendKeywords(selected, geo) - setAiKeywords(res.keywords || []) - } catch (e) { - showToast('AI expansion failed', 'error') - } finally { - setAiLoading(false) + // Remove a keyword + const removeKeyword = (kw: string) => { + setKeywords(prev => prev.filter(k => k !== kw)) + } + + // Add custom keyword + const [customKw, setCustomKw] = useState('') + const addKeyword = () => { + const kw = customKw.trim().toLowerCase().replace(/\s+/g, '') + if (kw && !keywords.includes(kw)) { + setKeywords(prev => [...prev, kw]) + setCustomKw('') } - }, [selected, geo, hasAI, showToast]) + } // Actions const copy = (domain: string) => { @@ -129,28 +160,29 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?: } const currentGeo = GEOS.find(g => g.code === geo) - const availableCount = results.filter(r => r.available).length + const availableResults = results.filter(r => r.available) + const takenResults = results.filter(r => !r.available) return ( -
+
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
+
-

+

- Trending Now + Trend Surfer

-

- Real-time Google Trends → Domain opportunities +

+ Find domains for trending topics • {currentGeo?.flag} {currentGeo?.name}

setCustomKw(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && addKeyword()} + placeholder="+ add" + className="w-20 px-2 py-1 bg-transparent border border-white/10 text-xs font-mono text-white placeholder:text-white/20 outline-none focus:border-white/30" + /> +
+
+
+ + {/* TLDs */} +
+

TLDs

{TLDS.map(tld => ( +
+ )} - {/* AI Expansion */} -
-
-

- - AI Related Keywords - {!hasAI && } + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* RESULTS */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {results.length > 0 && ( +

+ {/* Available */} + {availableResults.length > 0 && ( +
+

+ + {availableResults.length} Available

- {hasAI ? ( - - ) : ( - - Upgrade - - )} -
- {aiKeywords.length > 0 && ( -
- {aiKeywords.map(kw => ( - - ))} -
- )} -
- - {/* Results */} - {results.length > 0 && ( -
-

- {availableCount} of {results.length} available -

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

Select a trend to find available domains

+
+ +

Select a trending topic above

+

We'll find available domains for you

)}