Trends & Forge v2: clearer AI integration, unified layout, auto-expand keywords
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:
@ -101,11 +101,50 @@ function getStatusStyle(status: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const SECTION_CONFIG: Record<string, { icon: any; color: string; label: string }> = {
|
||||
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<string, { icon: any; color: string; label: string; description: string }> = {
|
||||
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<string, string> = {
|
||||
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() {
|
||||
<div className="px-5 pb-4">
|
||||
<div className="flex gap-3">
|
||||
{/* Pounce Score */}
|
||||
<div className={clsx("flex-1 p-4 border", getScoreBg(pounceScore))}>
|
||||
<div
|
||||
className={clsx("flex-1 p-4 border cursor-help", getScoreBg(pounceScore))}
|
||||
title="Pounce Score: Combined rating based on authority, market potential, and risk factors. 80+ is excellent, 60+ is good, below 40 needs caution."
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[9px] font-mono text-white/40 uppercase tracking-wider">Pounce Score</span>
|
||||
<Sparkles className={clsx("w-4 h-4", getScoreColor(pounceScore))} />
|
||||
@ -311,12 +353,21 @@ export function AnalyzePanel() {
|
||||
{pounceScore}
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-white/30 mt-1">
|
||||
{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'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recommendation Badge */}
|
||||
<div className={clsx("w-32 p-4 border flex flex-col items-center justify-center", recommendation.color)}>
|
||||
<div
|
||||
className={clsx("w-32 p-4 border flex flex-col items-center justify-center cursor-help", recommendation.color)}
|
||||
title={
|
||||
recommendation.label === 'BUY' ? 'Strong buy signal - this domain has excellent metrics' :
|
||||
recommendation.label === 'CONSIDER' ? 'Worth considering - do additional research' :
|
||||
recommendation.label === 'RISKY' ? 'Trademark risk detected - legal issues possible' :
|
||||
recommendation.label === 'TAKEN' ? 'Domain is not available for registration' :
|
||||
'Not recommended for purchase at this time'
|
||||
}
|
||||
>
|
||||
<recommendation.icon className="w-6 h-6 mb-2" />
|
||||
<span className="text-sm font-bold uppercase tracking-wider">{recommendation.label}</span>
|
||||
</div>
|
||||
@ -384,6 +435,7 @@ export function AnalyzePanel() {
|
||||
<button
|
||||
key={section.key}
|
||||
onClick={() => setActiveSection(section.key)}
|
||||
title={config.description}
|
||||
className={clsx(
|
||||
"flex items-center gap-1.5 px-3 py-2 border transition-all shrink-0",
|
||||
isActive ? colors.active : colors.inactive + ' hover:bg-white/[0.02]'
|
||||
@ -424,23 +476,44 @@ export function AnalyzePanel() {
|
||||
) : (
|
||||
<div className="p-5">
|
||||
{/* Active Section Items */}
|
||||
{data.sections.filter(s => s.key === activeSection).map((section) => (
|
||||
<div key={section.key} className="space-y-2">
|
||||
{data.sections.filter(s => s.key === activeSection).map((section) => {
|
||||
const sectionConfig = SECTION_CONFIG[section.key] || SECTION_CONFIG.authority
|
||||
return (
|
||||
<div key={section.key} className="space-y-3">
|
||||
{/* Section Description */}
|
||||
<div className="pb-3 mb-3 border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<sectionConfig.icon className="w-4 h-4 text-white/40" />
|
||||
<span className="text-sm font-bold text-white">{sectionConfig.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-white/40 font-mono">{sectionConfig.description}</p>
|
||||
</div>
|
||||
|
||||
{section.items.map((item) => {
|
||||
const statusStyle = getStatusStyle(item.status)
|
||||
const StatusIcon = statusStyle.icon
|
||||
const tooltip = ITEM_TOOLTIPS[item.key] || ''
|
||||
|
||||
// Special handling for TLD Matrix
|
||||
if (item.key === 'tld_matrix' && Array.isArray(item.value)) {
|
||||
return (
|
||||
<div key={item.key} className="p-4 bg-white/[0.02] border border-white/[0.06]">
|
||||
<div className="text-xs font-mono text-white/40 uppercase tracking-wider mb-3">{item.label}</div>
|
||||
<div key={item.key} className="p-4 bg-white/[0.02] border border-white/[0.06]" title={tooltip}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-xs font-mono text-white/40 uppercase tracking-wider">{item.label}</div>
|
||||
<div className="group relative">
|
||||
<Info className="w-3.5 h-3.5 text-white/20 hover:text-white/40 cursor-help" />
|
||||
<div className="absolute right-0 top-6 w-48 p-2 bg-black border border-white/20 text-[10px] text-white/60 hidden group-hover:block z-10">
|
||||
{tooltip}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-1.5">
|
||||
{(item.value as any[]).slice(0, 12).map((row: any) => (
|
||||
<div
|
||||
key={String(row.domain)}
|
||||
title={row.status === 'available' ? `${row.domain} is available!` : `${row.domain} is taken`}
|
||||
className={clsx(
|
||||
"px-2 py-1.5 text-[10px] font-mono flex items-center justify-between border",
|
||||
"px-2 py-1.5 text-[10px] font-mono flex items-center justify-between border cursor-default",
|
||||
row.status === 'available'
|
||||
? "border-accent/30 bg-accent/5 text-accent"
|
||||
: "border-white/[0.06] bg-white/[0.01] text-white/30"
|
||||
@ -459,24 +532,37 @@ export function AnalyzePanel() {
|
||||
<div
|
||||
key={item.key}
|
||||
className={clsx(
|
||||
"p-4 border transition-colors",
|
||||
"p-4 border transition-colors group",
|
||||
statusStyle.bg, statusStyle.border
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Status Icon */}
|
||||
<div className={clsx(
|
||||
"w-8 h-8 flex items-center justify-center shrink-0",
|
||||
statusStyle.bg, "border", statusStyle.border
|
||||
)}>
|
||||
<div
|
||||
className={clsx(
|
||||
"w-8 h-8 flex items-center justify-center shrink-0",
|
||||
statusStyle.bg, "border", statusStyle.border
|
||||
)}
|
||||
title={item.status === 'pass' ? 'Good' : item.status === 'warn' ? 'Warning' : item.status === 'fail' ? 'Issue' : 'Info'}
|
||||
>
|
||||
{StatusIcon && <StatusIcon className={clsx("w-4 h-4", statusStyle.text)} />}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
<span className="text-sm font-medium text-white">{item.label}</span>
|
||||
<span className="text-[9px] font-mono text-white/30 uppercase">{item.source}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-white">{item.label}</span>
|
||||
{tooltip && (
|
||||
<div className="relative">
|
||||
<Info className="w-3 h-3 text-white/20 hover:text-white/40 cursor-help" />
|
||||
<div className="absolute left-0 top-5 w-56 p-2 bg-black border border-white/20 text-[10px] text-white/60 hidden group-hover:block z-10 font-mono">
|
||||
{tooltip}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[9px] font-mono text-white/30 uppercase" title="Data source">{item.source}</span>
|
||||
</div>
|
||||
|
||||
<div className={clsx(
|
||||
@ -494,22 +580,22 @@ export function AnalyzePanel() {
|
||||
return (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{d.syllables !== undefined && (
|
||||
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50">
|
||||
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50" title="Number of syllables - fewer is better for memorability">
|
||||
{d.syllables} syllables
|
||||
</span>
|
||||
)}
|
||||
{d.length !== undefined && (
|
||||
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50">
|
||||
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50" title="Character count - shorter domains are more valuable">
|
||||
{d.length} chars
|
||||
</span>
|
||||
)}
|
||||
{d.has_hyphen && (
|
||||
<span className="px-2 py-0.5 bg-amber-500/10 border border-amber-500/20 text-[10px] font-mono text-amber-400">
|
||||
<span className="px-2 py-0.5 bg-amber-500/10 border border-amber-500/20 text-[10px] font-mono text-amber-400" title="Hyphens reduce brandability and resale value">
|
||||
has hyphen
|
||||
</span>
|
||||
)}
|
||||
{d.has_digits && (
|
||||
<span className="px-2 py-0.5 bg-amber-500/10 border border-amber-500/20 text-[10px] font-mono text-amber-400">
|
||||
<span className="px-2 py-0.5 bg-amber-500/10 border border-amber-500/20 text-[10px] font-mono text-amber-400" title="Numbers can reduce memorability">
|
||||
has digits
|
||||
</span>
|
||||
)}
|
||||
@ -521,7 +607,7 @@ export function AnalyzePanel() {
|
||||
{(item.key === 'tld_cheapest_register_usd' || item.key === 'tld_cheapest_renew_usd') && item.details && (() => {
|
||||
const d = item.details as Record<string, any>
|
||||
return d.registrar ? (
|
||||
<div className="mt-1 text-[10px] font-mono text-white/30">
|
||||
<div className="mt-1 text-[10px] font-mono text-white/30" title="Cheapest registrar offering this price">
|
||||
via {d.registrar}
|
||||
</div>
|
||||
) : null
|
||||
@ -532,7 +618,7 @@ export function AnalyzePanel() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
)})
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="mt-6 pt-4 border-t border-white/[0.06]">
|
||||
|
||||
@ -438,23 +438,23 @@ export function AuctionsTab({ showToast }: AuctionsTabProps) {
|
||||
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
|
||||
{/* Desktop Table Header */}
|
||||
<div className="hidden lg:grid grid-cols-[1fr_80px_100px_80px_120px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
|
||||
<button onClick={() => handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
|
||||
<button onClick={() => handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left" title="Domain name being auctioned">
|
||||
Domain
|
||||
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
|
||||
</button>
|
||||
<button onClick={() => handleSort('score')} className="flex items-center gap-1 justify-center hover:text-white/60">
|
||||
<button onClick={() => handleSort('score')} className="flex items-center gap-1 justify-center hover:text-white/60" title="Pounce Score - our AI-powered quality rating (0-100)">
|
||||
Score
|
||||
{sortField === 'score' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
|
||||
</button>
|
||||
<button onClick={() => handleSort('price')} className="flex items-center gap-1 justify-end hover:text-white/60">
|
||||
<button onClick={() => handleSort('price')} className="flex items-center gap-1 justify-end hover:text-white/60" title="Current bid or buy-now price">
|
||||
Price
|
||||
{sortField === 'price' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
|
||||
</button>
|
||||
<button onClick={() => handleSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60">
|
||||
<button onClick={() => handleSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60" title="Time remaining until auction ends">
|
||||
Time
|
||||
{sortField === 'time' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
|
||||
</button>
|
||||
<div className="text-right">Actions</div>
|
||||
<div className="text-right" title="Available actions: Analyze, Track, Buy">Actions</div>
|
||||
</div>
|
||||
|
||||
{filteredItems.map((item) => {
|
||||
|
||||
@ -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<string[]>([])
|
||||
|
||||
// Shared
|
||||
const [results, setResults] = useState<Array<{ domain: string; available: boolean }>>([])
|
||||
// State
|
||||
const [results, setResults] = useState<Array<{ domain: string }>>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
const [copied, setCopied] = useState<string | null>(null)
|
||||
const [tracking, setTracking] = useState<string | null>(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 (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HEADER + MODE TOGGLE */}
|
||||
{/* HEADER */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<Wand2 className="w-5 h-5 text-purple-400" />
|
||||
Brandable Forge
|
||||
</h2>
|
||||
<p className="text-xs text-white/40 font-mono mt-0.5">
|
||||
Generate unique, memorable domain names
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={() => { setMode('pattern'); setResults([]); setAiNames([]) }}
|
||||
className={clsx(
|
||||
"px-4 py-2 text-xs font-bold uppercase tracking-wider border-y border-l transition-all",
|
||||
mode === 'pattern'
|
||||
? "bg-accent/10 border-accent text-accent"
|
||||
: "bg-white/5 border-white/10 text-white/50 hover:text-white"
|
||||
)}
|
||||
>
|
||||
Patterns
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { if (hasAI) { setMode('ai'); setResults([]); setAiNames([]) }}}
|
||||
disabled={!hasAI}
|
||||
className={clsx(
|
||||
"px-4 py-2 text-xs font-bold uppercase tracking-wider border transition-all flex items-center gap-1.5",
|
||||
!hasAI && "opacity-50",
|
||||
mode === 'ai'
|
||||
? "bg-purple-500/10 border-purple-500 text-purple-400"
|
||||
: "bg-white/5 border-white/10 text-white/50 hover:text-white"
|
||||
)}
|
||||
>
|
||||
<Brain className="w-3.5 h-3.5" />
|
||||
AI
|
||||
{!hasAI && <Lock className="w-3 h-3" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="pb-4 border-b border-white/10">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Wand2 className="w-5 h-5 text-purple-400" />
|
||||
Brandable Forge
|
||||
</h2>
|
||||
<p className="text-sm text-white/40 mt-1">
|
||||
Generate unique, memorable domain names
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* PATTERN MODE */}
|
||||
{/* GENERATOR */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{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 className="bg-white/[0.02] border border-white/10 p-4 space-y-5">
|
||||
|
||||
{/* Row 1: Pattern Selection */}
|
||||
<div>
|
||||
<p className="text-xs text-white/40 font-mono uppercase tracking-wider mb-2">Pattern</p>
|
||||
<div className="flex gap-2">
|
||||
{PATTERNS.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => setPattern(p.key)}
|
||||
className={clsx(
|
||||
"flex-1 py-3 px-4 border text-center 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/60"
|
||||
)}>
|
||||
{p.label}
|
||||
</p>
|
||||
<p className="text-[10px] text-white/30 mt-0.5">{p.example}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* TLD Selection */}
|
||||
<div>
|
||||
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Select TLDs</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{TLDS.map(tld => (
|
||||
<button
|
||||
key={tld}
|
||||
onClick={() => setTlds(prev =>
|
||||
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
|
||||
)}
|
||||
className={clsx(
|
||||
"px-3 py-1.5 text-xs font-mono border transition-all",
|
||||
tlds.includes(tld)
|
||||
? "border-accent bg-accent/20 text-accent"
|
||||
: "border-white/10 text-white/40 hover:text-white"
|
||||
)}
|
||||
>
|
||||
.{tld}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<button
|
||||
onClick={generatePattern}
|
||||
disabled={loading || tlds.length === 0}
|
||||
className={clsx(
|
||||
"w-full py-3 text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all",
|
||||
loading || tlds.length === 0
|
||||
? "bg-white/10 text-white/30"
|
||||
: "bg-accent text-black hover:bg-white"
|
||||
)}
|
||||
>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
|
||||
Generate Brandables
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* AI MODE */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{mode === 'ai' && hasAI && (
|
||||
<div className="border border-purple-500/20 bg-purple-500/5 p-4 space-y-4">
|
||||
{/* Concept Input */}
|
||||
<div>
|
||||
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Describe Your Brand</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={concept}
|
||||
onChange={(e) => setConcept(e.target.value)}
|
||||
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 */}
|
||||
<div>
|
||||
<p className="text-xs text-white/40 font-mono uppercase tracking-wider mb-2">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/10 text-accent"
|
||||
: "border-white/10 text-white/30 hover:text-white"
|
||||
)}
|
||||
>
|
||||
.{tld}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Generate Button */}
|
||||
<button
|
||||
onClick={generatePattern}
|
||||
disabled={isGenerating || tlds.length === 0}
|
||||
className={clsx(
|
||||
"w-full py-3.5 text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all",
|
||||
isGenerating || 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 {pattern.toUpperCase()} Brandables
|
||||
</button>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative py-2">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-white/10" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="px-3 bg-[#0a0a0a] text-[10px] text-white/30 font-mono uppercase">or use AI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 4: AI Concept */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<p className="text-xs text-white/40 font-mono uppercase tracking-wider">AI Concept Generator</p>
|
||||
{!hasAI && (
|
||||
<span className="flex items-center gap-1 text-[10px] text-white/20">
|
||||
<Lock className="w-3 h-3" />
|
||||
Trader+
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={concept}
|
||||
onChange={(e) => 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 ? (
|
||||
<button
|
||||
onClick={generateFromConcept}
|
||||
disabled={!concept.trim() || aiLoading}
|
||||
className={clsx(
|
||||
"px-4 text-xs font-bold uppercase flex items-center gap-1.5 transition-all shrink-0",
|
||||
"px-4 text-sm font-bold uppercase flex items-center gap-2 transition-all shrink-0",
|
||||
!concept.trim() || aiLoading
|
||||
? "bg-white/10 text-white/30"
|
||||
: "bg-purple-500 text-white hover:bg-purple-400"
|
||||
)}
|
||||
>
|
||||
{aiLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Zap className="w-3.5 h-3.5" />}
|
||||
{aiLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
|
||||
<span className="hidden sm:inline">Generate</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OR Divider */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-px bg-white/10" />
|
||||
<span className="text-[10px] text-white/30 font-mono">OR</span>
|
||||
<div className="flex-1 h-px bg-white/10" />
|
||||
</div>
|
||||
|
||||
{/* Similar Brand Input */}
|
||||
<div>
|
||||
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Find Names Like...</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={similarBrand}
|
||||
onChange={(e) => setSimilarBrand(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
onClick={generateFromBrand}
|
||||
disabled={!similarBrand.trim() || aiLoading}
|
||||
className={clsx(
|
||||
"px-4 text-xs font-bold uppercase flex items-center gap-1.5 transition-all shrink-0",
|
||||
!similarBrand.trim() || aiLoading
|
||||
? "bg-white/10 text-white/30"
|
||||
: "bg-purple-500 text-white hover:bg-purple-400"
|
||||
)}
|
||||
) : (
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="px-4 flex items-center gap-2 bg-white/5 border border-white/10 text-white/40 text-xs font-bold uppercase hover:text-white hover:border-white/20 transition-all"
|
||||
>
|
||||
{aiLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Star className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
Upgrade
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* AI Names */}
|
||||
{aiNames.length > 0 && (
|
||||
<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">
|
||||
AI Suggestions ({aiNames.length})
|
||||
</p>
|
||||
<button
|
||||
onClick={checkAiNames}
|
||||
disabled={loading || tlds.length === 0}
|
||||
className="text-[10px] font-mono text-purple-400 hover:text-purple-300 flex items-center gap-1"
|
||||
>
|
||||
<Zap className="w-3 h-3" />
|
||||
Check Availability
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
{TLDS.map(tld => (
|
||||
<button
|
||||
key={tld}
|
||||
onClick={() => setTlds(prev =>
|
||||
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
|
||||
)}
|
||||
className={clsx(
|
||||
"px-2 py-1 text-[10px] font-mono border transition-all",
|
||||
tlds.includes(tld)
|
||||
? "border-purple-500/40 text-purple-400"
|
||||
: "border-white/10 text-white/30"
|
||||
)}
|
||||
>
|
||||
.{tld}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{aiNames.map(name => (
|
||||
<span key={name} className="px-3 py-1.5 text-xs font-mono text-purple-300 bg-purple-500/10 border border-purple-500/20">
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{hasAI && (
|
||||
<p className="text-[10px] text-white/20 mt-1.5">
|
||||
Examples: "AI startup for legal documents", "crypto wallet for teens", "fitness app"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upgrade CTA for Scout */}
|
||||
{mode === 'ai' && !hasAI && (
|
||||
<div className="border border-white/10 bg-white/[0.02] p-6 text-center">
|
||||
<Lock className="w-8 h-8 text-white/20 mx-auto mb-2" />
|
||||
<p className="text-sm text-white/60 mb-3">AI features require Trader or Tycoon</p>
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-accent text-black text-xs font-bold uppercase"
|
||||
>
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
Upgrade
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* RESULTS */}
|
||||
@ -413,73 +279,62 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type
|
||||
{results.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-white/50 font-mono">
|
||||
{results.length} available domains
|
||||
<p className="text-sm text-white/50">
|
||||
<span className="text-accent font-bold">{results.length}</span> available domains
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={copyAll}
|
||||
className="text-[10px] font-mono text-accent hover:text-white flex items-center gap-1"
|
||||
className="text-xs font-mono text-white/40 hover:text-accent flex items-center gap-1"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy All
|
||||
Copy all
|
||||
</button>
|
||||
<button
|
||||
onClick={() => mode === 'pattern' ? generatePattern() : checkAiNames()}
|
||||
disabled={loading}
|
||||
className="text-[10px] font-mono text-white/40 hover:text-white flex items-center gap-1"
|
||||
onClick={generatePattern}
|
||||
disabled={isGenerating}
|
||||
className="text-xs 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", isGenerating && "animate-spin")} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{results.map((r, idx) => (
|
||||
<div
|
||||
key={r.domain}
|
||||
className="flex items-center justify-between p-3 border border-accent/20 bg-accent/5 hover:bg-accent/10 transition-all"
|
||||
className="flex items-center justify-between p-3 bg-accent/5 border border-accent/20 hover:bg-accent/10 transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<span className="w-6 h-6 bg-accent/20 text-accent text-[10px] font-bold font-mono flex items-center justify-center shrink-0">
|
||||
{String(idx + 1).padStart(2, '0')}
|
||||
<span className="w-5 h-5 bg-accent/20 text-accent text-[9px] font-bold font-mono flex items-center justify-center shrink-0">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => openAnalyze(r.domain)}
|
||||
className="text-sm font-mono font-medium text-white truncate hover:text-accent"
|
||||
className="text-sm font-mono font-medium text-white truncate group-hover:text-accent"
|
||||
>
|
||||
{r.domain}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => copy(r.domain)}
|
||||
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
|
||||
>
|
||||
{copied === r.domain ? <Check className="w-3.5 h-3.5 text-accent" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
<div className="flex items-center gap-0.5 shrink-0 opacity-60 group-hover:opacity-100">
|
||||
<button onClick={() => copy(r.domain)} className="w-7 h-7 flex items-center justify-center text-white/50 hover:text-white">
|
||||
{copied === r.domain ? <Check className="w-3 h-3 text-accent" /> : <Copy className="w-3 h-3" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => track(r.domain)}
|
||||
disabled={tracking === r.domain}
|
||||
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
|
||||
>
|
||||
{tracking === r.domain ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
<button onClick={() => track(r.domain)} disabled={tracking === r.domain} className="w-7 h-7 flex items-center justify-center text-white/50 hover:text-white">
|
||||
{tracking === r.domain ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openAnalyze(r.domain)}
|
||||
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-accent"
|
||||
>
|
||||
<Shield className="w-3.5 h-3.5" />
|
||||
<button onClick={() => openAnalyze(r.domain)} className="w-7 h-7 flex items-center justify-center text-white/50 hover:text-accent">
|
||||
<Shield className="w-3 h-3" />
|
||||
</button>
|
||||
<a
|
||||
href={`https://www.namecheap.com/domains/registration/results/?domain=${r.domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="h-8 px-2.5 bg-accent text-black text-[10px] font-bold flex items-center gap-1"
|
||||
className="h-7 px-2 bg-accent text-black text-[9px] font-bold flex items-center"
|
||||
>
|
||||
<ShoppingCart className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">Buy</span>
|
||||
Buy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -489,14 +344,20 @@ export function BrandableForgeTab({ showToast }: { showToast: (msg: string, type
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{results.length === 0 && !loading && !aiLoading && (
|
||||
<div className="text-center py-10 border border-dashed border-white/10">
|
||||
<Wand2 className="w-8 h-8 text-white/20 mx-auto mb-2" />
|
||||
<p className="text-sm text-white/40">
|
||||
{mode === 'pattern'
|
||||
? 'Select a pattern and click Generate'
|
||||
: 'Describe your concept or enter a brand name'}
|
||||
</p>
|
||||
{results.length === 0 && !isGenerating && (
|
||||
<div className="text-center py-12 border border-dashed border-white/10">
|
||||
<Wand2 className="w-10 h-10 text-white/10 mx-auto mb-3" />
|
||||
<p className="text-white/40">Choose a pattern and generate</p>
|
||||
<p className="text-sm text-white/20 mt-1">or describe your concept for AI suggestions</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{isGenerating && results.length === 0 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<div key={i} className="h-12 bg-white/5 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -342,7 +342,7 @@ export function SearchTab({ showToast }: SearchTabProps) {
|
||||
) : (
|
||||
<div className={clsx(
|
||||
"border-2 overflow-hidden bg-[#020202]",
|
||||
searchResult.is_available ? "border-accent/40" : "border-white/[0.08]"
|
||||
searchResult.is_available ? "border-accent/40" : "border-rose-500/30"
|
||||
)}>
|
||||
{/* Result Row */}
|
||||
<div className="p-4">
|
||||
@ -351,29 +351,32 @@ export function SearchTab({ showToast }: SearchTabProps) {
|
||||
{/* Status Icon */}
|
||||
<div className={clsx(
|
||||
"w-12 h-12 flex items-center justify-center border shrink-0",
|
||||
searchResult.is_available ? "bg-accent/10 border-accent/30" : "bg-white/[0.02] border-white/[0.08]"
|
||||
searchResult.is_available ? "bg-accent/10 border-accent/30" : "bg-rose-500/10 border-rose-500/30"
|
||||
)}>
|
||||
{searchResult.is_available ? (
|
||||
<CheckCircle2 className="w-6 h-6 text-accent" />
|
||||
) : (
|
||||
<XCircle className="w-6 h-6 text-white/30" />
|
||||
<XCircle className="w-6 h-6 text-rose-500" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Domain Info */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-lg font-bold text-white font-mono truncate">{searchResult.domain}</div>
|
||||
<div className={clsx(
|
||||
"text-lg font-bold font-mono truncate",
|
||||
searchResult.is_available ? "text-white" : "text-rose-400"
|
||||
)}>{searchResult.domain}</div>
|
||||
<div className="flex items-center gap-3 text-[10px] font-mono text-white/40 mt-1">
|
||||
<span className={clsx(
|
||||
"px-2 py-0.5 uppercase font-bold",
|
||||
searchResult.is_available ? "bg-accent/20 text-accent" : "bg-white/10 text-white/50"
|
||||
searchResult.is_available ? "bg-accent/20 text-accent" : "bg-rose-500/20 text-rose-400"
|
||||
)}>
|
||||
{searchResult.is_available ? 'Available' : 'Taken'}
|
||||
</span>
|
||||
{searchResult.registrar && (
|
||||
<>
|
||||
<span className="text-white/10">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="flex items-center gap-1" title="Current registrar holding this domain">
|
||||
<Building className="w-3 h-3" />
|
||||
{searchResult.registrar}
|
||||
</span>
|
||||
@ -382,7 +385,7 @@ export function SearchTab({ showToast }: SearchTabProps) {
|
||||
{searchResult.expiration_date && (
|
||||
<>
|
||||
<span className="text-white/10">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="flex items-center gap-1" title="Domain expiration date - monitor for potential drop">
|
||||
<Calendar className="w-3 h-3" />
|
||||
Expires {new Date(searchResult.expiration_date).toLocaleDateString()}
|
||||
</span>
|
||||
@ -397,7 +400,7 @@ export function SearchTab({ showToast }: SearchTabProps) {
|
||||
<button
|
||||
onClick={() => openAnalyze(searchResult.domain)}
|
||||
className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/30 hover:text-accent hover:border-accent/20 hover:bg-accent/10 transition-colors"
|
||||
title="Deep Analysis"
|
||||
title="Deep Analysis - View SEO metrics, backlinks, and valuation"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
</button>
|
||||
@ -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 ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
@ -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
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
href={`https://www.expireddomains.net/domain-name-search/?q=${searchResult.domain.split('.')[0]}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="h-9 px-4 bg-white/10 text-white text-xs font-bold uppercase tracking-wider flex items-center gap-2 hover:bg-white/20 transition-colors"
|
||||
<button
|
||||
onClick={handleAddToWatchlist}
|
||||
disabled={addingToWatchlist}
|
||||
className="h-9 px-4 bg-rose-500/20 text-rose-400 text-xs font-bold uppercase tracking-wider flex items-center gap-2 hover:bg-rose-500/30 border border-rose-500/30 transition-colors"
|
||||
title="Add to watchlist and get notified when this domain becomes available"
|
||||
>
|
||||
Find Similar
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
{addingToWatchlist ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
Monitor
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -467,13 +471,19 @@ export function SearchTab({ showToast }: SearchTabProps) {
|
||||
{tldResults.map((result) => (
|
||||
<div
|
||||
key={result.tld}
|
||||
title={result.loading
|
||||
? `Checking ${result.domain}...`
|
||||
: result.is_available
|
||||
? `${result.domain} is available! Click to see details.`
|
||||
: `${result.domain} is already registered`
|
||||
}
|
||||
className={clsx(
|
||||
"p-3 border transition-all",
|
||||
result.loading
|
||||
? "border-white/[0.08] bg-white/[0.02]"
|
||||
: result.is_available
|
||||
? "border-accent/40 bg-accent/[0.05] hover:bg-accent/10 cursor-pointer"
|
||||
: "border-white/[0.06] bg-white/[0.01]"
|
||||
: "border-rose-500/20 bg-rose-500/[0.02]"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (result.is_available && !result.loading) {
|
||||
@ -493,19 +503,19 @@ export function SearchTab({ showToast }: SearchTabProps) {
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className={clsx(
|
||||
"text-xs font-mono font-bold",
|
||||
result.is_available ? "text-accent" : "text-white/30"
|
||||
result.is_available ? "text-accent" : "text-rose-400/60"
|
||||
)}>
|
||||
.{result.tld}
|
||||
</span>
|
||||
{result.is_available ? (
|
||||
<CheckCircle2 className="w-3.5 h-3.5 text-accent" />
|
||||
) : (
|
||||
<XCircle className="w-3.5 h-3.5 text-white/20" />
|
||||
<XCircle className="w-3.5 h-3.5 text-rose-400/40" />
|
||||
)}
|
||||
</div>
|
||||
<div className={clsx(
|
||||
"text-[9px] font-mono uppercase",
|
||||
result.is_available ? "text-accent/60" : "text-white/20"
|
||||
result.is_available ? "text-accent/60" : "text-rose-400/40"
|
||||
)}>
|
||||
{result.is_available ? 'Available' : 'Taken'}
|
||||
</div>
|
||||
|
||||
@ -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<Array<{ title: string; approx_traffic?: string | null }>>([])
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
const [tlds, setTlds] = useState(['com', 'io', 'ai'])
|
||||
|
||||
// Keywords to check (original + AI expanded)
|
||||
const [keywords, setKeywords] = useState<string[]>([])
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
|
||||
// Results
|
||||
const [results, setResults] = useState<Array<{ domain: string; available: boolean }>>([])
|
||||
const [checking, setChecking] = useState(false)
|
||||
const [aiKeywords, setAiKeywords] = useState<string[]>([])
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
const [copied, setCopied] = useState<string | null>(null)
|
||||
const [tracking, setTracking] = useState<string | null>(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 (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HEADER */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-4 border-b border-white/10">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Flame className="w-5 h-5 text-orange-400" />
|
||||
Trending Now
|
||||
Trend Surfer
|
||||
</h2>
|
||||
<p className="text-xs text-white/40 font-mono mt-0.5">
|
||||
Real-time Google Trends → Domain opportunities
|
||||
<p className="text-sm text-white/40 mt-1">
|
||||
Find domains for trending topics • {currentGeo?.flag} {currentGeo?.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={geo}
|
||||
onChange={(e) => { setGeo(e.target.value); setSelected(null); setResults([]); setAiKeywords([]) }}
|
||||
className="h-10 px-3 bg-white/5 border border-white/10 text-sm font-mono text-white outline-none"
|
||||
onChange={(e) => { setGeo(e.target.value); setSelected(null); setKeywords([]); setResults([]) }}
|
||||
className="h-9 px-3 bg-white/5 border border-white/10 text-sm text-white outline-none"
|
||||
>
|
||||
{GEOS.map(g => (
|
||||
<option key={g.code} value={g.code}>{g.flag} {g.name}</option>
|
||||
@ -159,7 +191,7 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
|
||||
<button
|
||||
onClick={loadTrends}
|
||||
disabled={loading}
|
||||
className="h-10 w-10 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5"
|
||||
className="h-9 w-9 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", loading && "animate-spin")} />
|
||||
</button>
|
||||
@ -169,72 +201,122 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* TRENDS GRID */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div key={i} className="h-16 bg-white/5 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||
{trends.slice(0, 16).map((t, idx) => {
|
||||
const isSelected = selected === t.title
|
||||
const isHot = idx < 3
|
||||
return (
|
||||
<button
|
||||
key={t.title}
|
||||
onClick={() => {
|
||||
setSelected(isSelected ? null : t.title)
|
||||
setResults([])
|
||||
setAiKeywords([])
|
||||
}}
|
||||
className={clsx(
|
||||
"relative p-3 text-left border transition-all",
|
||||
isSelected
|
||||
? "border-accent bg-accent/10"
|
||||
: "border-white/10 bg-white/[0.02] hover:border-white/20 hover:bg-white/[0.04]"
|
||||
)}
|
||||
>
|
||||
{isHot && (
|
||||
<span className="absolute top-2 right-2 text-[10px]">🔥</span>
|
||||
)}
|
||||
<p className={clsx(
|
||||
"text-sm font-medium truncate",
|
||||
isSelected ? "text-accent" : "text-white/80"
|
||||
)}>
|
||||
{t.title}
|
||||
</p>
|
||||
{t.approx_traffic && (
|
||||
<p className="text-[10px] text-white/30 font-mono mt-1">{t.approx_traffic}</p>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-xs text-white/30 font-mono uppercase tracking-wider mb-3">
|
||||
Select a trending topic
|
||||
</p>
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div key={i} className="h-14 bg-white/5 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{trends.slice(0, 12).map((t, idx) => {
|
||||
const isSelected = selected === t.title
|
||||
return (
|
||||
<button
|
||||
key={t.title}
|
||||
onClick={() => selectTrend(t.title)}
|
||||
className={clsx(
|
||||
"relative p-3 text-left border transition-all",
|
||||
isSelected
|
||||
? "border-accent bg-accent/10"
|
||||
: "border-white/10 bg-white/[0.02] hover:border-white/20"
|
||||
)}
|
||||
>
|
||||
{idx < 3 && <span className="absolute top-1.5 right-1.5 text-[10px]">🔥</span>}
|
||||
<p className={clsx(
|
||||
"text-sm font-medium truncate pr-4",
|
||||
isSelected ? "text-accent" : "text-white/80"
|
||||
)}>
|
||||
{t.title}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* SELECTED TREND PANEL */}
|
||||
{/* KEYWORD BUILDER */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{selected && (
|
||||
<div className="border border-accent/30 bg-accent/5 p-4 space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="bg-white/[0.02] border border-white/10 p-4 space-y-4">
|
||||
{/* Selected Trend Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-accent font-mono uppercase tracking-wider">Selected Trend</p>
|
||||
<h3 className="text-xl font-bold text-white mt-1">{selected}</h3>
|
||||
<p className="text-xs text-white/40 font-mono uppercase tracking-wider">Building domains for</p>
|
||||
<h3 className="text-lg font-bold text-white">{selected}</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setSelected(null); setResults([]); setAiKeywords([]) }}
|
||||
className="p-2 text-white/40 hover:text-white"
|
||||
onClick={() => { setSelected(null); setKeywords([]); setResults([]) }}
|
||||
className="p-2 text-white/30 hover:text-white"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* TLD Selection */}
|
||||
{/* Keywords */}
|
||||
<div>
|
||||
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">Select TLDs</p>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<p className="text-xs text-white/40 font-mono uppercase tracking-wider">Keywords to check</p>
|
||||
{aiLoading && (
|
||||
<span className="flex items-center gap-1 text-[10px] text-purple-400">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
AI expanding...
|
||||
</span>
|
||||
)}
|
||||
{hasAI && keywords.length > 1 && !aiLoading && (
|
||||
<span className="flex items-center gap-1 text-[10px] text-purple-400">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
AI expanded
|
||||
</span>
|
||||
)}
|
||||
{!hasAI && (
|
||||
<Link href="/pricing" className="flex items-center gap-1 text-[10px] text-white/30 hover:text-accent">
|
||||
<Lock className="w-3 h-3" />
|
||||
Upgrade for AI
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
{keywords.map((kw, idx) => (
|
||||
<span
|
||||
key={kw}
|
||||
className={clsx(
|
||||
"inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-mono border",
|
||||
idx === 0
|
||||
? "bg-accent/10 border-accent/30 text-accent"
|
||||
: "bg-purple-500/10 border-purple-500/20 text-purple-400"
|
||||
)}
|
||||
>
|
||||
{kw}
|
||||
{keywords.length > 1 && (
|
||||
<button onClick={() => removeKeyword(kw)} className="hover:text-white">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
{/* Add custom keyword */}
|
||||
<div className="flex">
|
||||
<input
|
||||
value={customKw}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TLDs */}
|
||||
<div>
|
||||
<p className="text-xs text-white/40 font-mono uppercase tracking-wider mb-2">TLDs</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{TLDS.map(tld => (
|
||||
<button
|
||||
@ -243,10 +325,10 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
|
||||
prev.includes(tld) ? prev.filter(t => t !== tld) : [...prev, tld]
|
||||
)}
|
||||
className={clsx(
|
||||
"px-3 py-1.5 text-xs font-mono border transition-all",
|
||||
"px-2.5 py-1 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"
|
||||
? "border-accent bg-accent/10 text-accent"
|
||||
: "border-white/10 text-white/30 hover:text-white"
|
||||
)}
|
||||
>
|
||||
.{tld}
|
||||
@ -257,134 +339,96 @@ export function TrendSurferTab({ showToast }: { showToast: (msg: string, type?:
|
||||
|
||||
{/* Check Button */}
|
||||
<button
|
||||
onClick={() => checkAvailability(selected)}
|
||||
disabled={checking || tlds.length === 0}
|
||||
onClick={checkAvailability}
|
||||
disabled={checking || tlds.length === 0 || keywords.length === 0}
|
||||
className={clsx(
|
||||
"w-full py-3 text-sm font-bold uppercase tracking-wider flex items-center justify-center gap-2 transition-all",
|
||||
checking || tlds.length === 0
|
||||
checking || tlds.length === 0 || keywords.length === 0
|
||||
? "bg-white/10 text-white/30"
|
||||
: "bg-accent text-black hover:bg-white"
|
||||
)}
|
||||
>
|
||||
{checking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
|
||||
Check {selected.toLowerCase().replace(/\s+/g, '')} Availability
|
||||
Check {keywords.length} × {tlds.length} = {keywords.length * tlds.length} Domains
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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" />}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* RESULTS */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{results.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{/* Available */}
|
||||
{availableResults.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-accent font-mono uppercase tracking-wider mb-2 flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-accent rounded-full" />
|
||||
{availableResults.length} Available
|
||||
</p>
|
||||
{hasAI ? (
|
||||
<button
|
||||
onClick={expandWithAI}
|
||||
disabled={aiLoading}
|
||||
className="text-[10px] font-mono text-purple-400 hover:text-purple-300 flex items-center gap-1"
|
||||
>
|
||||
{aiLoading ? <Loader2 className="w-3 h-3 animate-spin" /> : <ChevronRight className="w-3 h-3" />}
|
||||
Expand
|
||||
</button>
|
||||
) : (
|
||||
<Link href="/pricing" className="text-[10px] font-mono text-accent">
|
||||
Upgrade
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{aiKeywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{aiKeywords.map(kw => (
|
||||
<button
|
||||
key={kw}
|
||||
onClick={() => checkAvailability(kw)}
|
||||
className="px-2.5 py-1 text-[11px] font-mono text-purple-400 bg-purple-500/10 border border-purple-500/20 hover:bg-purple-500/20"
|
||||
>
|
||||
{kw}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{results.length > 0 && (
|
||||
<div className="pt-3 border-t border-white/10">
|
||||
<p className="text-[10px] text-white/40 font-mono uppercase tracking-wider mb-2">
|
||||
{availableCount} of {results.length} available
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{results.map(r => (
|
||||
<div
|
||||
key={r.domain}
|
||||
className={clsx(
|
||||
"flex items-center justify-between p-2.5 border transition-all",
|
||||
r.available
|
||||
? "border-accent/30 bg-accent/5"
|
||||
: "border-white/5 bg-white/[0.02]"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className={clsx(
|
||||
"w-2 h-2 rounded-full shrink-0",
|
||||
r.available ? "bg-accent" : "bg-white/20"
|
||||
)} />
|
||||
<button
|
||||
onClick={() => openAnalyze(r.domain)}
|
||||
className={clsx(
|
||||
"text-sm font-mono truncate",
|
||||
r.available ? "text-white hover:text-accent" : "text-white/40"
|
||||
)}
|
||||
>
|
||||
{r.domain}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{availableResults.map(r => (
|
||||
<div key={r.domain} className="flex items-center justify-between p-2.5 bg-accent/5 border border-accent/20">
|
||||
<button
|
||||
onClick={() => openAnalyze(r.domain)}
|
||||
className="text-sm font-mono text-white hover:text-accent truncate"
|
||||
>
|
||||
{r.domain}
|
||||
</button>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => copy(r.domain)}
|
||||
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
|
||||
>
|
||||
<button onClick={() => copy(r.domain)} className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white">
|
||||
{copied === r.domain ? <Check className="w-3.5 h-3.5 text-accent" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => track(r.domain)}
|
||||
disabled={tracking === r.domain}
|
||||
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white"
|
||||
>
|
||||
<button onClick={() => track(r.domain)} disabled={tracking === r.domain} className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white">
|
||||
{tracking === r.domain ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openAnalyze(r.domain)}
|
||||
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-accent"
|
||||
>
|
||||
<button onClick={() => openAnalyze(r.domain)} className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-accent">
|
||||
<Shield className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
{r.available && (
|
||||
<a
|
||||
href={`https://www.namecheap.com/domains/registration/results/?domain=${r.domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="h-8 px-2.5 bg-accent text-black text-[10px] font-bold flex items-center gap-1"
|
||||
>
|
||||
<ShoppingCart className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">Buy</span>
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href={`https://www.namecheap.com/domains/registration/results/?domain=${r.domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="h-8 px-3 bg-accent text-black text-[10px] font-bold flex items-center gap-1"
|
||||
>
|
||||
<ShoppingCart className="w-3 h-3" />
|
||||
Buy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Taken (collapsed) */}
|
||||
{takenResults.length > 0 && (
|
||||
<details className="group">
|
||||
<summary className="text-xs text-white/30 font-mono uppercase tracking-wider cursor-pointer flex items-center gap-2 py-2">
|
||||
<ChevronDown className="w-3 h-3 group-open:rotate-180 transition-transform" />
|
||||
{takenResults.length} Taken
|
||||
</summary>
|
||||
<div className="space-y-1 mt-2">
|
||||
{takenResults.map(r => (
|
||||
<div key={r.domain} className="flex items-center justify-between p-2 bg-white/[0.02] border border-white/5">
|
||||
<span className="text-sm font-mono text-white/30 truncate">{r.domain}</span>
|
||||
<button onClick={() => openAnalyze(r.domain)} className="text-white/20 hover:text-white">
|
||||
<Shield className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!selected && !loading && trends.length > 0 && (
|
||||
<div className="text-center py-8 border border-dashed border-white/10">
|
||||
<Globe className="w-8 h-8 text-white/20 mx-auto mb-2" />
|
||||
<p className="text-sm text-white/40">Select a trend to find available domains</p>
|
||||
<div className="text-center py-12 border border-dashed border-white/10">
|
||||
<Globe className="w-10 h-10 text-white/10 mx-auto mb-3" />
|
||||
<p className="text-white/40">Select a trending topic above</p>
|
||||
<p className="text-sm text-white/20 mt-1">We'll find available domains for you</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user