Fix AnalyzePanel syntax error, restore working version + Trends & Forge redesign
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 16:15:05 +01:00
parent 129716ad1d
commit c23d3c4b6c
8 changed files with 296 additions and 494 deletions

View File

@ -20,7 +20,7 @@ Neu: **DISCOVER → TRACK → TRADE → YIELD**
│ "Let your domains work for you." │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 🔌 Connect Point DNS to ns.pounce.io │ │
│ │ 🔌 Connect Point DNS to ns.pounce.ch │ │
│ │ 🧠 Analyze We detect: "kredit.ch" → Loan Intent │ │
│ │ 💰 Earn Affiliate routing → CHF 25/lead │ │
│ └─────────────────────────────────────────────────────────┘ │
@ -161,8 +161,8 @@ SETTINGS
│ Change your nameservers to: │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ ns1.pounce.io [📋] │ │
│ │ ns2.pounce.io [📋] │ │
│ │ ns1.pounce.ch [📋] │ │
│ │ ns2.pounce.ch [📋] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ⏳ We're checking your DNS... │
@ -380,7 +380,7 @@ class YieldDNSService:
"""Verwaltet DNS und Hosting für Yield-Domains."""
async def verify_nameservers(self, domain: str) -> bool:
"""Prüft ob Domain auf ns1/ns2.pounce.io zeigt."""
"""Prüft ob Domain auf ns1/ns2.pounce.ch zeigt."""
async def provision_landing_page(self, domain: str, intent: str) -> str:
"""Erstellt minimale Landing Page für Routing."""
@ -468,7 +468,7 @@ class YieldDNSService:
| Komponente | Benötigt | Status |
|------------|----------|--------|
| Eigene Nameserver (ns1/ns2.pounce.io) | ✅ | Neu |
| Eigene Nameserver (ns1/ns2.pounce.ch) | ✅ | Neu |
| DNS-Hosting (Cloudflare API oder ähnlich) | ✅ | Neu |
| Landing Page CDN | ✅ | Neu |
| Affiliate-Netzwerk Accounts | ✅ | Neu |

View File

@ -64,15 +64,15 @@ For yield domains to work, you need to set up DNS infrastructure:
#### Option A: Dedicated Nameservers (Recommended for Scale)
1. Set up two nameserver instances (e.g., `ns1.pounce.io`, `ns2.pounce.io`)
1. Set up two nameserver instances (e.g., `ns1.pounce.ch`, `ns2.pounce.ch`)
2. Run PowerDNS or similar with a backend that queries your yield_domains table
3. Return A records pointing to your yield routing service
#### Option B: CNAME Approach (Simpler)
1. Set up a wildcard SSL certificate for `*.yield.pounce.io`
1. Set up a wildcard SSL certificate for `*.yield.pounce.ch`
2. Configure Nginx/Caddy to handle all incoming hosts
3. Users add CNAME: `@ → yield.pounce.io`
3. Users add CNAME: `@ → yield.pounce.ch`
### 4. Nginx Configuration
@ -85,8 +85,8 @@ server {
server_name ~^(?<domain>.+)$;
# Wildcard cert
ssl_certificate /etc/ssl/yield.pounce.io.crt;
ssl_certificate_key /etc/ssl/yield.pounce.io.key;
ssl_certificate /etc/ssl/yield.pounce.ch.crt;
ssl_certificate_key /etc/ssl/yield.pounce.ch.key;
location / {
proxy_pass http://backend:8000/api/v1/r/$domain;

View File

@ -7,7 +7,7 @@ This handles incoming HTTP requests to yield domains:
3. Track the click
4. Redirect to the appropriate affiliate landing page
In production, this runs on a separate subdomain or IP (yield.pounce.io)
In production, this runs on a separate subdomain or IP (yield.pounce.ch)
that yield domains CNAME to.
"""
@ -272,7 +272,7 @@ async def catch_all_route(
is the yield domain itself (e.g., zahnarzt-zuerich.ch).
This requires:
1. Yield domains to CNAME to yield.pounce.io
1. Yield domains to CNAME to yield.pounce.ch
2. Nginx/Caddy to route all hosts to this backend
3. This endpoint to parse the Host header
"""
@ -283,7 +283,7 @@ async def catch_all_route(
host = host.split(":")[0]
# Skip our own domains
our_domains = ["pounce.ch", "pounce.io", "localhost", "127.0.0.1"]
our_domains = ["pounce.ch", "localhost", "127.0.0.1"]
if any(host.endswith(d) for d in our_domains):
return {"status": "not a yield domain", "host": host}

View File

@ -77,11 +77,11 @@ class Settings(BaseSettings):
# Yield / Intent Routing
# =================================
# Comma-separated list of nameservers the user must delegate to for Yield.
# Example: "ns1.pounce.io,ns2.pounce.io"
yield_nameservers: str = "ns1.pounce.io,ns2.pounce.io"
# Example: "ns1.pounce.ch,ns2.pounce.ch"
yield_nameservers: str = "ns1.pounce.ch,ns2.pounce.ch"
# CNAME/ALIAS target for simpler DNS setup (provider-dependent).
# Example: "yield.pounce.io"
yield_cname_target: str = "yield.pounce.io"
# Example: "yield.pounce.ch"
yield_cname_target: str = "yield.pounce.ch"
@property
def yield_nameserver_list(self) -> list[str]:

View File

@ -396,7 +396,7 @@ export default function HomePage() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-8">
{[
{ icon: Network, step: '1', title: 'Connect', desc: 'Point nameservers to ns.pounce.io (optional)' },
{ icon: Network, step: '1', title: 'Connect', desc: 'Point nameservers to ns.pounce.ch (optional)' },
{ icon: Cpu, step: '2', title: 'Analyze', desc: 'We detect intent and risk signals from real usage' },
{ icon: Share2, step: '3', title: 'Route', desc: 'Send traffic to the best destination (partner, listing, or page)' },
].map((item, i) => (

View File

@ -15,136 +15,63 @@ 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 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) {
function getStatusColor(status: string) {
switch (status) {
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 }
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 }
}
}
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'
},
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
}
}
// 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.',
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) {
@ -165,19 +92,35 @@ 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 } = useAnalyzePanelStore()
const {
isOpen,
domain,
close,
fastMode,
setFastMode,
sectionVisibility,
setSectionVisibility
} = 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 [activeSection, setActiveSection] = useState<string>('authority')
const [yieldIntent, setYieldIntent] = useState<any>(null)
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
authority: true,
market: true,
risk: true,
value: true
})
const refresh = useCallback(async () => {
if (!domain) return
@ -186,11 +129,6 @@ 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)
@ -205,16 +143,13 @@ export function AnalyzePanel() {
const run = async () => {
setLoading(true)
setError(null)
setYieldIntent(null)
try {
const [res, yieldRes] = await Promise.allSettled([
api.analyzeDomain(domain, { fast: fastMode, refresh: false }),
api.analyzeYieldDomain(domain),
])
const res = await api.analyzeDomain(domain, { fast: fastMode, refresh: false })
if (!cancelled) setData(res)
} catch (e) {
if (!cancelled) {
if (res.status === 'fulfilled') setData(res.value)
else setError(res.reason?.message || 'Analysis failed')
if (yieldRes.status === 'fulfilled') setYieldIntent(yieldRes.value)
setError(e instanceof Error ? e.message : String(e))
setData(null)
}
} finally {
if (!cancelled) setLoading(false)
@ -227,15 +162,28 @@ 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])
// Calculate Pounce Score from data
const pounceScore = useMemo(() => {
if (!data?.sections) return 50
let score = 50
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
let pass = 0, warn = 0, fail = 0
data.sections.forEach(s => {
s.items.forEach(item => {
@ -245,27 +193,11 @@ export function AnalyzePanel() {
})
})
const total = pass + warn + fail
if (total > 0) score = Math.round((pass * 100 + warn * 50) / total)
return Math.min(100, Math.max(0, score))
if (total === 0) return null
const score = Math.round((pass * 100 + warn * 50) / total)
return { score, pass, warn, fail, total }
}, [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
@ -273,30 +205,28 @@ export function AnalyzePanel() {
return (
<div className="fixed inset-0 z-[200]">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/90 backdrop-blur-sm" onClick={close} />
<div className="absolute inset-0 bg-black/85 backdrop-blur-md" onClick={close} />
{/* 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">
{/* 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">
{/* ════════════════════════════════════════════════════════════════════ */}
{/* HEADER */}
{/* ════════════════════════════════════════════════════════════════════ */}
<div className="shrink-0 bg-[#050505] border-b border-white/[0.08]">
{/* Header */}
<div className="shrink-0 border-b border-white/10 bg-[#050505]">
{/* Top Bar */}
<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 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>
<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">
<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]">
{headerDomain}
</div>
</div>
</div>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-2">
<button
onClick={async () => {
const ok = await copyToClipboard(headerDomain)
@ -304,359 +234,231 @@ export function AnalyzePanel() {
setTimeout(() => setCopied(false), 1500)
}}
className={clsx(
"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]"
"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"
)}
>
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
</button>
<a
href={`https://${encodeURIComponent(headerDomain)}`}
target="_blank"
rel="noopener noreferrer"
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"
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"
>
<ExternalLink className="w-4 h-4" />
<ExternalLink className="w-5 h-5" />
</a>
<button
onClick={refresh}
disabled={loading}
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"
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"
>
<RefreshCw className={clsx('w-4 h-4', loading && 'animate-spin')} />
<RefreshCw className={clsx('w-5 h-5', loading && 'animate-spin')} />
</button>
<button
onClick={close}
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"
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"
>
<X className="w-4 h-4" />
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* ════════════════════════════════════════════════════════════════════ */}
{/* 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 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))} />
</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 investment' : pounceScore >= 60 ? 'Good opportunity' : pounceScore >= 40 ? 'Proceed with caution' : 'High risk'}
{/* 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}%` }}
/>
</div>
</div>
{/* Recommendation Badge */}
<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 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>
</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>
)}
{/* ════════════════════════════════════════════════════════════════════ */}
{/* 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)}
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]'
)}
>
<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>
)}
{/* 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>
</div>
{/* ════════════════════════════════════════════════════════════════════ */}
{/* CONTENT */}
{/* ════════════════════════════════════════════════════════════════════ */}
{/* Body - BETTER SPACING */}
<div className="flex-1 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="flex items-center justify-center py-24">
<div className="text-center">
<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>
<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>
</div>
</div>
) : error ? (
<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 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>
</div>
) : !data ? (
<div className="flex items-center justify-center py-20">
<div className="text-sm font-mono text-white/30">No data available</div>
<div className="flex items-center justify-center py-24">
<div className="text-base font-mono text-white/40">No data available</div>
</div>
) : (
<div className="p-5">
{/* Active Section Items */}
{data.sections.filter(s => s.key === activeSection).map((section) => {
const sectionConfig = SECTION_CONFIG[section.key] || SECTION_CONFIG.authority
<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
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]" 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 cursor-default",
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 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
)}
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">
<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(
"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 && (() => {
const d = item.details as Record<string, any>
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" 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" 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" 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" title="Numbers can reduce memorability">
has digits
</span>
)}
</div>
)
})()}
{/* Registrar Details */}
{(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" title="Cheapest registrar offering this price">
via {d.registrar}
</div>
) : null
})()}
</div>
</div>
<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>
)
})}
</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
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-white/40" />
) : (
<ChevronDown className="w-5 h-5 text-white/40" />
)}
</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>
{/* 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>
)
})}
</div>
)}
</div>

View File

@ -400,15 +400,15 @@ export function DropsTab({ showToast }: DropsTabProps) {
<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_60px_80px_100px] 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="Freshly dropped domain - was registered but not renewed">
Domain
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('length')} className="flex items-center gap-1 justify-center hover:text-white/60">
<button onClick={() => handleSort('length')} className="flex items-center gap-1 justify-center hover:text-white/60" title="Character count - shorter domains are more valuable">
Len
{sortField === 'length' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('date')} className="flex items-center gap-1 justify-center hover:text-white/60">
<button onClick={() => handleSort('date')} className="flex items-center gap-1 justify-center hover:text-white/60" title="When the domain was detected as dropped">
When
{sortField === 'date' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>

View File

@ -25,7 +25,7 @@ Wir teilen Pounce nun final in **4 operative Module**. Das Konzept "Intent Routi
*„Lass das Asset arbeiten.“*
* **Die Vision:** Verwandlung von toten Namen in aktive "Intent Router".
* **Der Mechanismus:**
1. **Connect:** User ändert Nameserver auf `ns.pounce.io`.
1. **Connect:** User ändert Nameserver auf `ns.pounce.ch`.
2. **Analyze:** Pounce erkennt `zahnarzt-zuerich.ch` → Intent: "Termin buchen".
3. **Route:** Pounce schaltet automatisch eine Minimal-Seite ("Zahnarzt finden") und leitet Traffic zu Partnern (Doctolib, Comparis) weiter.
* **Der Clou:** Kein "Parking" (Werbung für 0,01€), sondern "Routing" (Leads für 20,00€).