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
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:
@ -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 |
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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€).
|
||||
|
||||
Reference in New Issue
Block a user