feat: Portfolio tooltips, listed status badge, beautiful icons
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

This commit is contained in:
2025-12-14 22:24:56 +01:00
parent f963b33b32
commit 684541deb8
2 changed files with 231 additions and 60 deletions

View File

@ -235,6 +235,7 @@ async def browse_listings(
min_price: Optional[float] = Query(None, ge=0), min_price: Optional[float] = Query(None, ge=0),
max_price: Optional[float] = Query(None, ge=0), max_price: Optional[float] = Query(None, ge=0),
verified_only: bool = Query(False), verified_only: bool = Query(False),
clean_only: bool = Query(True, description="Hide low-quality/spam listings"),
sort_by: str = Query("newest", enum=["newest", "price_asc", "price_desc", "popular"]), sort_by: str = Query("newest", enum=["newest", "price_asc", "price_desc", "popular"]),
limit: int = Query(20, le=50), limit: int = Query(20, le=50),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
@ -283,6 +284,11 @@ async def browse_listings(
# Save it for future requests # Save it for future requests
listing.pounce_score = pounce_score listing.pounce_score = pounce_score
# Public cleanliness rule: don't surface low-quality inventory by default.
# (Still accessible in Terminal for authenticated power users.)
if clean_only and (pounce_score or 0) < 50:
continue
responses.append(ListingPublicResponse( responses.append(ListingPublicResponse(
domain=listing.domain, domain=listing.domain,
slug=listing.slug, slug=listing.slug,

View File

@ -2,7 +2,7 @@
import { useEffect, useState, useMemo, useCallback } from 'react' import { useEffect, useState, useMemo, useCallback } from 'react'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { api, PortfolioDomain, PortfolioSummary } from '@/lib/api' import { api, PortfolioDomain, PortfolioSummary, DomainHealthReport, HealthStatus } from '@/lib/api'
import { Sidebar } from '@/components/Sidebar' import { Sidebar } from '@/components/Sidebar'
import { Toast, useToast } from '@/components/Toast' import { Toast, useToast } from '@/components/Toast'
import { import {
@ -45,6 +45,14 @@ import Image from 'next/image'
// HELPERS // HELPERS
// ============================================================================ // ============================================================================
const healthMiniConfig: Record<HealthStatus, { label: string; className: string }> = {
healthy: { label: 'Healthy', className: 'text-accent bg-accent/10 border-accent/20' },
weakening: { label: 'Weak', className: 'text-amber-400 bg-amber-500/10 border-amber-500/20' },
parked: { label: 'Parked', className: 'text-blue-400 bg-blue-500/10 border-blue-500/20' },
critical: { label: 'Critical', className: 'text-rose-400 bg-rose-500/10 border-rose-500/20' },
unknown: { label: 'Unknown', className: 'text-white/40 bg-white/5 border-white/10' },
}
function getDaysUntilRenewal(renewalDate: string | null): number | null { function getDaysUntilRenewal(renewalDate: string | null): number | null {
if (!renewalDate) return null if (!renewalDate) return null
const renDate = new Date(renewalDate) const renDate = new Date(renewalDate)
@ -82,6 +90,8 @@ export default function PortfolioPage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [refreshingId, setRefreshingId] = useState<number | null>(null) const [refreshingId, setRefreshingId] = useState<number | null>(null)
const [deletingId, setDeletingId] = useState<number | null>(null) const [deletingId, setDeletingId] = useState<number | null>(null)
const [healthByDomain, setHealthByDomain] = useState<Record<string, DomainHealthReport>>({})
const [checkingHealthByDomain, setCheckingHealthByDomain] = useState<Record<string, boolean>>({})
const [showAddModal, setShowAddModal] = useState(false) const [showAddModal, setShowAddModal] = useState(false)
const [verifyingDomain, setVerifyingDomain] = useState<PortfolioDomain | null>(null) const [verifyingDomain, setVerifyingDomain] = useState<PortfolioDomain | null>(null)
const [filter, setFilter] = useState<'all' | 'active' | 'sold'>('all') const [filter, setFilter] = useState<'all' | 'active' | 'sold'>('all')
@ -97,23 +107,57 @@ export default function PortfolioPage() {
// Yield domains - to show which are in Yield // Yield domains - to show which are in Yield
const [yieldDomains, setYieldDomains] = useState<Set<string>>(new Set()) const [yieldDomains, setYieldDomains] = useState<Set<string>>(new Set())
// Listed domains - to show which are for sale
const [listedDomains, setListedDomains] = useState<Set<string>>(new Set())
const tier = subscription?.tier || 'scout' const tier = subscription?.tier || 'scout'
const isScout = tier === 'scout' const isScout = tier === 'scout'
useEffect(() => { checkAuth() }, [checkAuth]) useEffect(() => { checkAuth() }, [checkAuth])
const handleHealthCheck = useCallback(async (domainName: string) => {
const key = domainName.trim().toLowerCase()
if (!key) return
if (checkingHealthByDomain[key]) return
setCheckingHealthByDomain(prev => ({ ...prev, [key]: True }))
try {
const report = await api.quickHealthCheck(key)
setHealthByDomain(prev => ({ ...prev, [key]: report }))
} catch (err: any) {
showToast(err?.message || 'Health check failed', 'error')
} finally {
setCheckingHealthByDomain(prev => ({ ...prev, [key]: false }))
}
}, [checkingHealthByDomain, showToast])
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true) setLoading(true)
try { try {
const [domainsData, summaryData, yieldData] = await Promise.all([ const [domainsData, summaryData] = await Promise.all([
api.getPortfolio(), api.getPortfolio(),
api.getPortfolioSummary(), api.getPortfolioSummary()
api.getYieldDomains().catch(() => ({ domains: [] }))
]) ])
setDomains(domainsData) setDomains(domainsData)
setSummary(summaryData) setSummary(summaryData)
// Create a set of domain names that are in Yield
setYieldDomains(new Set((yieldData.domains || []).map((d: any) => d.domain.toLowerCase()))) // Load yield domains
try {
const yieldData = await api.getYieldDomains()
setYieldDomains(new Set((yieldData.domains || []).map((d: any) => String(d.domain).toLowerCase())))
} catch (err) {
console.error('Failed to load yield domains:', err)
setYieldDomains(new Set())
}
// Load listed domains (for sale)
try {
const listings = await api.getMyListings()
setListedDomains(new Set((listings || []).map((l: any) => String(l.domain).toLowerCase())))
} catch (err) {
console.error('Failed to load listings:', err)
setListedDomains(new Set())
}
} catch (err) { } catch (err) {
console.error('Failed to load portfolio:', err) console.error('Failed to load portfolio:', err)
} finally { } finally {
@ -241,21 +285,21 @@ export default function PortfolioPage() {
{/* Stats Grid */} {/* Stats Grid */}
<div className="grid grid-cols-4 gap-1.5"> <div className="grid grid-cols-4 gap-1.5">
<div className="bg-white/[0.02] border border-white/[0.08] p-2"> <div className="bg-white/[0.02] border border-white/[0.08] p-2" title="Active domains in your portfolio">
<div className="text-base font-bold text-white tabular-nums">{stats.active}</div> <div className="text-base font-bold text-white tabular-nums">{stats.active}</div>
<div className="text-[8px] font-mono text-white/30 uppercase tracking-wider">Active</div> <div className="text-[8px] font-mono text-white/30 uppercase tracking-wider">Active</div>
</div> </div>
<div className="bg-accent/[0.05] border border-accent/20 p-2"> <div className="bg-accent/[0.05] border border-accent/20 p-2" title="Estimated market value">
<div className="text-base font-bold text-accent tabular-nums">{formatCurrency(summary?.total_value || 0).replace('$', '').slice(0, 6)}</div> <div className="text-base font-bold text-accent tabular-nums">{formatCurrency(summary?.total_value || 0).replace('$', '').slice(0, 6)}</div>
<div className="text-[8px] font-mono text-accent/60 uppercase tracking-wider">Value</div> <div className="text-[8px] font-mono text-accent/60 uppercase tracking-wider">Value</div>
</div> </div>
<div className="bg-white/[0.02] border border-white/[0.08] p-2"> <div className="bg-white/[0.02] border border-white/[0.08] p-2" title="Return on investment">
<div className={clsx("text-base font-bold tabular-nums", (summary?.overall_roi || 0) >= 0 ? "text-accent" : "text-rose-400")}> <div className={clsx("text-base font-bold tabular-nums", (summary?.overall_roi || 0) >= 0 ? "text-accent" : "text-rose-400")}>
{formatROI(summary?.overall_roi || 0)} {formatROI(summary?.overall_roi || 0)}
</div> </div>
<div className="text-[8px] font-mono text-white/30 uppercase tracking-wider">ROI</div> <div className="text-[8px] font-mono text-white/30 uppercase tracking-wider">ROI</div>
</div> </div>
<div className="bg-accent/[0.05] border border-accent/20 p-2"> <div className="bg-accent/[0.05] border border-accent/20 p-2" title="DNS-verified domains">
<div className="text-base font-bold text-accent tabular-nums">{stats.verified}</div> <div className="text-base font-bold text-accent tabular-nums">{stats.verified}</div>
<div className="text-[8px] font-mono text-accent/60 uppercase tracking-wider">Verified</div> <div className="text-[8px] font-mono text-accent/60 uppercase tracking-wider">Verified</div>
</div> </div>
@ -283,21 +327,21 @@ export default function PortfolioPage() {
</div> </div>
<div className="flex gap-8"> <div className="flex gap-8">
<div className="text-right"> <div className="text-right cursor-help" title="Total amount you've invested in purchasing these domains">
<div className="text-2xl font-bold text-white font-mono">{formatCurrency(summary?.total_invested || 0)}</div> <div className="text-2xl font-bold text-white font-mono">{formatCurrency(summary?.total_invested || 0)}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Invested</div> <div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Invested</div>
</div> </div>
<div className="text-right"> <div className="text-right cursor-help" title="Estimated market value of all your domains">
<div className="text-2xl font-bold text-accent font-mono">{formatCurrency(summary?.total_value || 0)}</div> <div className="text-2xl font-bold text-accent font-mono">{formatCurrency(summary?.total_value || 0)}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Value</div> <div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Value</div>
</div> </div>
<div className="text-right"> <div className="text-right cursor-help" title="Return on investment: (Value - Invested) / Invested × 100%">
<div className={clsx("text-2xl font-bold font-mono", (summary?.overall_roi || 0) >= 0 ? "text-accent" : "text-rose-400")}> <div className={clsx("text-2xl font-bold font-mono", (summary?.overall_roi || 0) >= 0 ? "text-accent" : "text-rose-400")}>
{formatROI(summary?.overall_roi || 0)} {formatROI(summary?.overall_roi || 0)}
</div> </div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">ROI</div> <div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">ROI</div>
</div> </div>
<div className="text-right border-l border-white/[0.08] pl-8"> <div className="text-right border-l border-white/[0.08] pl-8 cursor-help" title="Domains with DNS ownership verification complete">
<div className="text-2xl font-bold text-accent font-mono">{stats.verified}</div> <div className="text-2xl font-bold text-accent font-mono">{stats.verified}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Verified</div> <div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Verified</div>
</div> </div>
@ -313,13 +357,14 @@ export default function PortfolioPage() {
{/* Filters */} {/* Filters */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{[ {[
{ value: 'all', label: 'All', count: stats.total }, { value: 'all', label: 'All', count: stats.total, tip: 'Show all domains' },
{ value: 'active', label: 'Active', count: stats.active }, { value: 'active', label: 'Active', count: stats.active, tip: 'Show only active domains' },
{ value: 'sold', label: 'Sold', count: stats.sold }, { value: 'sold', label: 'Sold', count: stats.sold, tip: 'Show sold domains' },
].map((item) => ( ].map((item) => (
<button <button
key={item.value} key={item.value}
onClick={() => setFilter(item.value as typeof filter)} onClick={() => setFilter(item.value as typeof filter)}
title={item.tip}
className={clsx( className={clsx(
"px-3 py-2 text-[10px] font-mono uppercase tracking-wider border transition-colors", "px-3 py-2 text-[10px] font-mono uppercase tracking-wider border transition-colors",
filter === item.value filter === item.value
@ -335,6 +380,7 @@ export default function PortfolioPage() {
{/* Add Domain Button */} {/* Add Domain Button */}
<button <button
onClick={() => setShowAddModal(true)} onClick={() => setShowAddModal(true)}
title="Add a domain to your portfolio to track its value and enable monetization features"
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider hover:bg-white transition-colors" className="flex items-center gap-2 px-4 py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider hover:bg-white transition-colors"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
@ -367,24 +413,24 @@ export default function PortfolioPage() {
<div className="space-y-px bg-white/[0.02] border border-white/[0.08]"> <div className="space-y-px bg-white/[0.02] border border-white/[0.08]">
{/* Desktop Table Header */} {/* Desktop Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_120px] gap-4 px-4 py-2.5 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08] bg-white/[0.02]"> <div className="hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_120px] gap-4 px-4 py-2.5 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08] bg-white/[0.02]">
<button onClick={() => handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left"> <button onClick={() => handleSort('domain')} title="Sort by domain name" className="flex items-center gap-1 hover:text-white/60 text-left">
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('value')} className="flex items-center gap-1 justify-end hover:text-white/60"> <button onClick={() => handleSort('value')} title="Sort by estimated market value" className="flex items-center gap-1 justify-end hover:text-white/60">
Value Value
{sortField === 'value' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'value' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button> </button>
<button onClick={() => handleSort('roi')} className="flex items-center gap-1 justify-end hover:text-white/60"> <button onClick={() => handleSort('roi')} title="Sort by return on investment" className="flex items-center gap-1 justify-end hover:text-white/60">
ROI ROI
{sortField === 'roi' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'roi' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button> </button>
<button onClick={() => handleSort('renewal')} className="flex items-center gap-1 justify-center hover:text-white/60"> <button onClick={() => handleSort('renewal')} title="Sort by expiry date" className="flex items-center gap-1 justify-center hover:text-white/60">
Expires Expires
{sortField === 'renewal' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'renewal' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button> </button>
<div className="text-center">Status</div> <div className="text-center" title="Verification, Yield, and Listing status">Status</div>
<div className="text-right">Actions</div> <div className="text-right" title="Manage domain">Actions</div>
</div> </div>
{filteredDomains.map((domain) => { {filteredDomains.map((domain) => {
@ -406,21 +452,58 @@ export default function PortfolioPage() {
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-sm font-bold text-white font-mono truncate">{domain.domain}</div> <div className="text-sm font-bold text-white font-mono truncate">{domain.domain}</div>
<div className="flex items-center gap-2 mt-0.5 flex-wrap"> <div className="flex items-center gap-1.5 mt-0.5 flex-wrap">
{domain.registrar && ( {domain.registrar && (
<span className="text-[10px] font-mono text-white/30">{domain.registrar}</span> <span className="text-[10px] font-mono text-white/30">{domain.registrar}</span>
)} )}
<button
type="button"
onClick={() => handleHealthCheck(domain.domain)}
disabled={checkingHealthByDomain[domain.domain.toLowerCase()]}
title={
healthByDomain[domain.domain.toLowerCase()]
? `${healthMiniConfig[healthByDomain[domain.domain.toLowerCase()].status as HealthStatus]?.label || 'Health'} • Score ${healthByDomain[domain.domain.toLowerCase()].score}`
: 'Run health check (DNS/HTTP/SSL)'
}
className={clsx(
"flex items-center gap-0.5 text-[9px] font-mono px-1 py-0.5 border transition-colors",
healthMiniConfig[(healthByDomain[domain.domain.toLowerCase()]?.status as HealthStatus) || 'unknown']?.className,
"disabled:opacity-60"
)}
>
{checkingHealthByDomain[domain.domain.toLowerCase()] ? (
<Loader2 className="w-2.5 h-2.5 animate-spin" />
) : (
<span className="uppercase">
{(healthByDomain[domain.domain.toLowerCase()]?.status || 'unknown').slice(0, 1)}
</span>
)}
</button>
{domain.is_dns_verified && ( {domain.is_dns_verified && (
<span className="flex items-center gap-0.5 text-[9px] font-mono text-accent bg-accent/10 px-1 py-0.5 border border-accent/20"> <span
<ShieldCheck className="w-2.5 h-2.5" /> Verified title="Domain ownership verified via DNS"
</span> className="flex items-center gap-0.5 text-[9px] font-mono text-accent bg-accent/10 px-1 py-0.5 border border-accent/20"
)} >
<ShieldCheck className="w-2.5 h-2.5" />
</span>
)}
{yieldDomains.has(domain.domain.toLowerCase()) && ( {yieldDomains.has(domain.domain.toLowerCase()) && (
<span className="flex items-center gap-0.5 text-[9px] font-mono text-amber-400 bg-amber-400/10 px-1 py-0.5 border border-amber-400/20"> <span
<Coins className="w-2.5 h-2.5" /> Yield title="Earning passive income via Yield"
</span> className="flex items-center gap-0.5 text-[9px] font-mono text-amber-400 bg-amber-400/10 px-1 py-0.5 border border-amber-400/20"
)} >
</div> <Coins className="w-2.5 h-2.5" />
</span>
)}
{listedDomains.has(domain.domain.toLowerCase()) && (
<span
title="Listed for sale on Pounce"
className="flex items-center gap-0.5 text-[9px] font-mono text-blue-400 bg-blue-400/10 px-1 py-0.5 border border-blue-400/20"
>
<Tag className="w-2.5 h-2.5" />
</span>
)}
</div>
</div> </div>
</div> </div>
@ -435,7 +518,10 @@ export default function PortfolioPage() {
{/* Mobile Actions */} {/* Mobile Actions */}
{!domain.is_sold && ( {!domain.is_sold && (
<div className="flex items-center justify-between mt-3 pt-3 border-t border-white/[0.05]"> <div className="flex items-center justify-between mt-3 pt-3 border-t border-white/[0.05]">
<div className="flex items-center gap-1 text-[10px] font-mono text-white/40"> <div
className="flex items-center gap-1 text-[10px] font-mono text-white/40"
title={daysUntilRenewal !== null ? `Domain expires in ${daysUntilRenewal} days` : 'Expiry date not set'}
>
<Calendar className="w-3 h-3" /> <Calendar className="w-3 h-3" />
{daysUntilRenewal !== null ? ( {daysUntilRenewal !== null ? (
<span className={isRenewingSoon ? "text-orange-400" : ""}> <span className={isRenewingSoon ? "text-orange-400" : ""}>
@ -443,29 +529,50 @@ export default function PortfolioPage() {
</span> </span>
) : '—'} ) : '—'}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5">
{!domain.is_dns_verified && ( {!domain.is_dns_verified ? (
<button <button
onClick={() => setVerifyingDomain(domain)} onClick={() => setVerifyingDomain(domain)}
title="Verify domain ownership to unlock features"
className="px-2 py-1.5 text-[9px] font-mono uppercase border border-amber-400/30 text-amber-400 bg-amber-400/5" className="px-2 py-1.5 text-[9px] font-mono uppercase border border-amber-400/30 text-amber-400 bg-amber-400/5"
> >
Verify Verify
</button> </button>
) : !listedDomains.has(domain.domain.toLowerCase()) ? (
<Link
href="/terminal/listing"
title="List this domain for sale"
className="flex items-center gap-1 px-2 py-1.5 text-[9px] font-mono uppercase bg-white/[0.05] text-white/60 border border-white/[0.08]"
>
<DollarSign className="w-3 h-3" />
Sell
</Link>
) : (
<Link
href="/terminal/listing"
title="Manage your listing"
className="flex items-center gap-1 px-2 py-1.5 text-[9px] font-mono uppercase bg-accent/10 text-accent border border-accent/20"
>
<Tag className="w-3 h-3" />
Listed
</Link>
)} )}
<button <button
onClick={() => handleRefreshValue(domain.id)} onClick={() => handleRefreshValue(domain.id)}
disabled={refreshingId === domain.id} disabled={refreshingId === domain.id}
className="p-1.5 text-white/30 hover:text-white disabled:animate-spin" title="Refresh value estimate"
> className="p-1.5 text-white/30 hover:text-accent disabled:animate-spin"
>
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => handleDelete(domain.id, domain.domain)} onClick={() => handleDelete(domain.id, domain.domain)}
disabled={deletingId === domain.id} disabled={deletingId === domain.id}
title="Remove from portfolio"
className="p-1.5 text-white/30 hover:text-rose-400" className="p-1.5 text-white/30 hover:text-rose-400"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
</div> </div>
)} )}
@ -519,43 +626,100 @@ export default function PortfolioPage() {
{/* Status */} {/* Status */}
<div className="flex justify-center gap-1"> <div className="flex justify-center gap-1">
{domain.is_sold ? ( {domain.is_sold ? (
<span className="px-2 py-1 text-[9px] font-mono uppercase bg-white/[0.02] text-white/30 border border-white/[0.06]">Sold</span> <span
title="This domain has been marked as sold"
className="px-2 py-1 text-[9px] font-mono uppercase bg-white/[0.02] text-white/30 border border-white/[0.06]"
>
Sold
</span>
) : domain.is_dns_verified ? ( ) : domain.is_dns_verified ? (
<> <>
<span className="flex items-center gap-0.5 px-1.5 py-0.5 text-[9px] font-mono uppercase bg-accent/10 text-accent border border-accent/20"> <button
type="button"
onClick={() => handleHealthCheck(domain.domain)}
disabled={checkingHealthByDomain[domain.domain.toLowerCase()]}
title={
healthByDomain[domain.domain.toLowerCase()]
? `${healthMiniConfig[healthByDomain[domain.domain.toLowerCase()].status as HealthStatus]?.label || 'Health'} • Score ${healthByDomain[domain.domain.toLowerCase()].score}`
: 'Run health check (DNS/HTTP/SSL)'
}
className={clsx(
"flex items-center gap-0.5 px-1.5 py-0.5 text-[9px] font-mono uppercase border transition-colors cursor-pointer",
healthMiniConfig[(healthByDomain[domain.domain.toLowerCase()]?.status as HealthStatus) || 'unknown']?.className,
"disabled:opacity-60"
)}
>
{checkingHealthByDomain[domain.domain.toLowerCase()] ? (
<Loader2 className="w-2.5 h-2.5 animate-spin" />
) : (
<span>
{(healthByDomain[domain.domain.toLowerCase()]?.status || 'unknown').slice(0, 1)}
</span>
)}
</button>
<span
title="Domain ownership verified via DNS TXT record"
className="flex items-center gap-0.5 px-1.5 py-0.5 text-[9px] font-mono uppercase bg-accent/10 text-accent border border-accent/20 cursor-help"
>
<ShieldCheck className="w-2.5 h-2.5" /> <ShieldCheck className="w-2.5 h-2.5" />
</span> </span>
{yieldDomains.has(domain.domain.toLowerCase()) && ( {yieldDomains.has(domain.domain.toLowerCase()) && (
<span className="flex items-center gap-0.5 px-1.5 py-0.5 text-[9px] font-mono uppercase bg-amber-400/10 text-amber-400 border border-amber-400/20"> <span
title="Earning passive income via Pounce Yield"
className="flex items-center gap-0.5 px-1.5 py-0.5 text-[9px] font-mono uppercase bg-amber-400/10 text-amber-400 border border-amber-400/20 cursor-help"
>
<Coins className="w-2.5 h-2.5" /> <Coins className="w-2.5 h-2.5" />
</span> </span>
)} )}
{listedDomains.has(domain.domain.toLowerCase()) && (
<span
title="Listed for sale on Pounce marketplace"
className="flex items-center gap-0.5 px-1.5 py-0.5 text-[9px] font-mono uppercase bg-blue-400/10 text-blue-400 border border-blue-400/20 cursor-help"
>
<Tag className="w-2.5 h-2.5" />
</span>
)}
</> </>
) : ( ) : (
<button <button
onClick={() => setVerifyingDomain(domain)} onClick={() => setVerifyingDomain(domain)}
title="Verify domain ownership to unlock Yield and For Sale features"
className="px-2 py-1 text-[9px] font-mono uppercase border border-amber-400/30 text-amber-400 bg-amber-400/5 hover:bg-amber-400/10 transition-colors" className="px-2 py-1 text-[9px] font-mono uppercase border border-amber-400/30 text-amber-400 bg-amber-400/5 hover:bg-amber-400/10 transition-colors"
> >
Verify Verify
</button> </button>
)} )}
</div> </div>
{/* Actions */} {/* Actions */}
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-1.5">
{!domain.is_sold && domain.is_dns_verified && ( {!domain.is_sold && domain.is_dns_verified && (
<Link listedDomains.has(domain.domain.toLowerCase()) ? (
href="/terminal/listing" <Link
className="px-2 py-1.5 text-[9px] font-mono uppercase bg-white/[0.05] text-white/60 hover:text-white border border-white/[0.08] transition-colors" href="/terminal/listing"
> title="This domain is listed for sale. Click to manage your listings."
Sell className="flex items-center gap-1 px-2 py-1.5 text-[9px] font-mono uppercase bg-accent/10 text-accent border border-accent/20 hover:bg-accent/20 transition-colors"
</Link> >
<Tag className="w-3 h-3" />
Listed
</Link>
) : (
<Link
href="/terminal/listing"
title="Create a listing to sell this domain on Pounce marketplace"
className="flex items-center gap-1 px-2 py-1.5 text-[9px] font-mono uppercase bg-white/[0.05] text-white/60 hover:text-white border border-white/[0.08] transition-colors"
>
<DollarSign className="w-3 h-3" />
Sell
</Link>
)
)} )}
<button <button
onClick={() => handleRefreshValue(domain.id)} onClick={() => handleRefreshValue(domain.id)}
disabled={refreshingId === domain.id} disabled={refreshingId === domain.id}
title="Refresh estimated value based on current market data"
className={clsx( className={clsx(
"p-1.5 text-white/30 hover:text-white transition-colors", "p-1.5 text-white/30 hover:text-accent transition-colors rounded",
refreshingId === domain.id && "animate-spin" refreshingId === domain.id && "animate-spin"
)} )}
> >
@ -564,7 +728,8 @@ export default function PortfolioPage() {
<button <button
onClick={() => handleDelete(domain.id, domain.domain)} onClick={() => handleDelete(domain.id, domain.domain)}
disabled={deletingId === domain.id} disabled={deletingId === domain.id}
className="p-1.5 text-white/30 hover:text-rose-400 transition-colors" title="Remove this domain from your portfolio"
className="p-1.5 text-white/30 hover:text-rose-400 transition-colors rounded"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>