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