From c23d3c4b6c95c3bf0a4778793e853864774e5d29 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Wed, 17 Dec 2025 16:15:05 +0100 Subject: [PATCH] Fix AnalyzePanel syntax error, restore working version + Trends & Forge redesign --- YIELD_INTEGRATION_CONCEPT.md | 10 +- YIELD_SETUP.md | 10 +- backend/app/api/yield_routing.py | 6 +- backend/app/config.py | 8 +- frontend/src/app/page.tsx | 2 +- .../src/components/analyze/AnalyzePanel.tsx | 746 +++++++----------- frontend/src/components/hunt/DropsTab.tsx | 6 +- pounce_endgame.md | 2 +- 8 files changed, 296 insertions(+), 494 deletions(-) diff --git a/YIELD_INTEGRATION_CONCEPT.md b/YIELD_INTEGRATION_CONCEPT.md index 3e32db6..c2c7a9d 100644 --- a/YIELD_INTEGRATION_CONCEPT.md +++ b/YIELD_INTEGRATION_CONCEPT.md @@ -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 | diff --git a/YIELD_SETUP.md b/YIELD_SETUP.md index fd6f455..4e4fb88 100644 --- a/YIELD_SETUP.md +++ b/YIELD_SETUP.md @@ -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 ~^(?.+)$; # 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; diff --git a/backend/app/api/yield_routing.py b/backend/app/api/yield_routing.py index 11ba332..49b4955 100644 --- a/backend/app/api/yield_routing.py +++ b/backend/app/api/yield_routing.py @@ -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} diff --git a/backend/app/config.py b/backend/app/config.py index e389e7a..c914870 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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]: diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 98f7e7d..0c17e19 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -396,7 +396,7 @@ export default function HomePage() {
{[ - { 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) => ( diff --git a/frontend/src/components/analyze/AnalyzePanel.tsx b/frontend/src/components/analyze/AnalyzePanel.tsx index a6efd92..199eedf 100644 --- a/frontend/src/components/analyze/AnalyzePanel.tsx +++ b/frontend/src/components/analyze/AnalyzePanel.tsx @@ -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 = { - 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 = { - 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(null) const [data, setData] = useState(null) const [copied, setCopied] = useState(false) - const [activeSection, setActiveSection] = useState('authority') - const [yieldIntent, setYieldIntent] = useState(null) + const [expandedSections, setExpandedSections] = useState>({ + 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 (
{/* Backdrop */} -
+
- {/* Panel */} -
+ {/* Panel - WIDER & MORE READABLE */} +
- {/* ════════════════════════════════════════════════════════════════════ */} - {/* HEADER */} - {/* ════════════════════════════════════════════════════════════════════ */} -
+ {/* Header */} +
{/* Top Bar */} -
-
-
- +
+
+
+
-
-
Domain Analysis
-
+
+
Domain Analysis
+
{headerDomain}
-
+
- +
- {/* ════════════════════════════════════════════════════════════════════ */} - {/* HERO: Score + Recommendation */} - {/* ════════════════════════════════════════════════════════════════════ */} - {!loading && data && ( -
-
- {/* Pounce Score */} -
-
- Pounce Score - -
-
- {pounceScore} -
-
- {pounceScore >= 80 ? 'Excellent investment' : pounceScore >= 60 ? 'Good opportunity' : pounceScore >= 40 ? 'Proceed with caution' : 'High risk'} + {/* Score Bar - LARGER */} + {overallScore && !loading && ( +
+
+
= 70 ? "text-accent" : overallScore.score >= 40 ? "text-amber-400" : "text-red-400" + )}> + {overallScore.score} +
+
+
Overall Score
+
+
+
+
- - {/* Recommendation Badge */} -
- - {recommendation.label} +
+ {overallScore.pass} + {overallScore.warn} + {overallScore.fail}
- - {/* Trademark Warning */} - {trademark.risk && ( -
- -
-
Trademark Risk Detected
-
- Contains "{trademark.match}" - potential legal issues. Research before buying. -
-
-
- )} - - {/* Yield Intent Tip */} - {yieldIntent && yieldIntent.monetization_potential !== 'low' && ( -
- -
-
- Yield Potential - - {yieldIntent.monetization_potential} - -
-
- {yieldIntent.intent?.category?.replace(/_/g, ' ')} - {yieldIntent.intent?.suggested_partners?.length > 0 && ( - → {yieldIntent.intent.suggested_partners.slice(0, 2).join(', ')} - )} -
-
- -
- )}
)} - {/* ════════════════════════════════════════════════════════════════════ */} - {/* SECTION TABS */} - {/* ════════════════════════════════════════════════════════════════════ */} - {!loading && data && ( -
- {data.sections.map((section) => { - const config = SECTION_CONFIG[section.key] || SECTION_CONFIG.authority - const isActive = activeSection === section.key - const colorMap: Record = { - 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 ( - - ) - })} -
- )} + {/* Mode Toggle */} +
+ + {data?.cached && ( + + ⚡ Cached + + )} +
- {/* ════════════════════════════════════════════════════════════════════ */} - {/* CONTENT */} - {/* ════════════════════════════════════════════════════════════════════ */} + {/* Body - BETTER SPACING */}
{loading ? ( -
+
- -
Analyzing domain...
+ +
Analyzing domain...
) : error ? ( -
-
-
Analysis Failed
-
{error}
+
+
+
Analysis Failed
+
{error}
) : !data ? ( -
-
No data available
+
+
No data available
) : ( -
- {/* Active Section Items */} - {data.sections.filter(s => s.key === activeSection).map((section) => { - const sectionConfig = SECTION_CONFIG[section.key] || SECTION_CONFIG.authority +
+ {visibleSections.map((section) => { + const SectionIcon = getSectionIcon(section.key) + const sectionStyle = getSectionColor(section.key) + const isExpanded = expandedSections[section.key] !== false + return ( -
- {/* Section Description */} -
-
- - {sectionConfig.label} -
-

{sectionConfig.description}

-
- - {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 ( -
-
-
{item.label}
-
- -
- {tooltip} -
-
-
-
- {(item.value as any[]).slice(0, 12).map((row: any) => ( -
- .{row.tld} - {row.status === 'available' && } -
- ))} -
-
- ) - } - - return ( -
-
- {/* Status Icon */} -
- {StatusIcon && } -
- - {/* Content */} -
-
-
- {item.label} - {tooltip && ( -
- -
- {tooltip} -
-
- )} -
- {item.source} -
- -
- {formatValue(item.value)} -
- - {/* Radio Test Details */} - {item.key === 'radio_test' && item.details && (() => { - const d = item.details as Record - return ( -
- {d.syllables !== undefined && ( - - {d.syllables} syllables - - )} - {d.length !== undefined && ( - - {d.length} chars - - )} - {d.has_hyphen && ( - - has hyphen - - )} - {d.has_digits && ( - - has digits - - )} -
- ) - })()} - - {/* Registrar Details */} - {(item.key === 'tld_cheapest_register_usd' || item.key === 'tld_cheapest_renew_usd') && item.details && (() => { - const d = item.details as Record - return d.registrar ? ( -
- via {d.registrar} -
- ) : null - })()} -
-
+
+ {/* Section Header - LARGER */} +
- )}) - - {/* Quick Actions */} -
-
Quick Actions
-
- - {isAvailable && yieldIntent?.monetization_potential !== 'low' && ( - - )} -
-
- {/* Mode Toggle */} -
- - {data?.cached && ( - - ⚡ Cached - - )} -
+ {/* Section Items - BETTER CONTRAST */} + {isExpanded && ( +
+ {section.items.map((item) => { + const statusStyle = getStatusColor(item.status) + const StatusIcon = statusStyle.icon + + return ( +
+
+ {/* Status Indicator - LARGER */} +
+ {StatusIcon && } +
+ + {/* Content - BETTER READABILITY */} +
+
+ + {item.label} + + + {item.source} + +
+ + {/* Value - LARGER TEXT */} +
+ {isMatrix(item) ? ( +
+ {(item.value as any[]).slice(0, 12).map((row: any) => ( +
+ {String(row.domain)} + {row.status === 'available' && } +
+ ))} +
+ ) : ( +
+ {formatValue(item.value)} +
+ )} +
+ + {/* Details Toggle */} + {item.details && Object.keys(item.details).length > 0 && ( +
+ + View raw details + +
+                                        {JSON.stringify(item.details, null, 2)}
+                                      
+
+ )} +
+
+
+ ) + })} +
+ )} +
+ ) + })}
)}
diff --git a/frontend/src/components/hunt/DropsTab.tsx b/frontend/src/components/hunt/DropsTab.tsx index c3f3e29..bd105db 100644 --- a/frontend/src/components/hunt/DropsTab.tsx +++ b/frontend/src/components/hunt/DropsTab.tsx @@ -400,15 +400,15 @@ export function DropsTab({ showToast }: DropsTabProps) {
{/* Desktop Table Header */}
- - - diff --git a/pounce_endgame.md b/pounce_endgame.md index 350c69c..91462fc 100644 --- a/pounce_endgame.md +++ b/pounce_endgame.md @@ -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€).