Complete AnalyzePanel redesign: Hunt-style, Pounce Score, Buy/Skip, Yield Intent, Trademark Warning
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:
2025-12-17 09:58:50 +01:00
parent 90ec2648fc
commit 2553c7d4c4
2 changed files with 471 additions and 276 deletions

View File

@ -15,63 +15,97 @@ import {
Zap, Zap,
Globe, Globe,
Calendar, Calendar,
Link2,
Radio, Radio,
Eye, Eye,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
CheckCircle2, CheckCircle2,
XCircle, XCircle,
Sparkles,
Target,
Coins,
ShoppingCart,
Ban,
AlertCircle,
Info,
Bookmark,
ArrowRight,
} from 'lucide-react' } from 'lucide-react'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { useAnalyzePanelStore } from '@/lib/analyze-store' import { useAnalyzePanelStore } from '@/lib/analyze-store'
import type { AnalyzeResponse, AnalyzeSection, AnalyzeItem } from '@/components/analyze/types' import type { AnalyzeResponse, AnalyzeSection, AnalyzeItem } from '@/components/analyze/types'
// ============================================================================
// KNOWN TRADEMARKS (for warning)
// ============================================================================
const KNOWN_TRADEMARKS = [
'google', 'facebook', 'meta', 'apple', 'microsoft', 'amazon', 'netflix', 'spotify',
'nike', 'adidas', 'puma', 'gucci', 'louis vuitton', 'chanel', 'rolex', 'omega',
'tesla', 'bmw', 'mercedes', 'audi', 'porsche', 'ferrari', 'lamborghini',
'coca cola', 'pepsi', 'mcdonalds', 'starbucks', 'burger king', 'subway',
'disney', 'marvel', 'pixar', 'warner', 'paramount', 'universal',
'visa', 'mastercard', 'paypal', 'stripe', 'shopify', 'airbnb',
'twitter', 'instagram', 'tiktok', 'snapchat', 'linkedin', 'whatsapp',
'youtube', 'twitch', 'reddit', 'pinterest', 'dropbox', 'slack', 'zoom',
'samsung', 'sony', 'lg', 'nintendo', 'playstation', 'xbox', 'nvidia', 'intel', 'amd',
'ibm', 'oracle', 'salesforce', 'adobe', 'autodesk', 'atlassian',
'swisscom', 'sunrise', 'salt', 'ubs', 'credit suisse', 'zurich', 'swiss re',
'migros', 'coop', 'denner', 'lidl', 'aldi', 'sbb', 'post', 'swiss',
]
function checkTrademarkRisk(domain: string): { risk: boolean; match: string | null } {
const name = domain.split('.')[0].toLowerCase().replace(/[-_0-9]/g, '')
for (const tm of KNOWN_TRADEMARKS) {
const cleanTm = tm.replace(/\s+/g, '')
if (name.includes(cleanTm) || cleanTm.includes(name)) {
return { risk: true, match: tm }
}
}
return { risk: false, match: null }
}
// ============================================================================ // ============================================================================
// HELPERS // HELPERS
// ============================================================================ // ============================================================================
function getStatusColor(status: string) { function getScoreColor(score: number) {
if (score >= 80) return 'text-accent'
if (score >= 60) return 'text-emerald-400'
if (score >= 40) return 'text-amber-400'
return 'text-red-400'
}
function getScoreBg(score: number) {
if (score >= 80) return 'bg-accent/20 border-accent/40'
if (score >= 60) return 'bg-emerald-500/20 border-emerald-500/40'
if (score >= 40) return 'bg-amber-500/20 border-amber-500/40'
return 'bg-red-500/20 border-red-500/40'
}
function getRecommendation(score: number, trademarkRisk: boolean, isAvailable: boolean) {
if (trademarkRisk) return { label: 'RISKY', color: 'text-red-400 bg-red-500/20 border-red-500/40', icon: Ban }
if (!isAvailable) return { label: 'TAKEN', color: 'text-white/40 bg-white/10 border-white/20', icon: XCircle }
if (score >= 75) return { label: 'BUY', color: 'text-accent bg-accent/20 border-accent/40', icon: ShoppingCart }
if (score >= 50) return { label: 'CONSIDER', color: 'text-amber-400 bg-amber-500/20 border-amber-500/40', icon: Eye }
return { label: 'SKIP', color: 'text-white/40 bg-white/10 border-white/20', icon: Ban }
}
function getStatusStyle(status: string) {
switch (status) { switch (status) {
case 'pass': case 'pass': return { bg: 'bg-accent/10', text: 'text-accent', border: 'border-accent/30', icon: CheckCircle2 }
return { bg: 'bg-accent/20', text: 'text-accent', border: 'border-accent/40', icon: CheckCircle2 } case 'warn': return { bg: 'bg-amber-400/10', text: 'text-amber-400', border: 'border-amber-400/30', icon: AlertTriangle }
case 'warn': case 'fail': return { bg: 'bg-red-500/10', text: 'text-red-400', border: 'border-red-500/30', icon: XCircle }
return { bg: 'bg-amber-400/20', text: 'text-amber-300', border: 'border-amber-400/40', icon: AlertTriangle } case 'info': return { bg: 'bg-blue-500/10', text: 'text-blue-400', border: 'border-blue-500/30', icon: Info }
case 'fail': default: return { bg: 'bg-white/5', text: 'text-white/40', border: 'border-white/10', icon: null }
return { bg: 'bg-red-500/20', text: 'text-red-400', border: 'border-red-500/40', icon: XCircle }
default:
return { bg: 'bg-white/10', text: 'text-white/50', border: 'border-white/20', icon: null }
} }
} }
function getSectionIcon(key: string) { const SECTION_CONFIG: Record<string, { icon: any; color: string; label: string }> = {
switch (key) { authority: { icon: Shield, color: 'blue', label: 'Authority' },
case 'authority': market: { icon: TrendingUp, color: 'emerald', label: 'Market' },
return Shield risk: { icon: AlertTriangle, color: 'amber', label: 'Risk' },
case 'market': value: { icon: DollarSign, color: 'violet', label: 'Value' },
return TrendingUp
case 'risk':
return AlertTriangle
case 'value':
return DollarSign
default:
return Globe
}
}
function getSectionColor(key: string) {
switch (key) {
case 'authority':
return { text: 'text-blue-400', bg: 'bg-blue-500/10', border: 'border-blue-500/30' }
case 'market':
return { text: 'text-emerald-400', bg: 'bg-emerald-500/10', border: 'border-emerald-500/30' }
case 'risk':
return { text: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/30' }
case 'value':
return { text: 'text-violet-400', bg: 'bg-violet-500/10', border: 'border-violet-500/30' }
default:
return { text: 'text-white/60', bg: 'bg-white/5', border: 'border-white/20' }
}
} }
async function copyToClipboard(text: string) { async function copyToClipboard(text: string) {
@ -92,35 +126,19 @@ function formatValue(value: unknown): string {
return 'Details' return 'Details'
} }
function isMatrix(item: AnalyzeItem) {
return item.key === 'tld_matrix' && Array.isArray(item.value)
}
// ============================================================================ // ============================================================================
// COMPONENT // COMPONENT
// ============================================================================ // ============================================================================
export function AnalyzePanel() { export function AnalyzePanel() {
const { const { isOpen, domain, close, fastMode, setFastMode } = useAnalyzePanelStore()
isOpen,
domain,
close,
fastMode,
setFastMode,
sectionVisibility,
setSectionVisibility
} = useAnalyzePanelStore()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [data, setData] = useState<AnalyzeResponse | null>(null) const [data, setData] = useState<AnalyzeResponse | null>(null)
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({ const [activeSection, setActiveSection] = useState<string>('authority')
authority: true, const [yieldIntent, setYieldIntent] = useState<any>(null)
market: true,
risk: true,
value: true
})
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
if (!domain) return if (!domain) return
@ -129,6 +147,11 @@ export function AnalyzePanel() {
try { try {
const res = await api.analyzeDomain(domain, { fast: fastMode, refresh: true }) const res = await api.analyzeDomain(domain, { fast: fastMode, refresh: true })
setData(res) setData(res)
// Also fetch yield intent
try {
const yieldRes = await api.analyzeYieldDomain(domain)
setYieldIntent(yieldRes)
} catch { setYieldIntent(null) }
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : String(e)) setError(e instanceof Error ? e.message : String(e))
setData(null) setData(null)
@ -143,13 +166,16 @@ export function AnalyzePanel() {
const run = async () => { const run = async () => {
setLoading(true) setLoading(true)
setError(null) setError(null)
setYieldIntent(null)
try { try {
const res = await api.analyzeDomain(domain, { fast: fastMode, refresh: false }) const [res, yieldRes] = await Promise.allSettled([
if (!cancelled) setData(res) api.analyzeDomain(domain, { fast: fastMode, refresh: false }),
} catch (e) { api.analyzeYieldDomain(domain),
])
if (!cancelled) { if (!cancelled) {
setError(e instanceof Error ? e.message : String(e)) if (res.status === 'fulfilled') setData(res.value)
setData(null) else setError(res.reason?.message || 'Analysis failed')
if (yieldRes.status === 'fulfilled') setYieldIntent(yieldRes.value)
} }
} finally { } finally {
if (!cancelled) setLoading(false) if (!cancelled) setLoading(false)
@ -162,28 +188,15 @@ export function AnalyzePanel() {
// ESC to close // ESC to close
useEffect(() => { useEffect(() => {
if (!isOpen) return if (!isOpen) return
const onKey = (ev: KeyboardEvent) => { const onKey = (ev: KeyboardEvent) => { if (ev.key === 'Escape') close() }
if (ev.key === 'Escape') close()
}
window.addEventListener('keydown', onKey) window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey)
}, [isOpen, close]) }, [isOpen, close])
const toggleSection = useCallback((key: string) => { // Calculate Pounce Score from data
setExpandedSections(prev => ({ ...prev, [key]: !prev[key] })) const pounceScore = useMemo(() => {
}, []) if (!data?.sections) return 50
let score = 50
const visibleSections = useMemo(() => {
const sections = data?.sections || []
const order = ['authority', 'market', 'risk', 'value']
return [...sections]
.sort((a, b) => order.indexOf(a.key) - order.indexOf(b.key))
.filter((s) => sectionVisibility[s.key] !== false)
}, [data, sectionVisibility])
// Calculate overall score
const overallScore = useMemo(() => {
if (!data?.sections) return null
let pass = 0, warn = 0, fail = 0 let pass = 0, warn = 0, fail = 0
data.sections.forEach(s => { data.sections.forEach(s => {
s.items.forEach(item => { s.items.forEach(item => {
@ -193,11 +206,27 @@ export function AnalyzePanel() {
}) })
}) })
const total = pass + warn + fail const total = pass + warn + fail
if (total === 0) return null if (total > 0) score = Math.round((pass * 100 + warn * 50) / total)
const score = Math.round((pass * 100 + warn * 50) / total) return Math.min(100, Math.max(0, score))
return { score, pass, warn, fail, total }
}, [data]) }, [data])
// Check trademark risk
const trademark = useMemo(() => checkTrademarkRisk(domain || ''), [domain])
// Is available?
const isAvailable = useMemo(() => {
const availItem = data?.sections
?.find(s => s.key === 'authority')
?.items.find(i => i.key === 'availability')
return availItem?.value === 'available'
}, [data])
// Recommendation
const recommendation = useMemo(
() => getRecommendation(pounceScore, trademark.risk, isAvailable),
[pounceScore, trademark.risk, isAvailable]
)
const headerDomain = data?.domain || domain || '' const headerDomain = data?.domain || domain || ''
if (!isOpen) return null if (!isOpen) return null
@ -205,28 +234,30 @@ export function AnalyzePanel() {
return ( return (
<div className="fixed inset-0 z-[200]"> <div className="fixed inset-0 z-[200]">
{/* Backdrop */} {/* Backdrop */}
<div className="absolute inset-0 bg-black/85 backdrop-blur-md" onClick={close} /> <div className="absolute inset-0 bg-black/90 backdrop-blur-sm" onClick={close} />
{/* Panel - WIDER & MORE READABLE */} {/* Panel */}
<div className="absolute right-0 top-0 bottom-0 w-full sm:w-[600px] lg:w-[680px] bg-[#0A0A0A] border-l border-white/10 flex flex-col overflow-hidden shadow-2xl"> <div className="absolute right-0 top-0 bottom-0 w-full sm:w-[540px] lg:w-[600px] bg-[#0A0A0A] border-l border-white/[0.08] flex flex-col overflow-hidden">
{/* Header */} {/* ════════════════════════════════════════════════════════════════════ */}
<div className="shrink-0 border-b border-white/10 bg-[#050505]"> {/* HEADER */}
{/* ════════════════════════════════════════════════════════════════════ */}
<div className="shrink-0 bg-[#050505] border-b border-white/[0.08]">
{/* Top Bar */} {/* Top Bar */}
<div className="px-6 py-5 flex items-center justify-between"> <div className="px-5 py-4 flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-3 min-w-0">
<div className="w-12 h-12 bg-accent/15 border border-accent/30 flex items-center justify-center"> <div className="w-10 h-10 bg-accent/10 border border-accent/30 flex items-center justify-center shrink-0">
<Shield className="w-6 h-6 text-accent" /> <Target className="w-5 h-5 text-accent" />
</div> </div>
<div> <div className="min-w-0">
<div className="text-xs font-mono text-accent uppercase tracking-widest mb-1">Domain Analysis</div> <div className="text-[9px] font-mono text-accent uppercase tracking-[0.2em] mb-0.5">Domain Analysis</div>
<div className="text-xl font-bold text-white font-mono truncate max-w-[300px]"> <div className="text-lg font-bold text-white font-mono truncate">
{headerDomain} {headerDomain}
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5">
<button <button
onClick={async () => { onClick={async () => {
const ok = await copyToClipboard(headerDomain) const ok = await copyToClipboard(headerDomain)
@ -234,231 +265,306 @@ export function AnalyzePanel() {
setTimeout(() => setCopied(false), 1500) setTimeout(() => setCopied(false), 1500)
}} }}
className={clsx( className={clsx(
"w-10 h-10 flex items-center justify-center border transition-all", "w-9 h-9 flex items-center justify-center border transition-all",
copied ? "border-accent bg-accent/20 text-accent" : "border-white/20 text-white/50 hover:text-white hover:bg-white/10" copied ? "border-accent/40 bg-accent/10 text-accent" : "border-white/[0.08] text-white/40 hover:text-white hover:bg-white/[0.02]"
)} )}
> >
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />} {copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</button> </button>
<a <a
href={`https://${encodeURIComponent(headerDomain)}`} href={`https://${encodeURIComponent(headerDomain)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-10 h-10 flex items-center justify-center border border-white/20 text-white/50 hover:text-white hover:bg-white/10 transition-colors" className="w-9 h-9 flex items-center justify-center border border-white/[0.08] text-white/40 hover:text-white hover:bg-white/[0.02] transition-colors"
> >
<ExternalLink className="w-5 h-5" /> <ExternalLink className="w-4 h-4" />
</a> </a>
<button <button
onClick={refresh} onClick={refresh}
disabled={loading} disabled={loading}
className="w-10 h-10 flex items-center justify-center border border-white/20 text-white/50 hover:text-white hover:bg-white/10 transition-colors disabled:opacity-50" className="w-9 h-9 flex items-center justify-center border border-white/[0.08] text-white/40 hover:text-white hover:bg-white/[0.02] transition-colors disabled:opacity-50"
> >
<RefreshCw className={clsx('w-5 h-5', loading && 'animate-spin')} /> <RefreshCw className={clsx('w-4 h-4', loading && 'animate-spin')} />
</button> </button>
<button <button
onClick={close} onClick={close}
className="w-10 h-10 flex items-center justify-center border border-white/20 text-white/50 hover:text-white hover:bg-white/10 transition-colors ml-2" className="w-9 h-9 flex items-center justify-center border border-white/[0.08] text-white/40 hover:text-white hover:bg-white/[0.02] transition-colors ml-1"
> >
<X className="w-5 h-5" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
</div> </div>
{/* Score Bar - LARGER */} {/* ════════════════════════════════════════════════════════════════════ */}
{overallScore && !loading && ( {/* HERO: Score + Recommendation */}
<div className="px-6 pb-5"> {/* ════════════════════════════════════════════════════════════════════ */}
<div className="flex items-center gap-4 p-4 bg-white/[0.03] border border-white/10"> {!loading && data && (
<div className={clsx( <div className="px-5 pb-4">
"text-4xl font-bold font-mono", <div className="flex gap-3">
overallScore.score >= 70 ? "text-accent" : overallScore.score >= 40 ? "text-amber-400" : "text-red-400" {/* Pounce Score */}
)}> <div className={clsx("flex-1 p-4 border", getScoreBg(pounceScore))}>
{overallScore.score} <div className="flex items-center justify-between mb-2">
</div> <span className="text-[9px] font-mono text-white/40 uppercase tracking-wider">Pounce Score</span>
<div className="flex-1"> <Sparkles className={clsx("w-4 h-4", getScoreColor(pounceScore))} />
<div className="text-xs font-mono text-white/50 uppercase tracking-wider mb-2">Overall Score</div> </div>
<div className="h-3 bg-white/10 overflow-hidden flex"> <div className={clsx("text-4xl font-bold font-mono", getScoreColor(pounceScore))}>
<div {pounceScore}
className="h-full bg-accent transition-all" </div>
style={{ width: `${(overallScore.pass / overallScore.total) * 100}%` }} <div className="text-[10px] font-mono text-white/30 mt-1">
/> {pounceScore >= 80 ? 'Excellent' : pounceScore >= 60 ? 'Good' : pounceScore >= 40 ? 'Fair' : 'Poor'}
<div
className="h-full bg-amber-400 transition-all"
style={{ width: `${(overallScore.warn / overallScore.total) * 100}%` }}
/>
<div
className="h-full bg-red-500 transition-all"
style={{ width: `${(overallScore.fail / overallScore.total) * 100}%` }}
/>
</div> </div>
</div> </div>
<div className="flex flex-col gap-1 text-sm font-mono">
<span className="text-accent flex items-center gap-2"><CheckCircle2 className="w-4 h-4" /> {overallScore.pass}</span> {/* Recommendation Badge */}
<span className="text-amber-400 flex items-center gap-2"><AlertTriangle className="w-4 h-4" /> {overallScore.warn}</span> <div className={clsx("w-32 p-4 border flex flex-col items-center justify-center", recommendation.color)}>
<span className="text-red-400 flex items-center gap-2"><XCircle className="w-4 h-4" /> {overallScore.fail}</span> <recommendation.icon className="w-6 h-6 mb-2" />
<span className="text-sm font-bold uppercase tracking-wider">{recommendation.label}</span>
</div> </div>
</div> </div>
{/* Trademark Warning */}
{trademark.risk && (
<div className="mt-3 p-3 bg-red-500/10 border border-red-500/30 flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
<div>
<div className="text-sm font-bold text-red-400">Trademark Risk Detected</div>
<div className="text-xs font-mono text-red-400/70 mt-0.5">
Contains "{trademark.match}" - potential legal issues. Research before buying.
</div>
</div>
</div>
)}
{/* Yield Intent Tip */}
{yieldIntent && yieldIntent.monetization_potential !== 'low' && (
<div className="mt-3 p-3 bg-accent/5 border border-accent/20 flex items-start gap-3">
<Coins className="w-5 h-5 text-accent shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-bold text-accent">Yield Potential</span>
<span className={clsx(
"px-1.5 py-0.5 text-[8px] font-mono uppercase",
yieldIntent.monetization_potential === 'high'
? "bg-accent/20 text-accent"
: "bg-amber-400/20 text-amber-400"
)}>
{yieldIntent.monetization_potential}
</span>
</div>
<div className="text-xs font-mono text-white/50">
{yieldIntent.intent?.category?.replace(/_/g, ' ')}
{yieldIntent.intent?.suggested_partners?.length > 0 && (
<span className="text-white/30"> {yieldIntent.intent.suggested_partners.slice(0, 2).join(', ')}</span>
)}
</div>
</div>
<ArrowRight className="w-4 h-4 text-accent/50 shrink-0" />
</div>
)}
</div> </div>
)} )}
{/* Mode Toggle */} {/* ════════════════════════════════════════════════════════════════════ */}
<div className="px-6 pb-4 flex items-center gap-3"> {/* SECTION TABS */}
<button {/* ════════════════════════════════════════════════════════════════════ */}
onClick={() => setFastMode(!fastMode)} {!loading && data && (
className={clsx( <div className="px-5 pb-3 flex gap-1.5 overflow-x-auto">
"flex items-center gap-2 px-4 py-2 text-sm font-bold uppercase tracking-wider border transition-all", {data.sections.map((section) => {
fastMode const config = SECTION_CONFIG[section.key] || SECTION_CONFIG.authority
? "border-accent/40 bg-accent/15 text-accent" const isActive = activeSection === section.key
: "border-white/20 text-white/50 hover:text-white hover:bg-white/10" const colorMap: Record<string, { active: string; inactive: string }> = {
)} blue: { active: 'border-blue-500 bg-blue-500/10 text-blue-400', inactive: 'border-white/[0.06] text-white/40' },
> emerald: { active: 'border-emerald-500 bg-emerald-500/10 text-emerald-400', inactive: 'border-white/[0.06] text-white/40' },
<Zap className="w-4 h-4" /> amber: { active: 'border-amber-500 bg-amber-500/10 text-amber-400', inactive: 'border-white/[0.06] text-white/40' },
Fast Mode violet: { active: 'border-violet-500 bg-violet-500/10 text-violet-400', inactive: 'border-white/[0.06] text-white/40' },
</button> }
{data?.cached && ( const colors = colorMap[config.color] || colorMap.blue
<span className="text-sm font-mono text-white/40 px-3 py-2 border border-white/20 bg-white/5">
Cached return (
</span> <button
)} key={section.key}
</div> onClick={() => setActiveSection(section.key)}
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]'
)}
>
<config.icon className="w-3.5 h-3.5" />
<span className="text-[10px] font-bold uppercase tracking-wider font-mono">{config.label}</span>
<span className="text-[9px] font-mono opacity-50">{section.items.length}</span>
</button>
)
})}
</div>
)}
</div> </div>
{/* Body - BETTER SPACING */} {/* ════════════════════════════════════════════════════════════════════ */}
{/* CONTENT */}
{/* ════════════════════════════════════════════════════════════════════ */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-24"> <div className="flex items-center justify-center py-20">
<div className="text-center"> <div className="text-center">
<RefreshCw className="w-10 h-10 text-accent animate-spin mx-auto mb-4" /> <RefreshCw className="w-8 h-8 text-accent animate-spin mx-auto mb-3" />
<div className="text-base font-mono text-white/50">Analyzing domain...</div> <div className="text-sm font-mono text-white/40">Analyzing domain...</div>
</div> </div>
</div> </div>
) : error ? ( ) : error ? (
<div className="p-6"> <div className="p-5">
<div className="border border-red-500/30 bg-red-500/10 p-6"> <div className="border border-red-500/30 bg-red-500/10 p-5">
<div className="text-lg font-bold text-red-400 mb-2">Analysis Failed</div> <div className="text-base font-bold text-red-400 mb-2">Analysis Failed</div>
<div className="text-sm font-mono text-white/60">{error}</div> <div className="text-sm font-mono text-white/50">{error}</div>
</div> </div>
</div> </div>
) : !data ? ( ) : !data ? (
<div className="flex items-center justify-center py-24"> <div className="flex items-center justify-center py-20">
<div className="text-base font-mono text-white/40">No data available</div> <div className="text-sm font-mono text-white/30">No data available</div>
</div> </div>
) : ( ) : (
<div className="p-6 space-y-4"> <div className="p-5">
{visibleSections.map((section) => { {/* Active Section Items */}
const SectionIcon = getSectionIcon(section.key) {data.sections.filter(s => s.key === activeSection).map((section) => (
const sectionStyle = getSectionColor(section.key) <div key={section.key} className="space-y-2">
const isExpanded = expandedSections[section.key] !== false {section.items.map((item) => {
const statusStyle = getStatusStyle(item.status)
const StatusIcon = statusStyle.icon
return ( // Special handling for TLD Matrix
<div key={section.key} className={clsx("border overflow-hidden", sectionStyle.border, "bg-[#050505]")}> if (item.key === 'tld_matrix' && Array.isArray(item.value)) {
{/* Section Header - LARGER */} return (
<button <div key={item.key} className="p-4 bg-white/[0.02] border border-white/[0.06]">
onClick={() => toggleSection(section.key)} <div className="text-xs font-mono text-white/40 uppercase tracking-wider mb-3">{item.label}</div>
className={clsx( <div className="grid grid-cols-4 gap-1.5">
"w-full px-5 py-4 flex items-center justify-between transition-colors", {(item.value as any[]).slice(0, 12).map((row: any) => (
sectionStyle.bg, "hover:brightness-110" <div
)} key={String(row.domain)}
> className={clsx(
<div className="flex items-center gap-3"> "px-2 py-1.5 text-[10px] font-mono flex items-center justify-between border",
<SectionIcon className={clsx("w-5 h-5", sectionStyle.text)} /> row.status === 'available'
<span className={clsx("text-sm font-bold uppercase tracking-wider", sectionStyle.text)}> ? "border-accent/30 bg-accent/5 text-accent"
{section.title} : "border-white/[0.06] bg-white/[0.01] text-white/30"
</span> )}
<span className="text-sm font-mono text-white/40 ml-2"> >
{section.items.length} checks <span className="truncate">.{row.tld}</span>
</span> {row.status === 'available' && <Check className="w-3 h-3 shrink-0 ml-1" />}
</div>
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-white/40" />
) : (
<ChevronDown className="w-5 h-5 text-white/40" />
)}
</button>
{/* Section Items - BETTER CONTRAST */}
{isExpanded && (
<div className="border-t border-white/10">
{section.items.map((item) => {
const statusStyle = getStatusColor(item.status)
const StatusIcon = statusStyle.icon
return (
<div
key={item.key}
className="px-5 py-4 border-b border-white/[0.06] last:border-0 hover:bg-white/[0.03] transition-colors"
>
<div className="flex items-start gap-4">
{/* Status Indicator - LARGER */}
<div className={clsx(
"w-10 h-10 flex items-center justify-center shrink-0",
statusStyle.bg, statusStyle.border, "border"
)}>
{StatusIcon && <StatusIcon className={clsx("w-5 h-5", statusStyle.text)} />}
</div>
{/* Content - BETTER READABILITY */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-4 mb-2">
<span className="text-base font-medium text-white">
{item.label}
</span>
<span className="text-xs font-mono text-white/40 uppercase tracking-wider">
{item.source}
</span>
</div>
{/* Value - LARGER TEXT */}
<div>
{isMatrix(item) ? (
<div className="grid grid-cols-4 gap-2">
{(item.value as any[]).slice(0, 12).map((row: any) => (
<div
key={String(row.domain)}
className={clsx(
"px-3 py-2 text-sm font-mono flex items-center justify-between border",
row.status === 'available'
? "border-accent/30 bg-accent/10 text-accent"
: "border-white/10 bg-white/[0.03] text-white/50"
)}
>
<span className="truncate">{String(row.domain)}</span>
{row.status === 'available' && <Check className="w-4 h-4 shrink-0 ml-2" />}
</div>
))}
</div>
) : (
<div className={clsx(
"text-base font-mono",
item.status === 'pass' ? "text-white/80" :
item.status === 'warn' ? "text-amber-300" :
item.status === 'fail' ? "text-red-300" : "text-white/50"
)}>
{formatValue(item.value)}
</div>
)}
</div>
{/* Details Toggle */}
{item.details && Object.keys(item.details).length > 0 && (
<details className="mt-3">
<summary className="text-sm font-mono text-white/40 cursor-pointer hover:text-white/60 select-none">
View raw details
</summary>
<pre className="mt-2 text-xs font-mono text-white/50 bg-black/50 border border-white/10 p-4 overflow-x-auto">
{JSON.stringify(item.details, null, 2)}
</pre>
</details>
)}
</div>
</div> </div>
))}
</div>
</div>
)
}
return (
<div
key={item.key}
className={clsx(
"p-4 border transition-colors",
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
)}>
{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> </div>
)
})} <div className={clsx(
"text-sm font-mono",
item.status === 'pass' ? "text-white/70" :
item.status === 'warn' ? "text-amber-300/80" :
item.status === 'fail' ? "text-red-300/80" : "text-white/40"
)}>
{formatValue(item.value)}
</div>
{/* Radio Test Details */}
{item.key === 'radio_test' && item.details && (
<div className="mt-2 flex flex-wrap gap-2">
{item.details.syllables !== undefined && (
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50">
{item.details.syllables} syllables
</span>
)}
{item.details.length !== undefined && (
<span className="px-2 py-0.5 bg-white/5 border border-white/10 text-[10px] font-mono text-white/50">
{item.details.length} chars
</span>
)}
{item.details.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">
has hyphen
</span>
)}
{item.details.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">
has digits
</span>
)}
</div>
)}
{/* Registrar Details */}
{(item.key === 'tld_cheapest_register_usd' || item.key === 'tld_cheapest_renew_usd') && item.details?.registrar && (
<div className="mt-1 text-[10px] font-mono text-white/30">
via {item.details.registrar}
</div>
)}
</div>
</div>
</div> </div>
)} )
</div> })}
) </div>
})} ))}
{/* Quick Actions */}
<div className="mt-6 pt-4 border-t border-white/[0.06]">
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider mb-3">Quick Actions</div>
<div className="flex flex-wrap gap-2">
<button className="flex items-center gap-2 px-3 py-2 bg-white/[0.02] border border-white/[0.08] text-white/60 text-xs font-mono hover:bg-white/[0.04] hover:text-white transition-colors">
<Eye className="w-3.5 h-3.5" />
Add to Watchlist
</button>
{isAvailable && yieldIntent?.monetization_potential !== 'low' && (
<button className="flex items-center gap-2 px-3 py-2 bg-accent/10 border border-accent/30 text-accent text-xs font-mono hover:bg-accent/20 transition-colors">
<Coins className="w-3.5 h-3.5" />
Activate Yield
</button>
)}
</div>
</div>
{/* Mode Toggle */}
<div className="mt-4 flex items-center gap-2">
<button
onClick={() => setFastMode(!fastMode)}
className={clsx(
"flex items-center gap-1.5 px-3 py-1.5 text-[10px] font-mono uppercase tracking-wider border transition-all",
fastMode
? "border-accent/30 bg-accent/10 text-accent"
: "border-white/[0.08] text-white/40 hover:text-white"
)}
>
<Zap className="w-3 h-3" />
Fast Mode
</button>
{data?.cached && (
<span className="text-[10px] font-mono text-white/30 px-2 py-1 border border-white/[0.08]">
Cached
</span>
)}
</div>
</div> </div>
)} )}
</div> </div>

89
pounce_user.md Normal file
View File

@ -0,0 +1,89 @@
Das ist der finale Schritt. Wir fügen jetzt die **Business-Intelligenz** von BrandBucket (Margot Bushnaq) hinzu.
Chris Koerner (Video 1) ist der **Jäger** (Offense).
Der Blogger ist der **Analyst** (Defense).
Margot (Video 2) ist die **CFO/Händlerin** (Sustainability).
Wenn wir ihre Insights integrieren, wird Pounce von einem "Tool für Zocker" zu einer **"Plattform für Domain-Unternehmer"**. Das macht es "Stickier" (Kundenbindung) und schützt die User vor dem Bankrott.
Hier ist das **finale, angereicherte Unicorn-Konzept**. Es vereint **Jagd, Analyse, Cashflow und Finanzplanung**.
---
### THE POUNCE ALPHA TERMINAL (Final Master Plan)
**Der Pitch:** "Pounce combines the hunter's instinct, the analyst's diligence, and the merchant's discipline into one AI Operating System."
#### 1. DISCOVERY: The Opportunity Engine
*Hier finden wir Assets, die andere übersehen.*
* **Feature A: "The $5 Closeout Sniper" (Koerner)**
* *Logik:* Filtert Domains < $10, 5+ Jahre alt, Backlinks. "Free Money".
* **Feature B: "The Viral Trend Scanner" (Koerner)**
* *Logik:* Google Trends API + Domain Availability. "Buy the Trend".
* **Feature C: "The Typo Generator" (Koerner)**
* *Logik:* Phonetische Varianten von großen Marken. Traffic-Grabber.
* **Feature D: "The Brandable Forge" (BrandBucket - NEU)** 💎
* *Insight:* In Rezessionen kaufen Firmen "Invented Names" (Fantasienamen wie Zillow), keine Keywords.
* *Pounce Funktion:* Ein Generator für 5-Letter CVCVC-Pattern (Konsonant-Vokal...).
* *Check:* Verfügbar? Aussprechbar?
* *Benefit:* Findet die "Rolex" von morgen für $10.
* **Feature E: "The Agent Hunter" (BrandBucket - NEU)** 💎
* *Insight:* Zukünftige Namen sind für AI Agents (kurz, menschlich, "Hey Siri").
* *Pounce Funktion:* Filtert nach 1-2 Silben, klingt wie ein Vorname.
* *Benefit:* Die perfekte Wette auf die AI-Zukunft.
#### 2. ANALYSIS: The Deep Diligence Deck
*Hier verhindern wir Fehlkäufe.*
* **Feature F: "The 9-in-1 Dashboard" (Blogger)**
* *Logik:* Keyword Volume, CPC, Trademark Check, Wayback Machine. Alles auf einen Blick.
* **Feature G: "The Authority Score" (Koerner)**
* *Logik:* Unterscheidet "echte" Backlinks (Wikipedia) von Spam.
* **Feature H: "The Radio Test AI" (Koerner & BrandBucket)**
* *Logik:* Margot und Chris sind sich einig: Aussprechbarkeit ist alles.
* *Pounce Funktion:* AI zählt Silben & bewertet "Spelling Confusion".
* *Display:* "🗣️ **Radio Test:** 100/100 (Klingt wie es geschrieben wird)."
#### 3. STRATEGY: The Yield & Pricing Engine
*Hier machen wir Geld.*
* **Feature I: "The Yield Strategist" (Koerner)**
* *Logik:* Pounce schlägt vor: "Route zu Amazon Affiliate" oder "Route zu SaaS Partnerprogramm".
* **Feature J: "The Fixed Price Oracle" (BrandBucket - NEU)** 💎
* *Insight:* "Make Offer" verwirrt Käufer. Festpreise verkaufen sich besser.
* *Pounce Funktion:* Pounce analysiert Comps (Vergleichsverkäufe) und gibt dir keinen "Schätzwert", sondern einen **konkreten Listenpreis**.
* *Output:* "List this for **$2,499**. Do not use 'Make Offer'."
* *Benefit:* Nimmt dem User die Unsicherheit beim Pricing.
#### 4. EXECUTION: The Portfolio Guard (CFO Mode)
*Hier verhindern wir, dass der User pleite geht.*
* **Feature K: "Trend Alert Watchlist" (Koerner)**
* *Logik:* Überwacht Themen ("AI Agents") statt nur Domains.
* **Feature L: "The Runway Monitor" (BrandBucket - NEU)** 💎
* *Insight:* Anfänger kaufen zu viel und sterben an den Renewal-Gebühren nach 12 Monaten.
* *Pounce Funktion:* Ein Finanz-Dashboard.
* *Display:* "⚠️ **Burn Rate Alert:** Du hast $400 Renewals fällig im Oktober. Deine aktuellen Einnahmen (Yield) decken nur $50."
* *Action:* Pounce markiert automatisch Domains zum "Droppen" (Löschen), die keinen Yield und keinen Traffic haben.
* *Benefit:* Pounce rettet dein Business vor dem Cashflow-Tod.
---
### Warum dieses Konzept unschlagbar ist
Wir decken jetzt den **gesamten Lebenszyklus** eines professionellen Investors ab:
1. **Kauf:** Wir finden Trends (Koerner) & Brandables (BrandBucket).
2. **Prüfung:** Wir machen den Radio-Test & Backlink-Check.
3. **Haltezeit:** Wir generieren Cashflow durch Yield (Intent Routing) & warnen vor Renewal-Kosten (BrandBucket).
4. **Verkauf:** Wir setzen den perfekten Festpreis (BrandBucket Oracle).
**Das Feedback an die Domainer:**
Wenn du das nächste Mal mit Chris, dem Blogger oder Yuyu sprichst, sagst du:
> "Pounce is not just a scanner. It's a **CFO in your pocket**.
> It finds the hidden gems (Koerner), keeps you legally safe (Blogger), and prevents you from going broke on renewals (BrandBucket).
> It tells you what to buy, how to price it, and when to drop it."
Das ist das Unicorn. 🦄