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." │ │ "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 │ │ │ │ 🧠 Analyze We detect: "kredit.ch" → Loan Intent │ │
│ │ 💰 Earn Affiliate routing → CHF 25/lead │ │ │ │ 💰 Earn Affiliate routing → CHF 25/lead │ │
│ └─────────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────────┘ │
@ -161,8 +161,8 @@ SETTINGS
│ Change your nameservers to: │ │ Change your nameservers to: │
│ │ │ │
│ ┌─────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────┐ │
│ │ ns1.pounce.io [📋] │ │ │ │ ns1.pounce.ch [📋] │ │
│ │ ns2.pounce.io [📋] │ │ │ │ ns2.pounce.ch [📋] │ │
│ └─────────────────────────────────────────┘ │ │ └─────────────────────────────────────────┘ │
│ │ │ │
│ ⏳ We're checking your DNS... │ │ ⏳ We're checking your DNS... │
@ -380,7 +380,7 @@ class YieldDNSService:
"""Verwaltet DNS und Hosting für Yield-Domains.""" """Verwaltet DNS und Hosting für Yield-Domains."""
async def verify_nameservers(self, domain: str) -> bool: 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: async def provision_landing_page(self, domain: str, intent: str) -> str:
"""Erstellt minimale Landing Page für Routing.""" """Erstellt minimale Landing Page für Routing."""
@ -468,7 +468,7 @@ class YieldDNSService:
| Komponente | Benötigt | Status | | 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 | | DNS-Hosting (Cloudflare API oder ähnlich) | ✅ | Neu |
| Landing Page CDN | ✅ | Neu | | Landing Page CDN | ✅ | Neu |
| Affiliate-Netzwerk Accounts | ✅ | 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) #### 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 2. Run PowerDNS or similar with a backend that queries your yield_domains table
3. Return A records pointing to your yield routing service 3. Return A records pointing to your yield routing service
#### Option B: CNAME Approach (Simpler) #### 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 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 ### 4. Nginx Configuration
@ -85,8 +85,8 @@ server {
server_name ~^(?<domain>.+)$; server_name ~^(?<domain>.+)$;
# Wildcard cert # Wildcard cert
ssl_certificate /etc/ssl/yield.pounce.io.crt; ssl_certificate /etc/ssl/yield.pounce.ch.crt;
ssl_certificate_key /etc/ssl/yield.pounce.io.key; ssl_certificate_key /etc/ssl/yield.pounce.ch.key;
location / { location / {
proxy_pass http://backend:8000/api/v1/r/$domain; 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 3. Track the click
4. Redirect to the appropriate affiliate landing page 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. that yield domains CNAME to.
""" """
@ -272,7 +272,7 @@ async def catch_all_route(
is the yield domain itself (e.g., zahnarzt-zuerich.ch). is the yield domain itself (e.g., zahnarzt-zuerich.ch).
This requires: 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 2. Nginx/Caddy to route all hosts to this backend
3. This endpoint to parse the Host header 3. This endpoint to parse the Host header
""" """
@ -283,7 +283,7 @@ async def catch_all_route(
host = host.split(":")[0] host = host.split(":")[0]
# Skip our own domains # 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): if any(host.endswith(d) for d in our_domains):
return {"status": "not a yield domain", "host": host} return {"status": "not a yield domain", "host": host}

View File

@ -77,11 +77,11 @@ class Settings(BaseSettings):
# Yield / Intent Routing # Yield / Intent Routing
# ================================= # =================================
# Comma-separated list of nameservers the user must delegate to for Yield. # Comma-separated list of nameservers the user must delegate to for Yield.
# Example: "ns1.pounce.io,ns2.pounce.io" # Example: "ns1.pounce.ch,ns2.pounce.ch"
yield_nameservers: str = "ns1.pounce.io,ns2.pounce.io" yield_nameservers: str = "ns1.pounce.ch,ns2.pounce.ch"
# CNAME/ALIAS target for simpler DNS setup (provider-dependent). # CNAME/ALIAS target for simpler DNS setup (provider-dependent).
# Example: "yield.pounce.io" # Example: "yield.pounce.ch"
yield_cname_target: str = "yield.pounce.io" yield_cname_target: str = "yield.pounce.ch"
@property @property
def yield_nameserver_list(self) -> list[str]: 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"> <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: 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)' }, { icon: Share2, step: '3', title: 'Route', desc: 'Send traffic to the best destination (partner, listing, or page)' },
].map((item, i) => ( ].map((item, i) => (

View File

@ -15,136 +15,63 @@ 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 getScoreColor(score: number) { function getStatusColor(status: string) {
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': return { bg: 'bg-accent/10', text: 'text-accent', border: 'border-accent/30', icon: CheckCircle2 } case 'pass':
case 'warn': return { bg: 'bg-amber-400/10', text: 'text-amber-400', border: 'border-amber-400/30', icon: AlertTriangle } return { bg: 'bg-accent/20', text: 'text-accent', border: 'border-accent/40', icon: CheckCircle2 }
case 'fail': return { bg: 'bg-red-500/10', text: 'text-red-400', border: 'border-red-500/30', icon: XCircle } case 'warn':
case 'info': return { bg: 'bg-blue-500/10', text: 'text-blue-400', border: 'border-blue-500/30', icon: Info } return { bg: 'bg-amber-400/20', text: 'text-amber-300', border: 'border-amber-400/40', icon: AlertTriangle }
default: return { bg: 'bg-white/5', text: 'text-white/40', border: 'border-white/10', icon: null } 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 }> = { function getSectionIcon(key: string) {
authority: { switch (key) {
icon: Shield, case 'authority':
color: 'blue', return Shield
label: 'Authority', case 'market':
description: 'Domain trust signals: age, backlinks, brand memorability' return TrendingUp
}, case 'risk':
market: { return AlertTriangle
icon: TrendingUp, case 'value':
color: 'emerald', return DollarSign
label: 'Market', default:
description: 'Search demand, competition, and commercial value indicators' return Globe
}, }
risk: {
icon: AlertTriangle,
color: 'amber',
label: 'Risk',
description: 'Legal, reputation and technical risks to consider'
},
value: {
icon: DollarSign,
color: 'violet',
label: 'Value',
description: 'Estimated worth and comparable sales data'
},
} }
// Tooltips for each analysis item function getSectionColor(key: string) {
const ITEM_TOOLTIPS: Record<string, string> = { switch (key) {
availability: 'Is this domain currently available for registration?', case 'authority':
radio_test: 'Can this domain be easily spelled when heard? Good brandables score high.', return { text: 'text-blue-400', bg: 'bg-blue-500/10', border: 'border-blue-500/30' }
age: 'Older domains often have more trust with search engines.', case 'market':
backlinks: 'Number of websites linking to this domain. More = higher authority.', return { text: 'text-emerald-400', bg: 'bg-emerald-500/10', border: 'border-emerald-500/30' }
trust_flow: 'Quality score of the backlink profile (0-100). Higher is better.', case 'risk':
search_volume: 'Monthly Google searches for this keyword. Higher = more traffic potential.', return { text: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/30' }
cpc: 'Cost-per-click for ads on this keyword. Higher CPC = more commercial intent.', case 'value':
competition: 'How competitive the keyword is for SEO and ads.', return { text: 'text-violet-400', bg: 'bg-violet-500/10', border: 'border-violet-500/30' }
tld_matrix: 'Availability of this name across different domain extensions.', default:
blacklist: 'Is this domain flagged for spam, malware, or phishing?', return { text: 'text-white/60', bg: 'bg-white/5', border: 'border-white/20' }
trademark: 'Does this domain potentially infringe on known trademarks?', }
archive: 'Historical data from the Wayback Machine - what was hosted here before?',
valuation: 'Estimated market value based on comparable sales and metrics.',
tld_cheapest_register_usd: 'Lowest registration price available from major registrars.',
tld_cheapest_renew_usd: 'Annual renewal cost - factor this into your ROI calculations.',
} }
async function copyToClipboard(text: string) { async function copyToClipboard(text: string) {
@ -165,19 +92,35 @@ 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 { isOpen, domain, close, fastMode, setFastMode } = useAnalyzePanelStore() const {
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 [activeSection, setActiveSection] = useState<string>('authority') const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
const [yieldIntent, setYieldIntent] = useState<any>(null) authority: true,
market: true,
risk: true,
value: true
})
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
if (!domain) return if (!domain) return
@ -186,11 +129,6 @@ 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)
@ -205,16 +143,13 @@ export function AnalyzePanel() {
const run = async () => { const run = async () => {
setLoading(true) setLoading(true)
setError(null) setError(null)
setYieldIntent(null)
try { try {
const [res, yieldRes] = await Promise.allSettled([ const res = await api.analyzeDomain(domain, { fast: fastMode, refresh: false })
api.analyzeDomain(domain, { fast: fastMode, refresh: false }), if (!cancelled) setData(res)
api.analyzeYieldDomain(domain), } catch (e) {
])
if (!cancelled) { if (!cancelled) {
if (res.status === 'fulfilled') setData(res.value) setError(e instanceof Error ? e.message : String(e))
else setError(res.reason?.message || 'Analysis failed') setData(null)
if (yieldRes.status === 'fulfilled') setYieldIntent(yieldRes.value)
} }
} finally { } finally {
if (!cancelled) setLoading(false) if (!cancelled) setLoading(false)
@ -227,15 +162,28 @@ export function AnalyzePanel() {
// ESC to close // ESC to close
useEffect(() => { useEffect(() => {
if (!isOpen) return 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) window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey)
}, [isOpen, close]) }, [isOpen, close])
// Calculate Pounce Score from data const toggleSection = useCallback((key: string) => {
const pounceScore = useMemo(() => { setExpandedSections(prev => ({ ...prev, [key]: !prev[key] }))
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 => {
@ -245,27 +193,11 @@ export function AnalyzePanel() {
}) })
}) })
const total = pass + warn + fail const total = pass + warn + fail
if (total > 0) score = Math.round((pass * 100 + warn * 50) / total) if (total === 0) return null
return Math.min(100, Math.max(0, score)) const score = Math.round((pass * 100 + warn * 50) / total)
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
@ -273,30 +205,28 @@ 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/90 backdrop-blur-sm" onClick={close} /> <div className="absolute inset-0 bg-black/85 backdrop-blur-md" onClick={close} />
{/* Panel */} {/* Panel - WIDER & MORE READABLE */}
<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"> <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 */}
{/* HEADER */} <div className="shrink-0 border-b border-white/10 bg-[#050505]">
{/* ════════════════════════════════════════════════════════════════════ */}
<div className="shrink-0 bg-[#050505] border-b border-white/[0.08]">
{/* Top Bar */} {/* Top Bar */}
<div className="px-5 py-4 flex items-center justify-between"> <div className="px-6 py-5 flex items-center justify-between">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-4">
<div className="w-10 h-10 bg-accent/10 border border-accent/30 flex items-center justify-center shrink-0"> <div className="w-12 h-12 bg-accent/15 border border-accent/30 flex items-center justify-center">
<Target className="w-5 h-5 text-accent" /> <Shield className="w-6 h-6 text-accent" />
</div> </div>
<div className="min-w-0"> <div>
<div className="text-[9px] font-mono text-accent uppercase tracking-[0.2em] mb-0.5">Domain Analysis</div> <div className="text-xs font-mono text-accent uppercase tracking-widest mb-1">Domain Analysis</div>
<div className="text-lg font-bold text-white font-mono truncate"> <div className="text-xl font-bold text-white font-mono truncate max-w-[300px]">
{headerDomain} {headerDomain}
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-2">
<button <button
onClick={async () => { onClick={async () => {
const ok = await copyToClipboard(headerDomain) const ok = await copyToClipboard(headerDomain)
@ -304,359 +234,231 @@ export function AnalyzePanel() {
setTimeout(() => setCopied(false), 1500) setTimeout(() => setCopied(false), 1500)
}} }}
className={clsx( className={clsx(
"w-9 h-9 flex items-center justify-center border transition-all", "w-10 h-10 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 ? "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> </button>
<a <a
href={`https://${encodeURIComponent(headerDomain)}`} href={`https://${encodeURIComponent(headerDomain)}`}
target="_blank" target="_blank"
rel="noopener noreferrer" 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> </a>
<button <button
onClick={refresh} onClick={refresh}
disabled={loading} 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>
<button <button
onClick={close} 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> </button>
</div> </div>
</div> </div>
{/* ════════════════════════════════════════════════════════════════════ */} {/* Score Bar - LARGER */}
{/* HERO: Score + Recommendation */} {overallScore && !loading && (
{/* ════════════════════════════════════════════════════════════════════ */} <div className="px-6 pb-5">
{!loading && data && ( <div className="flex items-center gap-4 p-4 bg-white/[0.03] border border-white/10">
<div className="px-5 pb-4"> <div className={clsx(
<div className="flex gap-3"> "text-4xl font-bold font-mono",
{/* Pounce Score */} overallScore.score >= 70 ? "text-accent" : overallScore.score >= 40 ? "text-amber-400" : "text-red-400"
<div )}>
className={clsx("flex-1 p-4 border cursor-help", getScoreBg(pounceScore))} {overallScore.score}
title="Pounce Score: Combined rating based on authority, market potential, and risk factors. 80+ is excellent, 60+ is good, below 40 needs caution." </div>
> <div className="flex-1">
<div className="flex items-center justify-between mb-2"> <div className="text-xs font-mono text-white/50 uppercase tracking-wider mb-2">Overall Score</div>
<span className="text-[9px] font-mono text-white/40 uppercase tracking-wider">Pounce Score</span> <div className="h-3 bg-white/10 overflow-hidden flex">
<Sparkles className={clsx("w-4 h-4", getScoreColor(pounceScore))} /> <div
</div> className="h-full bg-accent transition-all"
<div className={clsx("text-4xl font-bold font-mono", getScoreColor(pounceScore))}> style={{ width: `${(overallScore.pass / overallScore.total) * 100}%` }}
{pounceScore} />
</div> <div
<div className="text-[10px] font-mono text-white/30 mt-1"> className="h-full bg-amber-400 transition-all"
{pounceScore >= 80 ? 'Excellent investment' : pounceScore >= 60 ? 'Good opportunity' : pounceScore >= 40 ? 'Proceed with caution' : 'High risk'} 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">
{/* Recommendation Badge */} <span className="text-accent flex items-center gap-2"><CheckCircle2 className="w-4 h-4" /> {overallScore.pass}</span>
<div <span className="text-amber-400 flex items-center gap-2"><AlertTriangle className="w-4 h-4" /> {overallScore.warn}</span>
className={clsx("w-32 p-4 border flex flex-col items-center justify-center cursor-help", recommendation.color)} <span className="text-red-400 flex items-center gap-2"><XCircle className="w-4 h-4" /> {overallScore.fail}</span>
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> </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 */}
{/* SECTION TABS */} <div className="px-6 pb-4 flex items-center gap-3">
{/* ════════════════════════════════════════════════════════════════════ */} <button
{!loading && data && ( onClick={() => setFastMode(!fastMode)}
<div className="px-5 pb-3 flex gap-1.5 overflow-x-auto"> className={clsx(
{data.sections.map((section) => { "flex items-center gap-2 px-4 py-2 text-sm font-bold uppercase tracking-wider border transition-all",
const config = SECTION_CONFIG[section.key] || SECTION_CONFIG.authority fastMode
const isActive = activeSection === section.key ? "border-accent/40 bg-accent/15 text-accent"
const colorMap: Record<string, { active: string; inactive: string }> = { : "border-white/20 text-white/50 hover:text-white hover:bg-white/10"
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' }, <Zap className="w-4 h-4" />
violet: { active: 'border-violet-500 bg-violet-500/10 text-violet-400', inactive: 'border-white/[0.06] text-white/40' }, Fast Mode
} </button>
const colors = colorMap[config.color] || colorMap.blue {data?.cached && (
<span className="text-sm font-mono text-white/40 px-3 py-2 border border-white/20 bg-white/5">
return ( Cached
<button </span>
key={section.key} )}
onClick={() => setActiveSection(section.key)} </div>
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>
)}
</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-20"> <div className="flex items-center justify-center py-24">
<div className="text-center"> <div className="text-center">
<RefreshCw className="w-8 h-8 text-accent animate-spin mx-auto mb-3" /> <RefreshCw className="w-10 h-10 text-accent animate-spin mx-auto mb-4" />
<div className="text-sm font-mono text-white/40">Analyzing domain...</div> <div className="text-base font-mono text-white/50">Analyzing domain...</div>
</div> </div>
</div> </div>
) : error ? ( ) : error ? (
<div className="p-5"> <div className="p-6">
<div className="border border-red-500/30 bg-red-500/10 p-5"> <div className="border border-red-500/30 bg-red-500/10 p-6">
<div className="text-base font-bold text-red-400 mb-2">Analysis Failed</div> <div className="text-lg font-bold text-red-400 mb-2">Analysis Failed</div>
<div className="text-sm font-mono text-white/50">{error}</div> <div className="text-sm font-mono text-white/60">{error}</div>
</div> </div>
</div> </div>
) : !data ? ( ) : !data ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-24">
<div className="text-sm font-mono text-white/30">No data available</div> <div className="text-base font-mono text-white/40">No data available</div>
</div> </div>
) : ( ) : (
<div className="p-5"> <div className="p-6 space-y-4">
{/* Active Section Items */} {visibleSections.map((section) => {
{data.sections.filter(s => s.key === activeSection).map((section) => { const SectionIcon = getSectionIcon(section.key)
const sectionConfig = SECTION_CONFIG[section.key] || SECTION_CONFIG.authority const sectionStyle = getSectionColor(section.key)
const isExpanded = expandedSections[section.key] !== false
return ( return (
<div key={section.key} className="space-y-3"> <div key={section.key} className={clsx("border overflow-hidden", sectionStyle.border, "bg-[#050505]")}>
{/* Section Description */} {/* Section Header - LARGER */}
<div className="pb-3 mb-3 border-b border-white/[0.06]"> <button
<div className="flex items-center gap-2 mb-1"> onClick={() => toggleSection(section.key)}
<sectionConfig.icon className="w-4 h-4 text-white/40" /> className={clsx(
<span className="text-sm font-bold text-white">{sectionConfig.label}</span> "w-full px-5 py-4 flex items-center justify-between transition-colors",
</div> sectionStyle.bg, "hover:brightness-110"
<p className="text-xs text-white/40 font-mono">{sectionConfig.description}</p> )}
</div> >
<div className="flex items-center gap-3">
{section.items.map((item) => { <SectionIcon className={clsx("w-5 h-5", sectionStyle.text)} />
const statusStyle = getStatusStyle(item.status) <span className={clsx("text-sm font-bold uppercase tracking-wider", sectionStyle.text)}>
const StatusIcon = statusStyle.icon {section.title}
const tooltip = ITEM_TOOLTIPS[item.key] || '' </span>
<span className="text-sm font-mono text-white/40 ml-2">
// Special handling for TLD Matrix {section.items.length} checks
if (item.key === 'tld_matrix' && Array.isArray(item.value)) { </span>
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> </div>
) {isExpanded ? (
})} <ChevronUp className="w-5 h-5 text-white/40" />
</div> ) : (
)}) <ChevronDown className="w-5 h-5 text-white/40" />
)}
{/* 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> </button>
)}
</div>
</div>
{/* Mode Toggle */} {/* Section Items - BETTER CONTRAST */}
<div className="mt-4 flex items-center gap-2"> {isExpanded && (
<button <div className="border-t border-white/10">
onClick={() => setFastMode(!fastMode)} {section.items.map((item) => {
className={clsx( const statusStyle = getStatusColor(item.status)
"flex items-center gap-1.5 px-3 py-1.5 text-[10px] font-mono uppercase tracking-wider border transition-all", const StatusIcon = statusStyle.icon
fastMode
? "border-accent/30 bg-accent/10 text-accent" return (
: "border-white/[0.08] text-white/40 hover:text-white" <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"
<Zap className="w-3 h-3" /> >
Fast Mode <div className="flex items-start gap-4">
</button> {/* Status Indicator - LARGER */}
{data?.cached && ( <div className={clsx(
<span className="text-[10px] font-mono text-white/30 px-2 py-1 border border-white/[0.08]"> "w-10 h-10 flex items-center justify-center shrink-0",
Cached statusStyle.bg, statusStyle.border, "border"
</span> )}>
)} {StatusIcon && <StatusIcon className={clsx("w-5 h-5", statusStyle.text)} />}
</div> </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>
)} )}
</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]"> <div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{/* Desktop Table Header */} {/* 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]"> <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 Domain
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button> </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 Len
{sortField === 'length' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'length' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button> </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 When
{sortField === 'date' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'date' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button> </button>

View File

@ -25,7 +25,7 @@ Wir teilen Pounce nun final in **4 operative Module**. Das Konzept "Intent Routi
*„Lass das Asset arbeiten.“* *„Lass das Asset arbeiten.“*
* **Die Vision:** Verwandlung von toten Namen in aktive "Intent Router". * **Die Vision:** Verwandlung von toten Namen in aktive "Intent Router".
* **Der Mechanismus:** * **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". 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. 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€). * **Der Clou:** Kein "Parking" (Werbung für 0,01€), sondern "Routing" (Leads für 20,00€).