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
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:
@ -235,6 +235,7 @@ async def browse_listings(
|
||||
min_price: Optional[float] = Query(None, ge=0),
|
||||
max_price: Optional[float] = Query(None, ge=0),
|
||||
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"]),
|
||||
limit: int = Query(20, le=50),
|
||||
offset: int = Query(0, ge=0),
|
||||
@ -283,6 +284,11 @@ async def browse_listings(
|
||||
# Save it for future requests
|
||||
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(
|
||||
domain=listing.domain,
|
||||
slug=listing.slug,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react'
|
||||
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 { Toast, useToast } from '@/components/Toast'
|
||||
import {
|
||||
@ -45,6 +45,14 @@ import Image from 'next/image'
|
||||
// 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 {
|
||||
if (!renewalDate) return null
|
||||
const renDate = new Date(renewalDate)
|
||||
@ -82,6 +90,8 @@ export default function PortfolioPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshingId, setRefreshingId] = 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 [verifyingDomain, setVerifyingDomain] = useState<PortfolioDomain | null>(null)
|
||||
const [filter, setFilter] = useState<'all' | 'active' | 'sold'>('all')
|
||||
@ -97,23 +107,57 @@ export default function PortfolioPage() {
|
||||
// Yield domains - to show which are in Yield
|
||||
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 isScout = tier === 'scout'
|
||||
|
||||
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 () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [domainsData, summaryData, yieldData] = await Promise.all([
|
||||
const [domainsData, summaryData] = await Promise.all([
|
||||
api.getPortfolio(),
|
||||
api.getPortfolioSummary(),
|
||||
api.getYieldDomains().catch(() => ({ domains: [] }))
|
||||
api.getPortfolioSummary()
|
||||
])
|
||||
setDomains(domainsData)
|
||||
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) {
|
||||
console.error('Failed to load portfolio:', err)
|
||||
} finally {
|
||||
@ -241,21 +285,21 @@ export default function PortfolioPage() {
|
||||
|
||||
{/* Stats Grid */}
|
||||
<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-[8px] font-mono text-white/30 uppercase tracking-wider">Active</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-[8px] font-mono text-accent/60 uppercase tracking-wider">Value</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")}>
|
||||
{formatROI(summary?.overall_roi || 0)}
|
||||
</div>
|
||||
<div className="text-[8px] font-mono text-white/30 uppercase tracking-wider">ROI</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-[8px] font-mono text-accent/60 uppercase tracking-wider">Verified</div>
|
||||
</div>
|
||||
@ -283,21 +327,21 @@ export default function PortfolioPage() {
|
||||
</div>
|
||||
|
||||
<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-[9px] font-mono text-white/30 uppercase tracking-wider">Invested</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-[9px] font-mono text-white/30 uppercase tracking-wider">Value</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")}>
|
||||
{formatROI(summary?.overall_roi || 0)}
|
||||
</div>
|
||||
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">ROI</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-[9px] font-mono text-white/30 uppercase tracking-wider">Verified</div>
|
||||
</div>
|
||||
@ -313,13 +357,14 @@ export default function PortfolioPage() {
|
||||
{/* Filters */}
|
||||
<div className="flex items-center gap-1">
|
||||
{[
|
||||
{ value: 'all', label: 'All', count: stats.total },
|
||||
{ value: 'active', label: 'Active', count: stats.active },
|
||||
{ value: 'sold', label: 'Sold', count: stats.sold },
|
||||
{ value: 'all', label: 'All', count: stats.total, tip: 'Show all domains' },
|
||||
{ value: 'active', label: 'Active', count: stats.active, tip: 'Show only active domains' },
|
||||
{ value: 'sold', label: 'Sold', count: stats.sold, tip: 'Show sold domains' },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.value}
|
||||
onClick={() => setFilter(item.value as typeof filter)}
|
||||
title={item.tip}
|
||||
className={clsx(
|
||||
"px-3 py-2 text-[10px] font-mono uppercase tracking-wider border transition-colors",
|
||||
filter === item.value
|
||||
@ -335,6 +380,7 @@ export default function PortfolioPage() {
|
||||
{/* Add Domain Button */}
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<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]">
|
||||
{/* 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]">
|
||||
<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
|
||||
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
|
||||
</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
|
||||
{sortField === 'value' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
|
||||
</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
|
||||
{sortField === 'roi' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
|
||||
</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
|
||||
{sortField === 'renewal' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
|
||||
</button>
|
||||
<div className="text-center">Status</div>
|
||||
<div className="text-right">Actions</div>
|
||||
<div className="text-center" title="Verification, Yield, and Listing status">Status</div>
|
||||
<div className="text-right" title="Manage domain">Actions</div>
|
||||
</div>
|
||||
|
||||
{filteredDomains.map((domain) => {
|
||||
@ -406,21 +452,58 @@ export default function PortfolioPage() {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<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">
|
||||
{domain.registrar && (
|
||||
<div className="flex items-center gap-1.5 mt-0.5 flex-wrap">
|
||||
{domain.registrar && (
|
||||
<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 && (
|
||||
<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" /> Verified
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
title="Domain ownership verified via DNS"
|
||||
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()) && (
|
||||
<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">
|
||||
<Coins className="w-2.5 h-2.5" /> Yield
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
title="Earning passive income via Yield"
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
|
||||
@ -435,7 +518,10 @@ export default function PortfolioPage() {
|
||||
{/* Mobile Actions */}
|
||||
{!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 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" />
|
||||
{daysUntilRenewal !== null ? (
|
||||
<span className={isRenewingSoon ? "text-orange-400" : ""}>
|
||||
@ -443,29 +529,50 @@ export default function PortfolioPage() {
|
||||
</span>
|
||||
) : '—'}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!domain.is_dns_verified && (
|
||||
<button
|
||||
<div className="flex items-center gap-1.5">
|
||||
{!domain.is_dns_verified ? (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
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)}
|
||||
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" />
|
||||
</button>
|
||||
<button
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(domain.id, domain.domain)}
|
||||
disabled={deletingId === domain.id}
|
||||
title="Remove from portfolio"
|
||||
className="p-1.5 text-white/30 hover:text-rose-400"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -519,43 +626,100 @@ export default function PortfolioPage() {
|
||||
{/* Status */}
|
||||
<div className="flex justify-center gap-1">
|
||||
{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 ? (
|
||||
<>
|
||||
<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" />
|
||||
</span>
|
||||
{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" />
|
||||
</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)}
|
||||
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"
|
||||
>
|
||||
>
|
||||
Verify
|
||||
</button>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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 && (
|
||||
<Link
|
||||
href="/terminal/listing"
|
||||
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"
|
||||
>
|
||||
Sell
|
||||
</Link>
|
||||
listedDomains.has(domain.domain.toLowerCase()) ? (
|
||||
<Link
|
||||
href="/terminal/listing"
|
||||
title="This domain is listed for sale. Click to manage your listings."
|
||||
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"
|
||||
>
|
||||
<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
|
||||
onClick={() => handleRefreshValue(domain.id)}
|
||||
disabled={refreshingId === domain.id}
|
||||
title="Refresh estimated value based on current market data"
|
||||
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"
|
||||
)}
|
||||
>
|
||||
@ -564,7 +728,8 @@ export default function PortfolioPage() {
|
||||
<button
|
||||
onClick={() => handleDelete(domain.id, domain.domain)}
|
||||
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" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user