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
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:
@ -15,63 +15,97 @@ import {
|
||||
Zap,
|
||||
Globe,
|
||||
Calendar,
|
||||
Link2,
|
||||
Radio,
|
||||
Eye,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Sparkles,
|
||||
Target,
|
||||
Coins,
|
||||
ShoppingCart,
|
||||
Ban,
|
||||
AlertCircle,
|
||||
Info,
|
||||
Bookmark,
|
||||
ArrowRight,
|
||||
} from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import { useAnalyzePanelStore } from '@/lib/analyze-store'
|
||||
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
|
||||
// ============================================================================
|
||||
|
||||
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) {
|
||||
case 'pass':
|
||||
return { bg: 'bg-accent/20', text: 'text-accent', border: 'border-accent/40', icon: CheckCircle2 }
|
||||
case 'warn':
|
||||
return { bg: 'bg-amber-400/20', text: 'text-amber-300', border: 'border-amber-400/40', icon: AlertTriangle }
|
||||
case 'fail':
|
||||
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 }
|
||||
case 'pass': return { bg: 'bg-accent/10', text: 'text-accent', border: 'border-accent/30', icon: CheckCircle2 }
|
||||
case 'warn': return { bg: 'bg-amber-400/10', text: 'text-amber-400', border: 'border-amber-400/30', icon: AlertTriangle }
|
||||
case 'fail': return { bg: 'bg-red-500/10', text: 'text-red-400', border: 'border-red-500/30', icon: XCircle }
|
||||
case 'info': return { bg: 'bg-blue-500/10', text: 'text-blue-400', border: 'border-blue-500/30', icon: Info }
|
||||
default: return { bg: 'bg-white/5', text: 'text-white/40', border: 'border-white/10', icon: null }
|
||||
}
|
||||
}
|
||||
|
||||
function getSectionIcon(key: string) {
|
||||
switch (key) {
|
||||
case 'authority':
|
||||
return Shield
|
||||
case 'market':
|
||||
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' }
|
||||
}
|
||||
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' },
|
||||
}
|
||||
|
||||
async function copyToClipboard(text: string) {
|
||||
@ -92,35 +126,19 @@ function formatValue(value: unknown): string {
|
||||
return 'Details'
|
||||
}
|
||||
|
||||
function isMatrix(item: AnalyzeItem) {
|
||||
return item.key === 'tld_matrix' && Array.isArray(item.value)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export function AnalyzePanel() {
|
||||
const {
|
||||
isOpen,
|
||||
domain,
|
||||
close,
|
||||
fastMode,
|
||||
setFastMode,
|
||||
sectionVisibility,
|
||||
setSectionVisibility
|
||||
} = useAnalyzePanelStore()
|
||||
const { isOpen, domain, close, fastMode, setFastMode } = useAnalyzePanelStore()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [data, setData] = useState<AnalyzeResponse | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
||||
authority: true,
|
||||
market: true,
|
||||
risk: true,
|
||||
value: true
|
||||
})
|
||||
const [activeSection, setActiveSection] = useState<string>('authority')
|
||||
const [yieldIntent, setYieldIntent] = useState<any>(null)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!domain) return
|
||||
@ -129,6 +147,11 @@ export function AnalyzePanel() {
|
||||
try {
|
||||
const res = await api.analyzeDomain(domain, { fast: fastMode, refresh: true })
|
||||
setData(res)
|
||||
// Also fetch yield intent
|
||||
try {
|
||||
const yieldRes = await api.analyzeYieldDomain(domain)
|
||||
setYieldIntent(yieldRes)
|
||||
} catch { setYieldIntent(null) }
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setData(null)
|
||||
@ -143,13 +166,16 @@ export function AnalyzePanel() {
|
||||
const run = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setYieldIntent(null)
|
||||
try {
|
||||
const res = await api.analyzeDomain(domain, { fast: fastMode, refresh: false })
|
||||
if (!cancelled) setData(res)
|
||||
} catch (e) {
|
||||
const [res, yieldRes] = await Promise.allSettled([
|
||||
api.analyzeDomain(domain, { fast: fastMode, refresh: false }),
|
||||
api.analyzeYieldDomain(domain),
|
||||
])
|
||||
if (!cancelled) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setData(null)
|
||||
if (res.status === 'fulfilled') setData(res.value)
|
||||
else setError(res.reason?.message || 'Analysis failed')
|
||||
if (yieldRes.status === 'fulfilled') setYieldIntent(yieldRes.value)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
@ -162,28 +188,15 @@ export function AnalyzePanel() {
|
||||
// ESC to close
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
const onKey = (ev: KeyboardEvent) => {
|
||||
if (ev.key === 'Escape') close()
|
||||
}
|
||||
const onKey = (ev: KeyboardEvent) => { if (ev.key === 'Escape') close() }
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [isOpen, close])
|
||||
|
||||
const toggleSection = useCallback((key: string) => {
|
||||
setExpandedSections(prev => ({ ...prev, [key]: !prev[key] }))
|
||||
}, [])
|
||||
|
||||
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
|
||||
// Calculate Pounce Score from data
|
||||
const pounceScore = useMemo(() => {
|
||||
if (!data?.sections) return 50
|
||||
let score = 50
|
||||
let pass = 0, warn = 0, fail = 0
|
||||
data.sections.forEach(s => {
|
||||
s.items.forEach(item => {
|
||||
@ -193,11 +206,27 @@ export function AnalyzePanel() {
|
||||
})
|
||||
})
|
||||
const total = pass + warn + fail
|
||||
if (total === 0) return null
|
||||
const score = Math.round((pass * 100 + warn * 50) / total)
|
||||
return { score, pass, warn, fail, total }
|
||||
if (total > 0) score = Math.round((pass * 100 + warn * 50) / total)
|
||||
return Math.min(100, Math.max(0, score))
|
||||
}, [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 || ''
|
||||
|
||||
if (!isOpen) return null
|
||||
@ -205,28 +234,30 @@ export function AnalyzePanel() {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200]">
|
||||
{/* 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 */}
|
||||
<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">
|
||||
{/* Panel */}
|
||||
<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 */}
|
||||
<div className="px-6 py-5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-accent/15 border border-accent/30 flex items-center justify-center">
|
||||
<Shield className="w-6 h-6 text-accent" />
|
||||
<div className="px-5 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="w-10 h-10 bg-accent/10 border border-accent/30 flex items-center justify-center shrink-0">
|
||||
<Target className="w-5 h-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-mono text-accent uppercase tracking-widest mb-1">Domain Analysis</div>
|
||||
<div className="text-xl font-bold text-white font-mono truncate max-w-[300px]">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[9px] font-mono text-accent uppercase tracking-[0.2em] mb-0.5">Domain Analysis</div>
|
||||
<div className="text-lg font-bold text-white font-mono truncate">
|
||||
{headerDomain}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const ok = await copyToClipboard(headerDomain)
|
||||
@ -234,231 +265,306 @@ export function AnalyzePanel() {
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
}}
|
||||
className={clsx(
|
||||
"w-10 h-10 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"
|
||||
"w-9 h-9 flex items-center justify-center border transition-all",
|
||||
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>
|
||||
<a
|
||||
href={`https://${encodeURIComponent(headerDomain)}`}
|
||||
target="_blank"
|
||||
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>
|
||||
<button
|
||||
onClick={refresh}
|
||||
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
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Score Bar - LARGER */}
|
||||
{overallScore && !loading && (
|
||||
<div className="px-6 pb-5">
|
||||
<div className="flex items-center gap-4 p-4 bg-white/[0.03] border border-white/10">
|
||||
<div className={clsx(
|
||||
"text-4xl font-bold font-mono",
|
||||
overallScore.score >= 70 ? "text-accent" : overallScore.score >= 40 ? "text-amber-400" : "text-red-400"
|
||||
)}>
|
||||
{overallScore.score}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-mono text-white/50 uppercase tracking-wider mb-2">Overall Score</div>
|
||||
<div className="h-3 bg-white/10 overflow-hidden flex">
|
||||
<div
|
||||
className="h-full bg-accent transition-all"
|
||||
style={{ width: `${(overallScore.pass / overallScore.total) * 100}%` }}
|
||||
/>
|
||||
<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}%` }}
|
||||
/>
|
||||
{/* ════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HERO: Score + Recommendation */}
|
||||
{/* ════════════════════════════════════════════════════════════════════ */}
|
||||
{!loading && data && (
|
||||
<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="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))} />
|
||||
</div>
|
||||
<div className={clsx("text-4xl font-bold font-mono", getScoreColor(pounceScore))}>
|
||||
{pounceScore}
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-white/30 mt-1">
|
||||
{pounceScore >= 80 ? 'Excellent' : pounceScore >= 60 ? 'Good' : pounceScore >= 40 ? 'Fair' : 'Poor'}
|
||||
</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>
|
||||
<span className="text-amber-400 flex items-center gap-2"><AlertTriangle className="w-4 h-4" /> {overallScore.warn}</span>
|
||||
<span className="text-red-400 flex items-center gap-2"><XCircle className="w-4 h-4" /> {overallScore.fail}</span>
|
||||
|
||||
{/* Recommendation Badge */}
|
||||
<div className={clsx("w-32 p-4 border flex flex-col items-center justify-center", recommendation.color)}>
|
||||
<recommendation.icon className="w-6 h-6 mb-2" />
|
||||
<span className="text-sm font-bold uppercase tracking-wider">{recommendation.label}</span>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Mode Toggle */}
|
||||
<div className="px-6 pb-4 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setFastMode(!fastMode)}
|
||||
className={clsx(
|
||||
"flex items-center gap-2 px-4 py-2 text-sm font-bold uppercase tracking-wider border transition-all",
|
||||
fastMode
|
||||
? "border-accent/40 bg-accent/15 text-accent"
|
||||
: "border-white/20 text-white/50 hover:text-white hover:bg-white/10"
|
||||
)}
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
Fast Mode
|
||||
</button>
|
||||
{data?.cached && (
|
||||
<span className="text-sm font-mono text-white/40 px-3 py-2 border border-white/20 bg-white/5">
|
||||
⚡ Cached
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* ════════════════════════════════════════════════════════════════════ */}
|
||||
{/* SECTION TABS */}
|
||||
{/* ════════════════════════════════════════════════════════════════════ */}
|
||||
{!loading && data && (
|
||||
<div className="px-5 pb-3 flex gap-1.5 overflow-x-auto">
|
||||
{data.sections.map((section) => {
|
||||
const config = SECTION_CONFIG[section.key] || SECTION_CONFIG.authority
|
||||
const isActive = activeSection === section.key
|
||||
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' },
|
||||
amber: { active: 'border-amber-500 bg-amber-500/10 text-amber-400', inactive: 'border-white/[0.06] text-white/40' },
|
||||
violet: { active: 'border-violet-500 bg-violet-500/10 text-violet-400', inactive: 'border-white/[0.06] text-white/40' },
|
||||
}
|
||||
const colors = colorMap[config.color] || colorMap.blue
|
||||
|
||||
return (
|
||||
<button
|
||||
key={section.key}
|
||||
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>
|
||||
|
||||
{/* Body - BETTER SPACING */}
|
||||
{/* ════════════════════════════════════════════════════════════════════ */}
|
||||
{/* CONTENT */}
|
||||
{/* ════════════════════════════════════════════════════════════════════ */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<RefreshCw className="w-10 h-10 text-accent animate-spin mx-auto mb-4" />
|
||||
<div className="text-base font-mono text-white/50">Analyzing domain...</div>
|
||||
<RefreshCw className="w-8 h-8 text-accent animate-spin mx-auto mb-3" />
|
||||
<div className="text-sm font-mono text-white/40">Analyzing domain...</div>
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-6">
|
||||
<div className="border border-red-500/30 bg-red-500/10 p-6">
|
||||
<div className="text-lg font-bold text-red-400 mb-2">Analysis Failed</div>
|
||||
<div className="text-sm font-mono text-white/60">{error}</div>
|
||||
<div className="p-5">
|
||||
<div className="border border-red-500/30 bg-red-500/10 p-5">
|
||||
<div className="text-base font-bold text-red-400 mb-2">Analysis Failed</div>
|
||||
<div className="text-sm font-mono text-white/50">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : !data ? (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<div className="text-base font-mono text-white/40">No data available</div>
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-sm font-mono text-white/30">No data available</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 space-y-4">
|
||||
{visibleSections.map((section) => {
|
||||
const SectionIcon = getSectionIcon(section.key)
|
||||
const sectionStyle = getSectionColor(section.key)
|
||||
const isExpanded = expandedSections[section.key] !== false
|
||||
<div className="p-5">
|
||||
{/* Active Section Items */}
|
||||
{data.sections.filter(s => s.key === activeSection).map((section) => (
|
||||
<div key={section.key} className="space-y-2">
|
||||
{section.items.map((item) => {
|
||||
const statusStyle = getStatusStyle(item.status)
|
||||
const StatusIcon = statusStyle.icon
|
||||
|
||||
return (
|
||||
<div key={section.key} className={clsx("border overflow-hidden", sectionStyle.border, "bg-[#050505]")}>
|
||||
{/* Section Header - LARGER */}
|
||||
<button
|
||||
onClick={() => toggleSection(section.key)}
|
||||
className={clsx(
|
||||
"w-full px-5 py-4 flex items-center justify-between transition-colors",
|
||||
sectionStyle.bg, "hover:brightness-110"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SectionIcon className={clsx("w-5 h-5", sectionStyle.text)} />
|
||||
<span className={clsx("text-sm font-bold uppercase tracking-wider", sectionStyle.text)}>
|
||||
{section.title}
|
||||
</span>
|
||||
<span className="text-sm font-mono text-white/40 ml-2">
|
||||
{section.items.length} checks
|
||||
</span>
|
||||
</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>
|
||||
// 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 className="grid grid-cols-4 gap-1.5">
|
||||
{(item.value as any[]).slice(0, 12).map((row: any) => (
|
||||
<div
|
||||
key={String(row.domain)}
|
||||
className={clsx(
|
||||
"px-2 py-1.5 text-[10px] font-mono flex items-center justify-between border",
|
||||
row.status === 'available'
|
||||
? "border-accent/30 bg-accent/5 text-accent"
|
||||
: "border-white/[0.06] bg-white/[0.01] text-white/30"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">.{row.tld}</span>
|
||||
{row.status === 'available' && <Check className="w-3 h-3 shrink-0 ml-1" />}
|
||||
</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 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>
|
||||
))}
|
||||
|
||||
{/* 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>
|
||||
|
||||
89
pounce_user.md
Normal file
89
pounce_user.md
Normal 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. 🦄
|
||||
Reference in New Issue
Block a user